지난 3년간의 자바스크립트 및 타입스크립트의 모든 기능 리뷰

0

이 글은 Linus Schlumberger의
‘All JavaScript and TypeScript Features of the last 3 years’
라는 글을 참고했습니다.


이 글에서는 지난 3년 동안 자바스크립트와 타입스크립트에 추가된 대부분의 변경 사항을 살펴보지만, 일부 변경 사항은 더 이전에 추가된 기능입니다.

살펴보는 모든 기능이 모두 우리의 작업과 관련이 있거나 실용적인 것은 아니지만, 어떤 것이 가능한지 보여주고, 언어에 대한 이해를 심화시키는 데는 도움이 될 것입니다.

각 자바스크립트와 타입스크립트의 기능들은 오래된 것을 우선 순위로 하여 정리했습니다.

자바스크립트

아직도 유효한 이전 방식

템플릿 리터럴 태그

템플릿 리터럴 앞에 함수 이름을 붙이면 함수에 템플릿 리터럴템플릿 값의 일부가 전달되는데, 이 기능에는 몇 가지 흥미로운 용도가 있습니다.

숫자가 포함된 임의의 문자열을 기록하되, 숫자의 형식을 지정하는 방법을 작성하는 경우
// 이를 위해 태그가 지정된 템플릿을 사용할 수 있습니다.
function formatNumbers(strings: TemplateStringsArray, number: number): string {
  return strings[0] + number.toFixed(2) + strings[1];
}
console.log(formatNumbers`This is the value: ${0}, it's important.`); // This is the value: 0.00, it's important.

// 또는 문자열 내에서 키를 '소문자로 변경'하려는 경우입니다.
function translateKey(key: string): string {
  return key.toLocaleLowerCase();
}
function translate(strings: TemplateStringsArray, ...expressions: string[]): string {
  return strings.reduce((accumulator, currentValue, index) => accumulator + currentValue + translateKey(expressions[index] ?? ''), '');
}
console.log(translate`Hello, this is ${'NAME'} to say ${'MESSAGE'}.`); // Hello, this is name to say message.

심볼

객체의 고유한 키로 내부적으로 사용됩니다. Symbol("foo") === Symbol("foo");의 결과는 false입니다.

const obj: { [index: string]: string } = {};

const symbolA = Symbol('a');
const symbolB = Symbol.for('b');

console.log(symbolA.description); // "a"

obj[symbolA] = 'a';
obj[symbolB] = 'b';
obj['c'] = 'c';
obj.d = 'd';

console.log(obj[symbolA]); // "a"
console.log(obj[symbolB]); // "b"

// 해당 키는 다른 심볼이나 심볼 없이는 접근할 수 없습니다.
console.log(obj[Symbol('a')]); // undefined
console.log(obj['a']); // undefined

// for...in 구문을 사용하여 열거할 때, 해당 키는 열거되지 않습니다
for (const i in obj) {
  console.log(i); // "c", "d"
}

ES2020

옵셔널 체이닝

인덱싱을 통해 정의되지 않은 객체의 값에 접근할 필요가 있을 때, 부모 객체 이름 뒤에 ?를 사용하여 옵셔널 체이닝을 사용할 수 있습니다. 이 기능은 인덱싱([...])이나 함수 호출에도 사용할 수 있습니다.

이전 방식
// 객체 변수 또는 다른 구조가 정의되어 있는지 확실하지 않은 경우,
// 프로퍼티에 쉽게 접근할 수 없습니다.
const object: { name: string } | undefined = Math.random() > 0.5 ? undefined : { name: 'test' };
const value = object.name; // 타입 에러: 'object'가 'undefined'일 수 있습니다.

// 먼저 정의되어 있는지 확인할 수 있지만,
// 이렇게 하면 가독성이 떨어지고 중첩된 객체의 경우 사용하기가 복잡해집니다.
const objectOld: { name: string } | undefined = Math.random() > 0.5 ? undefined : { name: 'test' };
const valueOld = objectOld ? objectOld.name : undefined;
새로운 방식
// 대신 옵셔널 체이닝을 이용할 수 있습니다.
const objectNew: { name: string } | undefined = Math.random() > 0.5 ? undefined : { name: 'test' };
const valueNew = objectNew?.name;

// 인덱싱 및 함수에도 사용할 수 있습니다.
const array: string[] | undefined = Math.random() > 0.5 ? undefined : ['test'];
const item = array?.[0];
const func: (() => string) | undefined = Math.random() > 0.5 ? undefined : () => 'test';
const result = func?.();

널 병합 연산자:

조건부 할당에 || 연산자를 사용하는 대신 새로운 ?? 연산자를 사용할 수 있습니다. 모든 falsy 값에 적용되는 || 연산자와 달리, nullundefined에만 적용됩니다.

const value: string | undefined = Math.random() > 0.5 ? undefined : 'test';
이전 방식
// 만약 값을 할당하려는데 해당 값이 undefined나 null일 경우, "||" 연산자를 사용하여 다른 값으로 조건부 할당할 수 있습니다.
const anotherValue = value || 'hello';
console.log(anotherValue); // "test" 또는 "hello"

// 이 방법은 truthy한 값에 대해서는 잘 작동하지만, 0이나 빈 문자열과 비교할 경우에도 조건이 적용되어 버리는 문제가 있습니다.
const incorrectValue = '' || 'incorrect';
console.log(incorrectValue); // 항상 "incorrect"
const anotherIncorrectValue = 0 || 'incorrect';
console.log(anotherIncorrectValue); // 항상 "incorrect"
새로운 방식
// 대신 새로운 널 병합 연산자를 사용할 수 있습니다. 이 연산자는 오직 undefined와 null 값에만 적용됩니다.
const newValue = value ?? 'hello';
console.log(newValue) // "test" 또는 "hello"

// falsy 값인 경우, 대체되지 않습니다.
const correctValue = '' ?? 'incorrect';
console.log(correctValue); // 항상 ""
const anotherCorrectValue = 0 ?? 'incorrect';
console.log(anotherCorrectValue); // 항상 0

import()

동적 import 입니다. import ... from ...과 비슷하지만, 런타임에 동작하고, 변수를 사용합니다.

let importModule;
if (shouldImport) {
  importModule = await import('./module.mjs');
}

String.matchAll

루프를 사용하지 않고 캡처 그룹을 포함하여 정규식의 여러 개의 일치 항목을 가져옵니다.

const stringVar = 'testhello,testagain,';
이전 방식
// 일치하는 결과를 가져올 수는 있지만, 캡처 그룹은 가져올 수 없습니다.
console.log(stringVar.match(/test([\w]+?),/g)); // ["testhello,", "testagain,"]

// 캡처 그룹을 포함하여 한 개의 일치 결과만 가져옵니다.
const singleMatch = stringVar.match(/test([\w]+?),/);
if (singleMatch) {
  console.log(singleMatch[0]); // "testhello,"
  console.log(singleMatch[1]); // "hello"
}

// 동일한 결과를 얻지만 매우 직관적이지 않습니다. (실행 메서드는 마지막 인덱스를 저장합니다.)
// 상태를 저장하기 위해 루프 외부에서 정의되어야 하며 전역(/g)이어야 합니다.
// 그렇지 않으면 무한 루프가 생성됩니다.
const regex = /test([\w]+?),/g;
let execMatch;
while ((execMatch = regex.exec(stringVar)) !== null) {
  console.log(execMatch[0]); // "testhello,", "testagain,"
  console.log(execMatch[1]); // "hello", "again"
}
새로운 방식
// 정규식은 전역(/g)이어야 하며, 그렇지 않으면 의미가 없습니다.
const matchesIterator = stringVar.matchAll(/test([\w]+?),/g);
// 직접 인덱싱하지 않고 반복하거나 Array.from()을 이용하여 배열로 변환해야 합니다.
for (const match of matchesIterator) {
  console.log(match[0]); // "testhello,", "testagain,"
  console.log(match[1]); // "hello", "again"
}

Promise.allSettled()

Promise.all()과 비슷하지만 모든 Promise가 완료될 때까지 기다리며 첫 번째 reject/throw시, 반환하지 않습니다. 모든 에러를 더 쉽게 처리할 수 있습니다.

async function success1() {return 'a'}
async function success2() {return 'b'}
async function fail1() {throw 'fail 1'}
async function fail2() {throw 'fail 2'}
이전 방식
console.log(await Promise.all([success1(), success2()])); // ["a", "b"]
// 하지만 오류가 있는 경우, 다음과 같습니다.
try {
  await Promise.all([success1(), success2(), fail1(), fail2()]);
} catch (e) {
  console.log(e); // "fail 1"
}
// NOTE: 한 개의 오류만 포착할 수 있으며 성공 값에 접근할 수 없습니다.

// 차선책으로 이전 방식을 다음과 같이 수정할 수 있습니다.
console.log(await Promise.all([ // ["a", "b", undefined, undefined]
  success1().catch(e => { console.log(e); }),
  success2().catch(e => { console.log(e); }),
  fail1().catch(e => { console.log(e); }), // "fail 1"
  fail2().catch(e => { console.log(e); })])); // "fail 2"
새로운 방식
const results = await Promise.allSettled([success1(), success2(), fail1(), fail2()]);
const sucessfulResults = results
  .filter(result => result.status === 'fulfilled')
  .map(result => (result as PromiseFulfilledResult<string>).value);
console.log(sucessfulResults); // ["a", "b"]
results.filter(result => result.status === 'rejected').forEach(error => {
  console.log((error as PromiseRejectedResult).reason); // "fail 1", "fail 2"
});

// 또는 다음과 같습니다.
for (const result of results) {
  if (result.status === 'fulfilled') {
    console.log(result.value); // "a", "b"
  } else if (result.status === 'rejected') {
    console.log(result.reason); // "fail 1", "fail 2"
  }
}

BigInt

새로운 BigInt 데이터 타입을 사용하면 큰 정수의 숫자를 정확하게 저장하고 연산할 수 있으므로 자바스크립트가 숫자를 부동소수점으로 저장하면서 발생하는 오류를 방지할 수 있습니다. BigInt() 생성자(오류를 방지하기 위해 가능하면 문자열과 함께 사용하는 것이 좋습니다.)를 사용하여 생성하거나 숫자 뒤에 n을 추가하여 생성할 수 있습니다.

이전 방식
// 자바스크립트는 숫자를 부동소수점으로 저장하기 때문에 항상 약간의 부정확성이 존재합니다.
// 하지만 더 중요한 것은 일정 숫자 이상의 정수 연산에서 부정확성이 발생하기 시작한다는 점입니다.
const maxSafeInteger = 9007199254740991;
console.log(maxSafeInteger === Number.MAX_SAFE_INTEGER); // true

// 이보다 큰 숫자를 비교하면 부정확할 수 있습니다.
console.log(Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2);
새로운 방식
// 새로운 BigInt 데이터 타입을 사용하면 이론적으로 무한대 크기의 정수 숫자를 저장하고 연산할 수 있습니다.
// BigInt 생성자를 사용하거나 숫자 끝에 'n'을 추가하여 사용할 수 있습니다.
const maxSafeIntegerPreviously = 9007199254740991n;
console.log(maxSafeIntegerPreviously); // 9007199254740991

const anotherWay = BigInt(9007199254740991);
console.log(anotherWay); // 9007199254740991

// 숫자를 사용하여 생성자를 사용하면 MAX_SAFE_INTEGER보다 큰 정수를 안전하게 전달할 수 없습니다.
const incorrect = BigInt(9007199254740992);
console.log(incorrect); // 9007199254740992
const incorrectAgain = BigInt(9007199254740993);
console.log(incorrectAgain); // 9007199254740992
// 이런, 동일한 값으로 변환됩니다.

// 대신 문자열이나 더 나은 구문을 사용하세요.
const correct = BigInt('9007199254740993');
console.log(correct); // 9007199254740993
const correctAgain = 9007199254740993n;
console.log(correctAgain); // 9007199254740993

// 16진수, 8진수, 2진수도 문자열로 전달할 수 있습니다
const hex = BigInt('0x1fffffffffffff');
console.log(hex); // 9007199254740991
const octal = BigInt('0o377777777777777777');
console.log(octal); // 9007199254740991
const binary = BigInt('0b11111111111111111111111111111111111111111111111111111');
console.log(binary); // 9007199254740991

// 대부분의 산술 연산은 예상한 대로 작동하지만, 다른 연산자도 BigInt여야 합니다. 모든 연산은 BigInt를 반환합니다.
const addition = maxSafeIntegerPreviously + 2n;
console.log(addition); // 9007199254740993

const multiplication = maxSafeIntegerPreviously * 2n;
console.log(multiplication); // 18014398509481982

const subtraction = multiplication - 10n;
console.log(subtraction); // 18014398509481972

const modulo = multiplication % 10n;
console.log(modulo); // 2

const exponentiation = 2n ** 54n;
console.log(exponentiation); // 18014398509481984

const exponentiationAgain = 2n^54n;
console.log(exponentiationAgain); // 18014398509481984

const negative = exponentiation * -1n;
console.log(negative); // -18014398509481984

// 나눗셈은 약간 다르게 작동하는데, BigInt는 정수만 저장할 수 있기 때문입니다.
const division = multiplication / 2n;
console.log(division); // 9007199254740991
// 나눌 수 있는 정수의 경우 이 방법이 잘 작동합니다.

// 하지만 나눌 수 없는 숫자의 경우, 정수 나눗셈(반내림)처럼 작동합니다
const divisionAgain = 5n / 2n;
console.log(divisionAgain); // 2

// BigInt가 아닌 숫자와 엄격한 동등성은 없지만 느슨한 동등성은 있습니다
console.log(0n === 0); // false
console.log(0n == 0); // true

// 그러나 비교는 예상대로 작동합니다.
console.log(1n < 2); // true
console.log(2n > 1); // true
console.log(2 > 2); // false
console.log(2n > 2); // false
console.log(2n >= 2); // true

// "bigint" 타입 입니다.
console.log(typeof 1n); // "bigint"

// BigInt는 일반적인 숫자(부호가 있는 정수 및 부호 없는 정수 -> 음수가 없음)로 다시 변환할 수 있습니다.
// 물론 이 경우 정확도가 떨어집니다. 유효 자릿수를 지정할 수 있습니다.
console.log(BigInt.asIntN(0, -2n)); // 0
console.log(BigInt.asIntN(1, -2n)); // 0
console.log(BigInt.asIntN(2, -2n)); // -2

// 일반적으로 더 많은 비트 수를 사용합니다.
// 부호가 없는 숫자로 변환할 때 음수는 2의 보수로 변환됩니다.
console.log(BigInt.asUintN(8, -2n)); // 254

globalThis

브라우저, Node.js 등과 같은 환경에 관계없이 전역 컨텍스트 내 변수에 접근할 수 있습니다. 여전히 나쁜 습관으로 여겨지지만 때로는 필요합니다. 브라우저의 최상위 수준에서 this와 유사합니다.

console.log(globalThis.Math); // Math 객체

import.meta

ES 모듈을 사용하는 경우, import.meta.url을 사용하여 현재 모듈의 URL을 가져옵니다.

console.log(import.meta.url); // "file://..."

export * as … from …

기본값을 서브모듈로 쉽게 다시 내보낼 수 있습니다.

export * as am from 'another-module'
import { am } from 'module'

ES2021

String.replaceAll()

항상 정규식을 사용하는 대신, 전역 플래그(/g)를 사용해 문자열 내의 모든 부분 문자열 인스턴스를 바꿉니다.

const testString = 'hello/greetings everyone/everybody';
이전 방식
// 첫 번째 인스턴스만 대체합니다.
console.log(testString.replace('/', '|')); // 'hello|greetings everyone/everybody'

// 대신 성능이 더 나쁘고 이스케이프가 필요한 정규식을 사용해야 했습니다.
// 글로벌 플래그(/g)가 아닙니다.
console.log(testString.replace(/\//g, '|')); // 'hello|greetings everyone|everybody'
새로운 방식
// replaceAll을 사용하면 훨씬 더 명확하고 빠릅니다
console.log(testString.replaceAll('/', '|')); // 'hello|greetings everyone|everybody'

Promise.any

Promise 목록 중 하나의 결과만 필요한 경우, 첫 번째 결과를 반환합니다. 즉시 거부되는 Promise.race와 달리 모든 Promise가 거부되었을 때만 거부되고 AggregateError를 반환합니다.

async function success1() {return 'a'}
async function success2() {return 'b'}
async function fail1() {throw 'fail 1'}
async function fail2() {throw 'fail 2'}
이전 방식
console.log(await Promise.race([success1(), success2()])); // "a"
// 하지만 오류가 있는 경우, 다음과 같습니다.
try {
  await Promise.race([fail1(), fail2(), success1(), success2()]);
} catch (e) {
  console.log(e); // "fail 1"
}
// 알림: 하나의 오류만 catch하고 성공 값에 접근할 수 없습니다.

// 차선책으로 이전 방식을 다음과 같이 수정할 수 있습니다.
console.log(await Promise.race([ // "a"
  fail1().catch(e => { console.log(e); }), // "fail 1"
  fail2().catch(e => { console.log(e); }), // "fail 2"
  success1().catch(e => { console.log(e); }),
  success2().catch(e => { console.log(e); })]));
새로운 방식
console.log(await Promise.any([fail1(), fail2(), success1(), success2()])); // "a"
// 모든 프로미스가 거부될 때만 거부하고 모든 오류가 포함된 AggregateError를 반환합니다.
try {
  await Promise.any([fail1(), fail2()]);
} catch (e) {
  console.log(e); // [AggregateError: All promises were rejected]
  console.log(e.errors); // ["fail 1", "fail 2"]
}

Nullish coalescing 할당

??=는 이전에 “nullish”이었을 때(null 또는 undefined)만 값을 할당합니다.

let x1 = undefined;
let x2 = 'a';
const getNewValue = () => 'b';

// undefined는 nullish이므로 x1에 새 값을 할당합니다.
x1 ??= 'b';
console.log(x1) // "b"

// 문자열은 nullish가 아니므로 x2에 새 값을 할당하지 않습니다.
// 참고: 또한 getNewValue()는 절대 실행되지 않습니다.
x2 ??= getNewValue();
console.log(x2) // "a"

논리적 AND 할당

&&=는 이전에 “truthy”이었을 때(true 또는 true로 변환 가능한 값)만 값을 할당합니다.

let x1 = undefined;
let x2 = 'a';
const getNewValue = () => 'b';

// undefined는 truthy가 아니므로 x1에 새 값을 할당하지 않습니다.
// 참고: 또한 getNewValue()는 절대 실행되지 않습니다.
x1 &&= getNewValue();
console.log(x1) // undefined

// 문자열이 truthy이므로 x2에 새 값을 할당합니다.
x2 &&= 'b';
console.log(x2) // "b"

논리적 OR 할당 (||=):

||=는 이전에 “falsy”이었을 때(false 또는 false로 변환 가능한 값)만 값을 할당합니다.

let x1 = undefined;
let x2 = 'a';
const getNewValue = () => 'b';

// undefined는 falsy이므로 x1에 새 값을 할당합니다.
x1 ||= 'b';
console.log(x1) // "b"

// 문자열이 falsy가 아니므로 x2에 새 값을 할당하지 않습니다.
// 참고: 또한 getNewValue()는 절대 실행되지 않습니다.
x2 ||= getNewValue();
console.log(x2) // "a"

WeakRef

객체가 가비지 컬렉션되는 것을 막지 않고 객체에 대한 “약한” 참조를 유지합니다.

const ref = new WeakRef(element);

// 객체/엘리먼트가 여전히 존재하고 가비지 컬렉션되지 않은 경우 값을 가져옵니다.
const value = ref.deref;
console.log(value); // undefined
// 객체가 더 이상 존재하지 않는 것 같습니다.

숫자 리터럴 구분자

가독성을 높이기 위해 _를 사용하여 숫자를 구분합니다. 기능에는 영향을 주지 않습니다.

const int = 1_000_000_000;
const float = 1_000_000_000.999_999_999;
const max = 9_223_372_036_854_775_807n;
const binary = 0b1011_0101_0101;
const octal = 0o1234_5670;
const hex = 0xD0_E0_F0;

ES2022

Top level await

이제 ES 모듈의 최상위 수준에서 await 키워드를 사용할 수 있으므로 래퍼 함수가 필요하지 않아 오류 처리가 개선됩니다.

async function asyncFuncSuccess() {
  return 'test';
}
async function asyncFuncFail() {
  throw new Error('Test');
}
이전 방식
// Promise를 기다리는 것은 async 함수 안에서만 가능합니다.
// await asyncFuncSuccess(); // SyntaxError: await is only valid in async functions
// 따라서 우리는 이를 함수 안에 래핑해야 했고, 이로 인해 오류 처리와 최상위 동시성을 잃게 되었습니다.
try {
  (async () => {
    console.log(await asyncFuncSuccess()); // "test"
    try {
      await asyncFuncFail();
    } catch (e) {
      // 그렇지 않으면 오류가 전혀 발견되지 않거나 적절한 추적 없이 너무 늦게 발견되기 때문에 이 기능이 필요합니다.
      console.error(e); // Error: "Test"
      throw e;
    }
  })();
} catch (e) {
  // 이 함수는 비동기이기 때문에 트리거되지 않거나 적절한 추적 없이 너무 늦게 트리거될 수 있습니다.
  console.error(e);
}
// 비동기 함수를 기다릴 수 없기 때문에 Promise 결과보다 먼저 기록됩니다.
console.log('Hey'); // "Hey"
새로운 방식
// package.json에 설정되어 있고 내보내는 이름이 ".mts"인 경우인 경우, 최상위 레벨에서 기다릴 수 있습니다.
console.log(await asyncFuncSuccess()); // "test"
try {
  await asyncFuncFail();
} catch (e) {
  console.error(e); // Error: "Test"
}
// 모든 비동기 호출이 대기 중이므로 Promise 결과 이후에 기록됩니다.
console.log('Hello'); // "Hello"

#private

#로 시작하는 이름을 지정하여 클래스 멤버(프로퍼티 및 메서드)를 비공개로 설정합니다. 그러면 클래스 자체에서만 접근할 수 있으며 삭제하거나 동적으로 할당할 수 없습니다.

잘못된 동작이 발생하면 타입스크립트가 아닌 자바스크립트 구문 오류가 발생합니다. 타입스크립트 프로젝트에는 이 방법을 권장하지 않으며, 대신 기존 private 키워드를 사용하세요.

class ClassWithPrivateField {
  #privateField;
  #anotherPrivateField = 4;

  constructor() {
    this.#privateField = 42; // Valid
    this.#privateField; // Syntax error
    this.#undeclaredField = 444; // Syntax error
    console.log(this.#anotherPrivateField); // 4
  }
}

const instance = new ClassWithPrivateField();
instance.#privateField === 42; // Syntax error

정적 클래스 멤버

클래스 필드(프로퍼티 및 메서드)를 정적으로 표시합니다.

class Logger {
  static id = 'Logger1';
  static type = 'GenericLogger';
  static log(message: string | Error) {
    console.log(message);
  }
}

class ErrorLogger extends Logger {
  static type = 'ErrorLogger';
  static qualifiedType;
  static log(e: Error) {
    return super.log(e.toString());
  }
}

console.log(Logger.type); // "GenericLogger"
Logger.log('Test'); // "Test"

// 정적 전용 클래스의 인스턴스화는 쓸모가 없으며 여기서는 데모 목적으로만 수행됩니다.
const log = new Logger();

ErrorLogger.log(new Error('Test')); // Error: "Test"(부모 인스턴스화의 영향을 받지 않음)
console.log(ErrorLogger.type); // "ErrorLogger"
console.log(ErrorLogger.qualifiedType); // undefined
console.log(ErrorLogger.id); // "Logger1"

// log()가 인스턴스 메서드가 아니라 정적 메서드이기 때문에 발생하는 오류입니다.
console.log(log.log()); // log.log 는 함수가 아닙니다.

클래스에서의 정적 초기화 블록

클래스가 초기화될 때 실행되는 블록으로, 기본적으로 정적 멤버의 “생성자”입니다.

class Test {
  static staticProperty1 = 'Property 1';
  static staticProperty2;
  static {
    this.staticProperty2 = 'Property 2';
  }
}

console.log(Test.staticProperty1); // "Property 1"
console.log(Test.staticProperty2); // "Property 2"

Import Assertions()

V8에서 구현된 비표준 기능으로, import ... from ... assert { type: 'json' }를 사용하여 가져온 모듈의 타입을 단언합니다. 이를 통해 JSON을 파싱하지 않고 직접 가져올 수 있습니다.

import json from './foo.json' assert { type: 'json' };
console.log(json.answer); // 42

정규식 매치 인덱스

정규식 일치 및 캡처 그룹에 대한 시작 및 끝 인덱스를 가져옵니다. 이 기능은 RegExp.exec(), String.match()String.matchAll()에서 동작합니다.

const matchObj = /(test+)(hello+)/d.exec('start-testesthello-stop');
이전 방식
console.log(matchObj?.index);
새로운 방식
if (matchObj) {
  // 전체 일치하는 문자열의 시작 및 종료 인덱스 (이전에는 시작 인덱스만 있었습니다.)
  console.log(matchObj.indices[0]); // [9, 18]

  // 캡처 그룹의 시작 및 종료 인덱스
  console.log(matchObj.indices[1]); // [9, 13]
  console.log(matchObj.indices[2]); // [13, 18]
}

음수 인덱싱

배열이나 문자열을 인덱싱할 때 at()을 이용하여 끝부터 인덱싱할 수 있습니다. 이는 값을 가져올 때 arr[arr.length - 1]와 동일합니다(할당은 불가능).

console.log([4, 5].at(-1)) // 5

const array = [4, 5];
array.at(-1) = 3; // SyntaxError: Assigning to rvalue

hasOwn

obj.hasOwnProperty() 대신 권장하는 객체가 가지고 있는 프로퍼티를 찾는 방법입니다. 일부 엣지 케이스에서 더 잘 동작합니다.

const obj = { name: 'test' };

console.log(Object.hasOwn(obj, 'name')); // true
console.log(Object.hasOwn(obj, 'gender')); // false

Error cause

이제 Error에 선택적인 원인을 지정할 수 있으며, 오류를 다시 발생시킬 때 원래 오류를 지정할 수 있습니다.

try {
  try {
    connectToDatabase();
  } catch (err) {
    throw new Error('Connecting to database failed.', { cause: err });
  }
} catch (err) {
  console.log(err.cause); // ReferenceError: connectToDatabase is not defined
}

미래

미래에 적용될 것으로 생각되지만, 이미 TypeScript 4.9에서는 사용이 가능한 기능입니다.

Auto-Accessor

자동으로 프로퍼티를 비공개로 설정하고 해당 프로퍼티에 대한 get/set 접근자를 생성합니다.

class Person {
  accessor name: string;

  constructor(name: string) {
    this.name = name;
    console.log(this.name) // 'test'
  }
}

const person = new Person('test');

타입스크립트

기본 사항

제네릭

타입을 다른 타입으로 전달합니다. 이렇게 하면 타입을 일반화하면서도 여전히 타입 안전성을 유지할 수 있습니다. any 또는 unknown을 사용하는 것보다 항상 이 방법을 우선시하는 것이 좋습니다.

제네릭을 사용하지 않는 경우
function getFirstUnsafe(list: any[]): any {
  return list[0];
}

const firstUnsafe = getFirstUnsafe(['test']); // any
제네릭을 사용하는 경우
function getFirst<Type>(list: Type[]): Type {
  return list[0];
}

const first = getFirst<string>(['test']); // string

// 이 경우 인자를 통해 매개변수를 유추할 수 있으므로 매개변수를 삭제할 수도 있습니다.
const firstInferred = getFirst(['test']); // string

// 제네릭으로 허용되는 타입은 `extends`를 사용하여 제한할 수도 있습니다. 일반적으로 타입은 T로 줄여 씁니다.
class List<T extends string | number> {
  private list: T[] = [];

  get(key: number): T {
    return this.list[key];
  }

  push(value: T): void {
    this.list.push(value);
  }
}

const list = new List<string>();
list.push(9); // Type error: Argument of type 'number' is not assignable to parameter of type 'string'.
const booleanList = new List<boolean>(); // Type error: Type 'boolean' does not satisfy the constraint 'string | number'.

과거

아직도 유효하게 사용할 수 있는 이전 방식의 기능입니다.

유틸리티 타입

타입스크립트에는 많은 유틸리티 타입이 포함되어 있으며, 가장 유용한 몇 가지를 설명합니다.

interface Test {
  name: string;
  age: number;
}

// Partial 유틸리티 타입은 모든 프로퍼티들을 옵셔널하게 만듭니다.
type TestPartial = Partial<Test>; // { name?: string | undefined; age?: number | undefined; }

// Required 유틸리티 타입은 Partial과 반대로 모든 프로퍼티를 필수로 만듭니다.
type TestRequired = Required<TestPartial>; // { name: string; age: number; }

// Readonly 유틸리티 타입은 모든 프로퍼티들을 읽기 전용으로 만듭니다.
type TestReadonly = Readonly<Test>; // { readonly name: string; readonly age: string }

// Record 유틸리티 타입은 objects/maps/dictionaries의 간단한 정의를 가능하게 합니다. 가능한 경우 인덱스 시그니처 대신 Record 유틸리티 타입을 사용하는 것이 좋습니다.
const config: Record<string, boolean> = { option: false, anotherOption: true };

// Pick 유틸리티 타입은 지정된 프로퍼티만 가져옵니다.
type TestLess = Pick<Test, 'name'>; // { name: string; }
type TestBoth = Pick<Test, 'name' | 'age'>; // { name: string; age: string; }

// Omit 유틸리티 타입은 지정된 properties.type을 무시합니다.
type TestFewer = Omit<Test, 'name'>; // typed as { age: string; }
type TestNone = Omit<Test, 'name' | 'age'>; // typed as {}

// Parameters 유틸리티 타입은 함수 타입의 매개변수를 가져옵니다.
function doSmth(value: string, anotherValue: number): string {
  return 'test';
}
type Params = Parameters<typeof doSmth>; // [value: string, anotherValue: number]

// ReturnType 유틸리티 타입은 함수 타입의 반환 타입을 가져옵니다.
type Return = ReturnType<typeof doSmth>; // string

//* 더 많은 기능이 있습니다.

조건부 타입

어떤 타입이 다른 타입과 일치하거나 확장하는지에 따라 타입을 조건부로 설정합니다. 자바스크립트의 조건부(삼항) 연산자와 같은 방식으로 읽을 수 있습니다.

// 배열인 경우 배열 타입만 추출하고, 그렇지 않으면 동일한 타입을 반환합니다.
type Flatten<T> = T extends any[] ? T[number] : T;

// 배열의 요소에 대한 타입을 추출합니다.
type Str = Flatten<string[]>; // string

// 타입을 그대로 둡니다.
type Num = Flatten<number>; // number

조건부 타입으로 추론하기

모든 제네릭 타입을 사용자가 지정해야 하는 것은 아니며, 일부 타입은 코드에서 유추할 수도 있습니다. 추론된 타입을 기반으로 조건부 로직을 사용하려면 infer 키워드가 필요합니다. 이는 임시로 추론된 타입 변수를 정의합니다.

// 이전 예제를 기반으로 하여, 더 깔끔하게 작성할 수 있습니다.
type FlattenOld<T> = T extends any[] ? T[number] : T;

// 배열을 인덱싱하는 대신 배열에서 Item 타입을 추론할 수 있습니다.
type Flatten<T> = T extends (infer Item)[] ? Item : T;

// 만약 함수의 반환 타입을 가져오는 타입을 작성하고, 그렇지 않은 경우에는 undefined를 반환하도록 하고 싶다면, 이를 추론할 수도 있습니다.
type GetReturnType<Type> = Type extends (...args: any[]) => infer Return ? Return : undefined;

type Num = GetReturnType<() => number>; // number

type Str = GetReturnType<(x: string) => string>; // string

type Bools = GetReturnType<(a: boolean, b: boolean) => void>; // undefined

튜플 옵셔널 요소와 나머지

?를 사용하여 옵셔널한 요소를 튜플로 선언하고 나머지는 ...를 사용하여 다른 타입에 따라 선언합니다.

// 만약 튜플의 길이를 정확히는 모르지만 적어도 1 이상인 경우, `?`를 사용하여 선택적인 타입을 지정할 수 있습니다.
const list: [number, number?, boolean?] = [];
list[0] // number
list[1] // number | undefined
list[2] // boolean | undefined
list[3] // Type error: Tuple type '[number, (number | undefined)?, (boolean | undefined)?]' of length '3' has no element at index '3'.

// 기존 타입을 기반으로 튜플을 만들 수도 있습니다.
// 만약 배열의 시작 부분을 패딩하고 싶다면, rest 연산자 `...`를 사용하여 패딩할 수 있습니다.
function padStart<T extends any[]>(arr: T, pad: string): [string, ...T] {
  return [pad, ...arr];
}

const padded = padStart([1, 2], 'test'); // [string, number, number]

추상 클래스와 메소드

클래스와 그 안의 메서드는 추상적으로 선언하여 인스턴스화되지 않도록 할 수 있습니다.

abstract class Animal {
  abstract makeSound(): void;

  move(): void {
    console.log('roaming the earth...');
  }
}

// 확장 시 추상 메서드를 구현해야 합니다.
class Cat extends Animal {} // Compile error: Non-abstract class 'Cat' does not implement inherited abstract member 'makeSound' from class 'Animal'.

class Dog extends Animal {
  makeSound() {
    console.log('woof');
  }
}

// 추상 클래스는 인터페이스처럼 인스턴스화할 수 없으며 추상 메서드도 호출할 수 없습니다.
new Animal(); // Compile error: Cannot create an instance of an abstract class.

const dog = new Dog().makeSound(); // "woof"

생성자 시그니처

클래스 선언 외부에서 생성자 타입을 정의합니다. 대부분의 경우 사용해서는 안 되며, 대신 추상 클래스를 사용할 수 있습니다.

interface MyInterface {
  name: string;
}

interface ConstructsMyInterface {
  new(name: string): MyInterface;
}

class Test implements MyInterface {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

class AnotherTest {
  age: number;
}

function makeObj(n: ConstructsMyInterface) {
    return new n('hello!');
}

const obj = makeObj(Test); // Test
const anotherObj = makeObj(AnotherTest); // Type error: Argument of type 'typeof AnotherTest' is not assignable to parameter of type 'ConstructsMyInterface'.

ConstructorParameters 유틸리티 타입

클래스가 아닌 생성자 타입에서 생성자 매개변수를 가져오는 타입스크립트 헬퍼 함수입니다.

// makeObj 함수에 대한 생성자 인자를 가져오고 싶다면 어떻게 해야 할까요?
interface MyInterface {
  name: string;
}

interface ConstructsMyInterface {
  new(name: string): MyInterface;
}

class Test implements MyInterface {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

function makeObj(test: ConstructsMyInterface, ...args: ConstructorParameters<ConstructsMyInterface>) {
  return new test(...args);
}

makeObj(Test); // Type error: Expected 2 arguments, but got 1.
const obj = makeObj(Test, 'test'); // Test

TypeScript 4.0

가변 튜플 타입

이제 튜플의 나머지 요소는 제네릭일 수 있습니다. 이제 여러 개의 rest 요소 사용도 허용됩니다.

길이와 타입이 정의되지 않은 두 개의 튜플을 결합하는 함수가 있다면 어떨까요? 반환 타입을 어떻게 정의할 수 있을까요?

이전 방식
// 몇 가지 오버로드를 작성할 수 있습니다.
declare function concat(arr1: [], arr2: []): [];
declare function concat<A>(arr1: [A], arr2: []): [A];
declare function concat<A, B>(arr1: [A], arr2: [B]): [A, B];
declare function concat<A, B, C>(arr1: [A], arr2: [B, C]): [A, B, C];
declare function concat<A, B, C, D>(arr1: [A], arr2: [B, C, D]): [A, B, C, D];
declare function concat<A, B>(arr1: [A, B], arr2: []): [A, B];
declare function concat<A, B, C>(arr1: [A, B], arr2: [C]): [A, B, C];
declare function concat<A, B, C, D>(arr1: [A, B], arr2: [C, D]): [A, B, C, D];
declare function concat<A, B, C, D, E>(arr1: [A, B], arr2: [C, D, E]): [A, B, C, D, E];
declare function concat<A, B, C>(arr1: [A, B, C], arr2: []): [A, B, C];
declare function concat<A, B, C, D>(arr1: [A, B, C], arr2: [D]): [A, B, C, D];
declare function concat<A, B, C, D, E>(arr1: [A, B, C], arr2: [D, E]): [A, B, C, D, E];
declare function concat<A, B, C, D, E, F>(arr1: [A, B, C], arr2: [D, E, F]): [A, B, C, D, E, F];
// 각각 세 가지 항목만 봐도 이것은 정말 차선책입니다

// 대신 타입을 결합할 수 있습니다.
declare function concatBetter<T, U>(arr1: T[], arr2: U[]): (T | U)[];
// 하지만 이것은 (T | U)[] 타입 입니다.
새로운 방식
// 가변 튜플 타입을 사용하면 쉽게 정의하고 길이에 대한 정보를 유지할 수 있습니다.
declare function concatNew<T extends Arr, U extends Arr>(arr1: T, arr2: U): [...T, ...U];

const tuple = concatNew([23, 'hey', false] as [number, string, boolean], [5, 99, 20] as [number, number, number]);
console.log(tuple[0]); // 23
const element: number = tuple[1]; // Type error: Type 'string' is not assignable to type 'number'.
console.log(tuple[6]); // Type error: Tuple type '[23, "hey", false, 5, 99, 20]' of length '6' has no element at index '6'.

레이블링된 튜플 요소

이제 튜플 요소의 이름을 [start: number, end: number]와 같이 지정할 수 있습니다. 요소 중 하나에 이름이 지정된 경우, 모든 요소에 이름을 지정해야 합니다.

type Foo = [first: number, second?: string, ...rest: any[]];

// 이렇게 하면 여기에서 인자의 이름을 올바르게 지정할 수 있으며 에디터에도 표시됩니다.
declare function someFunc(...args: Foo);

생성자로부터 클래스 프로퍼티 추론

생성자에서 프로퍼티를 설정하면 이제 타입을 유추할 수 있으므로 더 이상 수동으로 설정할 필요가 없습니다.

class Animal {
  // 생성자에서 타입을 할당할 때 타입을 설정할 필요가 없습니다.
  name;

  constructor(name: string) {
    this.name = name;
    console.log(this.name); // string
  }
}

JSDoc @deprecated 지원

JSDoc/TSDoc @deprecated 태그는 이제 타입스트립트에서 인식됩니다.

/** @deprecated message */
type Test = string;

const test: Test = 'dfadsf'; // Type error: 'Test' is deprecated.

TypeScript 4.1

템플릿 리터럴 타입

리터럴 타입을 정의할 때 ${Type}과 같은 템플릿을 통해 타입을 지정할 수 있습니다. 이를 통해 여러 문자열 리터럴을 결합할 때와 같이 복잡한 문자열 타입을 구성할 수 있습니다.

type VerticalDirection = 'top' | 'bottom';
type HorizontalDirection = 'left' | 'right';
type Direction = `${VerticalDirection} ${HorizontalDirection}`;

const dir1: Direction = 'top left';
const dir2: Direction = 'left'; // Type error: Type '"left"' is not assignable to type '"top left" | "top right" | "bottom left" | "bottom right"'.
const dir3: Direction = 'left top'; // Type error: Type '"left top"' is not assignable to type '"top left" | "top right" | "bottom left" | "bottom right"'.

// 이 기능은 제네릭 및 새로운 유틸리티 타입과도 결합할 수 있습니다.
declare function makeId<T extends string, U extends string>(first: T, second: U): `${Capitalize<T>}-${Lowercase<U>}`;

매핑된 타입에서의 키 리매핑

[K in keyof T as NewKeyType]

T[K]와 같이 값은 그대로 사용하면서 매핑된 타입의 키를 다시 지정할 수 있습니다.

// 객체의 형식을 다시 지정하되 해당 객체의 ID에 밑줄을 추가하고 싶다고 가정해 보겠습니다.
const obj = { value1: 0, value2: 1, value3: 3 };
const newObj: { [Property in keyof typeof obj as `_${Property}`]: number }; // { _value1: number; _value2: number; _value3: number; }

재귀적인 조건부 타입

조건부 타입을 자신의 정의 내부에서 사용할 수 있습니다. 이를 통해 무한히 중첩된 값을 조건부로 언팩하는 타입을 만들 수 있습니다.

type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;

type P1 = Awaited<string>; // string
type P2 = Awaited<Promise<string>>; // string
type P3 = Awaited<Promise<Promise<string>>>; // string

JSDOC @see 태그를 지원하는 에디터

이제 에디터에서 JSDoc/TSDoc@see variable/type/link 태그가 지원됩니다.

const originalValue = 1;
/**
  * 다른 값을 복사합니다.
  * @see originalValue
  */
const value = originalValue;

tsc –explainFiles

--explainFiles 옵션은 타입스크립트 CLI에서 컴파일에 포함된 파일과 그 이유를 설명하는 데 사용할 수 있으며 디버깅에 유용할 수 있습니다.

** 대규모 프로젝트나 복잡한 설정의 경우, 이 옵션은 많은 출력을 생성하므로 대신 tsc --explainFiles | less 또는 이와 유사한 것을 사용하세요.

tsc --explainFiles

<<output
../../.asdf/installs/nodejs/16.13.1/.npm/lib/node_modules/typescript/lib/lib.es5.d.ts
  Library referenced via 'es5' from file '../../.asdf/installs/nodejs/16.13.1/.npm/lib/node_modules/typescript/lib/lib.es2015.d.ts'
  Library referenced via 'es5' from file '../../.asdf/installs/nodejs/16.13.1/.npm/lib/node_modules/typescript/lib/lib.es2015.d.ts'
../../.asdf/installs/nodejs/16.13.1/.npm/lib/node_modules/typescript/lib/lib.es2015.d.ts
  Library referenced via 'es2015' from file '../../.asdf/installs/nodejs/16.13.1/.npm/lib/node_modules/typescript/lib/lib.es2016.d.ts'
  Library referenced via 'es2015' from file '../../.asdf/installs/nodejs/16.13.1/.npm/lib/node_modules/typescript/lib/lib.es2016.d.ts'
../../.asdf/installs/nodejs/16.13.1/.npm/lib/node_modules/typescript/lib/lib.es2016.d.ts
  Library referenced via 'es2016' from file '../../.asdf/installs/nodejs/16.13.1/.npm/lib/node_modules/typescript/lib/lib.es2017.d.ts'
  Library referenced via 'es2016' from file '../../.asdf/installs/nodejs/16.13.1/.npm/lib/node_modules/typescript/lib/lib.es2017.d.ts'
...
output

분해된 변수는 명시적으로 사용하지 않는 것으로 표시 가능

구조 분해할 때 밑줄을 사용하여 변수를 사용되지 않은 것으로 표시할 수 있습니다. 이렇게 하면 타입스크립트에서 “사용되지 않은 변수” 오류가 발생하지 않습니다.

const [_first, second] = [3, 5];
console.log(second);

// 혹은 더 짧게 사용할 수 있습니다.
const [_, value] = [3, 5];
console.log(value);

TypeScript 4.3

프로퍼티에서 쓰기 타입 분리

이제 설정/조회 접근자를 정의할 때 쓰기/설정 타입읽기/조회 타입과 다를 수 있습니다. 이를 통해 동일한 값의 여러 형식을 허용하는 설정자를 사용할 수 있습니다.

class Test {
  private _value: number;

  get value(): number {
    return this._value;
  }

  set value(value: number | string) {
    if (typeof value === 'number') {
      this._value = value;
      return;
    }
    this._value = parseInt(value, 10);
  }
}

override

override를 사용하여 상속된 클래스 메서드를 명시적으로 오버라이드로 표시하면 상위 클래스가 변경될 때 타입스크립트에서 상위 메서드가 더 이상 존재하지 않음을 알릴 수 있습니다. 이를 통해 복잡한 상속 패턴을 보다 안전하게 관리할 수 있습니다.

class Parent {
  getName(): string {
    return 'name';
  }
}

class NewParent {
  getFirstName(): string {
    return 'name';
  }
}

class Test extends Parent {
  override getName(): string {
    return 'test';
  }
}

class NewTest extends NewParent {
  override getName(): string { // Type error: This member cannot have an 'override' modifier because it is not declared in the base class 'NewParent'.

'NewParent'.
    return 'test';
  }
}

정적 인덱스 시그니처

이제 클래스에서 정적 프로퍼티을 사용할 때, static [propName: string]: string을 사용하여 인덱스 시그니처를 설정할 수도 있습니다.

이전 방식
class Test {}

Test.test = ''; // Type error: Property 'test' does not exist on type 'typeof Test'.
새로운 방식
class NewTest {
  static [key: string]: string;
}

NewTest.test = '';

JSDOC @link 태그를 지원하는 에디터

JSDoc/TSDoc { 인라인 태그가 이제 지원되며, 에디터에서 표시되고 해결됩니다.

const originalValue = 1;
/**
  * {@link originalValue} 복사본
  */
const value = originalValue;

TypeScript 4.4

정확한 선택적 프로퍼티 타입

컴파일러 플래그 --exactOptionalPropertyTypes (또는 tsconfig.json에서)를 사용하면 암시적으로 undefined를 허용하는 프로퍼티에 대한 undefined 할당이 더 이상 허용되지 않습니다.

ex) property?: string)

대신 property: string | undefined와 같이 명시적으로 undefined를 허용해야 합니다.

class Test {
  name?: string;
  age: number | undefined;
}

const test = new Test();
test.name = undefined; // Type error: Type 'undefined' is not assignable to type 'string' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the type of the target.
test.age = undefined;
console.log(test.age); // undefined

TypeScript 4.5

Awaited 타입 및 Promise 개선

새로운 Awaited<> 유틸리티 타입은 무한하게 중첩된 Promises에서 값을 추출합니다.(await가 값을 추출하는 것과 동일) 이는 Promise.all()의 타입 추론도 개선시켰습니다.

// 만약 제네릭한 awaited 값을 가지고 싶다면, Awaited 유틸리티 타입을 사용할 수 있습니다.
// 이를 통해 무한하게 중첩된 Promises는 모두 값을 반환합니다. (이전 예제의 소스 코드에 포함되어 있습니다.)
type P1 = Awaited<string>; // string
type P2 = Awaited<Promise<string>>; // string
type P3 = Awaited<Promise<Promise<string>>>; // string

Import 명에 대한 type 수정자

일반적인 Import type이 아닌 import 문에서는 type 키워드를 사용하여 값이 타입 컴파일에만 필요하고 제거할 수 있는 것을 나타낼 수 있습니다.

이전 방식
// 타입을 가져오는 가장 최적의 방법은 `import type` 키워드를 사용하여 컴파일 후 실제로 가져오는 것을 방지하는 것입니다.
import { something } from './file';
import type { SomeType } from './file';
// 동일한 파일에 대해 두 개의 import 문이 필요합니다.
새로운 방식
// 이제 하나의 문장으로 결합될 수 있습니다.
import { something, type SomeType } from './file';

const 단언

상수를 정의할 때 as const를 사용하여 리터럴 타입으로 정확하게 타입 지정할 수 있습니다. 이는 많은 사용 사례가 있으며 정확한 타이핑을 쉽게 만듭니다. 또한 객체와 배열을 readonly로 만들어서 상수 객체의 변경을 방지합니다.

이전 방식
const obj = { name: 'foo', value: 9, toggle: false }; // { name: string; value: number; toggle: boolean; }
// 값은 일반적으로 입력되므로 어떤 값이라도 지정할 수 있습니다.
obj.name = 'bar';

const tuple = ['name', 4, true]; // (string | number | boolean)[]
// 길이와 정확한 타입은 타입에서 확인할 수 없습니다. 모든 값은 어디에나 지정할 수 있습니다.
tuple[0] = 0;
tuple[3] = 0;
새로운 방식
const objNew = { name: 'foo', value: 9, toggle: false } as const; // { readonly name: "foo"; readonly value: 9; readonly toggle: false; }
// "foo"로 정의되어 있고 읽기 전용이므로 값을 할당할 수 없습니다.
objNew.name = 'bar'; // type error: Cannot assign to 'name' because it is a read-only property.

const tupleNew = ['name', 4, true] as const; // readonly ["name", 4, true]
// 이제 길이와 정확한 타입이 정의되었으며 리터럴로 정의되어 읽기 전용이므로 아무 것도 할당할 수 없습니다.
tupleNew[0] = 0; // type error: Cannot assign to '0' because it is a read-only property.
tupleNew[3] = 0; // type error: Index signature in type 'readonly ["name", 4, true]' only permits reading.

클래스 내 메서드에 대한 코드 스니펫 완성

클래스가 메서드 유형을 상속할 때 이제 에디터에서 해당 메서드가 코드 스니펫으로 제안됩니다.

TypeScript 4.6

인덱싱된 액세스 추론 개선 사항

이제 키로 타입을 직접 인덱싱할 때 같은 객체에 있는 경우, 타입이 더 정확해집니다. 또한, 최신 타입스크립트로 무엇이 가능한지 보여주는 좋은 예시입니다.

interface AllowedTypes {
  'number': number;
  'string': string;
  'boolean': boolean;
}

// Record는 허용된 타입 중에서 종류와 값 타입을 지정합니다
type UnionRecord<AllowedKeys extends keyof AllowedTypes> = { [Key in AllowedKeys]:
{
  kind: Key;
  value: AllowedTypes[Key];
  logValue: (value: AllowedTypes[Key]) => void;
}
}[AllowedKeys];

logValue 함수는 Record 값만 허용합니다.
function processRecord<Key extends keyof AllowedTypes>(record: UnionRecord<Key>) {
  record.logValue(record.value);
}

processRecord({
  kind: 'string',
  value: 'hello!',
  // 암시적으로 string | number | boolean 타입을 갖는 데 사용되는 값입니다.
  // 이제 string으로만 올바르게 추론됩니다.
  logValue: value => {
    console.log(value.toUpperCase());
  }
});

타입스크립트 추적 분석기

--generateTrace <Output folder> 옵션은 타입스크립트 CLI에서 타입 검사 및 컴파일 프로세스에 관한 세부 정보가 포함된 파일을 생성하는 데 사용할 수 있습니다. 이는 복잡한 타입을 최적화하는 데 도움이 될 수 있습니다.

tsc --generateTrace trace

cat trace/trace.json
<<output
[
{"name":"process_name","args":{"name":"tsc"},"cat":"__metadata","ph":"M","ts":...,"pid":1,"tid":1},
{"name":"thread_name","args":{"name":"Main"},"cat":"__metadata","ph":"M","ts":...,"pid":1,"tid":1},
{"name":"TracingStartedInBrowser","cat":"disabled-by-default-devtools.timeline","ph":"M","ts":...,"pid":1,"tid":1},
{"pid":1,"tid":1,"ph":"B","cat":"program","ts":...,"name":"createProgram","args":{"configFilePath":"/...","rootDir":"/..."}},
{"pid":1,"tid":1,"ph":"B","cat":"parse","ts":...,"name":"createSourceFile","args":{"path":"/..."}},
{"pid":1,"tid":1,"ph":"E","cat":"parse","ts":...,"name":"createSourceFile","args":{"path":"/..."}},
{"pid":1,"tid":1,"ph":"X","cat":"program","ts":...,"name":"resolveModuleNamesWorker","dur":...,"args":{"containingFileName":"/..."}},
...
output

cat trace/types.json
<<output
[{"id":1,"intrinsicName":"any","recursionId":0,"flags":["..."]},
{"id":2,"intrinsicName":"any","recursionId":1,"flags":["..."]},
{"id":3,"intrinsicName":"any","recursionId":2,"flags":["..."]},
{"id":4,"intrinsicName":"error","recursionId":3,"flags":["..."]},
{"id":5,"intrinsicName":"unresolved","recursionId":4,"flags":["..."]},
{"id":6,"intrinsicName":"any","recursionId":5,"flags":["..."]},
{"id":7,"intrinsicName":"intrinsic","recursionId":6,"flags":["..."]},
{"id":8,"intrinsicName":"unknown","recursionId":7,"flags":["..."]},
{"id":9,"intrinsicName":"unknown","recursionId":8,"flags":["..."]},
{"id":10,"intrinsicName":"undefined","recursionId":9,"flags":["..."]},
{"id":11,"intrinsicName":"undefined","recursionId":10,"flags":["..."]},
{"id":12,"intrinsicName":"null","recursionId":11,"flags":["..."]},
{"id":13,"intrinsicName":"string","recursionId":12,"flags":["..."]},
...
output

TypeScript 4.7

Node.js의 ECMAScript 모듈 지원

CommonJS 대신 ES 모듈을 사용하는 경우, 이제 타입스크립트에서 기본값 지정을 지원합니다. tsconfig.json에서 지정하세요.

...
"compilerOptions": [
  ...
  "module": "es2020"
]
...

package.json에 입력

package.json의 필드 type을 “module”로 설정할 수 있으며, 이는 ES 모듈과 함께 node.js를 사용하는 데 필요합니다. 대부분의 경우 타입스크립트에 대해서는 이 정도면 충분하며 위의 컴파일러 옵션은 필요하지 않습니다.

...
"type": "module"
...

인스턴스화 표현식

인스턴스화 표현식을 사용하면 값을 참조할 때 타입 매개변수를 지정할 수 있습니다. 이를 통해 래퍼를 만들지 않고도 제네릭 타입을 좁힐 수 있습니다.

목록을 생성하지만 특정 값만 허용하는 함수를 만들고 싶다고 가정
class List<T> {
  private list: T[] = [];

  get(key: number): T {
    return this.list[key];
  }

  push(value: T): void {
    this.list.push(value);
  }
}

function makeList<T>(items: T[]): List<T> {
  const list = new List<T>();
  items.forEach(item => list.push(item));
  return list;
}
이전 방식
// 래퍼 함수를 수동으로 정의하고 인자를 전달해야 합니다.
function makeStringList(text: string[]) {
  return makeList(text);
}
새로운 방식
// 인스턴스화 표현식을 사용하면 이 작업이 훨씬 쉬워집니다.
const makeNumberList = makeList<number>;

infer 타입 변수에 대한 extends 제약 조건

조건부 타입에서 타입 변수를 추론할 때 이제 extends을 사용하여 직접 범위를 좁히거나 제한할 수 있습니다.

// 배열의 첫 번째 요소가 string인 경우에만 가져오는 타입을 입력한다고 가정해 보겠습니다.
// 이를 위해 조건부 타입을 사용할 수 있습니다.
이전 방식
type FirstIfStringOld<T> =
  T extends [infer S, ...unknown[]]
    ? S extends string ? S : never
    : never;

// 하지만 여기에는 두 개의 중첩된 조건부 타입들이 필요합니다. 하나로도 수행할 수도 있습니다.
type FirstIfString<T> =
  T extends [string, ...unknown[]]
    // `T`에서 첫 번째 타입을 가져옵니다.
    ? T[0]
    : never;

// 올바른 타입에 대해 배열을 인덱싱해야 하므로 여전히 차선책입니다.
새로운 방식
// infer 타입 변수에 제약 조건 확장을 사용하면 훨씬 쉽게 선언할 수 있습니다.
type FirstIfStringNew<T> =
  T extends [infer S extends string, ...unknown[]]
    ? S
    : never;
// 입력 방식은 이전과 동일하며, 단지 구문이 더 깔끔해졌을 뿐입니다.

type A = FirstIfStringNew<[string, number, number]>; // string
type B = FirstIfStringNew<["hello", number, number]>; // "hello"
type C = FirstIfStringNew<["hello" | "world", boolean]>; // "hello" | "world"
type D = FirstIfStringNew<[boolean, number, string]>; // never

타입 파라미터에 대한 선택적 변성 주석

제네릭은 “일치”를 확인할 때 서로 다른 동작을 가질 수 있으며, 예를 들어 GetterSetter의 상속 허용이 반대로 뒤집어질 수 있습니다. 이제 선택적으로 명확하게 지정할 수 있습니다.

다른 인터페이스를 확장하는 인터페이스/클래스가 있다고 가정
interface Animal {
  animalStuff: any;
}

interface Dog extends Animal {
  dogStuff: any;
}

// 그리고 제네릭 "getter"와 "setter"도 있습니다.
type Getter<T> = () => T;

type Setter<T> = (value: T) => void;

// Getter<T1>이 Getter<T2>와 일치하는지 또는 Setter<T1>이 Setter<T2>와 일치하는지 확인하려면 이는 공변성에 따라 달라집니다.
function useAnimalGetter(getter: Getter<Animal>) {
  getter();
}

// 이제 Getter를 함수에 전달할 수 있습니다.
useAnimalGetter((() => ({ animalStuff: 0 }) as Animal));
// 이것은 분명히 동작합니다.

// 하지만 대신 Dog를 반환하는 Getter를 사용하려면 어떻게 해야 할까요?
useAnimalGetter((() => ({ animalStuff: 0, dogStuff: 0 }) as Dog));
// Dog도 Animal이기 때문에 이것 또한 동작합니다.

function useDogGetter(getter: Getter<Dog>) {
  getter();
}

// useDogGetter 함수에 대해 동일한 시도를 하면 동일한 동작을 얻지 못할 것입니다.
useDogGetter((() => ({ animalStuff: 0 }) as Animal)); // Type error: Property 'dogStuff' is missing in type 'Animal' but required in type 'Dog'.
// Animal이 아닌 Dog로 예상되기 때문에 작동하지 않습니다.

useDogGetter((() => ({ animalStuff: 0, dogStuff: 0 }) as Dog));
// 하지만 이것은 동작합니다.

// 직관적으로 Setter가 동일하게 작동할 것으로 예상할 수 있지만 그렇지 않습니다.
function setAnimalSetter(setter: Setter<Animal>, value: Animal) {
  setter(value);
}

// 같은 타입의 Setter를 전달해도 여전히 작동합니다.
setAnimalSetter((value: Animal) => {}, { animalStuff: 0 });

function setDogSetter(setter: Setter<Dog>, value: Dog) {
  setter(value);
}

// 여기에서도 동일합니다.
setDogSetter((value: Dog) => {}, { animalStuff: 0, dogStuff: 0 });

// 하지만 Dog Setter를 setAnimalSetter 함수에 전달하면 Getters와 동작이 반대로 바뀝니다.
setAnimalSetter((value: Dog) => {}, { animalStuff: 0, dogStuff: 0 }); // Type error: Argument of type '(value: Dog) => void' is not assignable to parameter of type 'Setter<Animal>'.

// 이번에는 그 반대로 작동합니다.
setDogSetter((value: Animal) => {}, { animalStuff: 0, dogStuff: 0 });
새로운 방식
// 타입스크립트에 이를 알리려면(필수는 아니지만 가독성을 위해 도움이 됩니다), 새로운 타입 파라미터에 대한 선택적 변성 주석을 사용하세요.
type GetterNew<out T> = () => T;
type SetterNew<in T> = (value: T) => void;

moduleSuffixes를 사용한 사용자 정의 모듈 해석

사용자 정의 파일 접미사가 있는 환경(네이티브 앱 빌드의 경우 .ios)을 사용하는 경우 타입스크립트에서 import를 올바르게 해결하도록 이러한 접미사를 지정할 수 있습니다. tsconfig.json에서 지정하세요.

...
"compilerOptions": [
  ...
  "moduleSuffixes": [".ios", ".native", ""]
]
...
import * as foo from './foo';
// 먼저 ./foo.ios.ts, ./foo.native.ts, 마지막으로 ./foo.ts를 확인합니다.

에디터에서 소스가 정의된 곳으로 이동

에디터에서 새로운 “소스 정의로 이동” 메뉴 옵션을 사용할 수 있습니다. “정의로 이동”과 비슷하지만 타입 정의(.d.ts)보다 .ts.js 파일을 선호합니다.


TypeScript 4.9

satisfies 연산자

satisfies 연산자를 사용하면 해당 타입을 실제로 할당하지 않고도 타입과의 호환성을 확인할 수 있습니다. 이를 통해 호환성을 유지하면서 보다 정확한 추론된 타입을 유지할 수 있습니다.

이전 방식
// 다양한 항목과 그 색상을 저장하는 object/map/dictionary가 있다고 가정해 보겠습니다.
const obj = {
  fireTruck: [255, 0, 0],
  bush: '#00ff00',
  ocean: [0, 0, 255]
} // { fireTruck: number[]; bush: string; ocean: number[]; }

// 이렇게 하면 프로퍼티를 암시적으로 타이핑하여 배열과 문자열에 대해 작업할 수 있습니다.
const rgb1 = obj.fireTruck[0]; // number
const hex = obj.bush; // string

// 특정 객체만 허용하고 싶다고 가정해 봅시다.
// Record 타입을 사용할 수 있습니다.
const oldObj: Record<string, [number, number, number] | string> = {
  fireTruck: [255, 0, 0],
  bush: '#00ff00',
  ocean: [0, 0, 255]
} // Record<string, [number, number, number] | string>
// 하지만 이제 프로퍼티의 타이핑이 사라집니다.
const oldRgb1 = oldObj.fireTruck[0]; // string | number
const oldHex = oldObj.bush; // string | number
새로운 방식
// satisfies 키워드를 사용하면 실제로 할당하지 않고도 타입과의 호환성을 확인할 수 있습니다.
const newObj = {
  fireTruck: [255, 0, 0],
  bush: '#00ff00',
  ocean: [0, 0, 255]
} satisfies Record<string, [number, number, number] | string> // { fireTruck: [number, number, number]; bush: string; ocean: [number, number, number]; }
// 여전히 프로퍼티의 타입이 유지되며, 배열이 튜플이 됨으로써 더욱 정확해졌습니다.
const newRgb1 = newObj.fireTruck[0]; // number
const newRgb4 = newObj.fireTruck[3]; // Type error: Tuple type '[number, number, number]' of length '3' has no element at index '3'.
const newHex = newObj.bush; // string

에디터를 위한 ‘사용되지 않는 Import 제거’ 및 ‘Import 정렬’ 명령어

에디터에서 새로운 명령 및 자동 수정인 ‘사용하지 않는 Import 제거’‘Import 정렬’을 사용하면 가져오기를 더 쉽게 관리할 수 있습니다.

Linus Schlumberger, ‘All JavaScript and TypeScript Features of the last 3 years’

Leave a Reply