[번역] 지난 3년간의 모든 자바스크립트 및 타입스크립트 기능

Sonny·2023년 3월 29일
84

Article

목록 보기
10/27
post-thumbnail

원문 : https://betterprogramming.pub/all-javascript-and-typescript-features-of-the-last-3-years-629c57e73e42

이 글에서는 지난 3년 동안(일부 변경 사항은 더 이전의) 자바스크립트/ECMAScript타입스크립트의 대부분의 변경 사항을 살펴봅니다.

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

"이전에는 기대했던 대로 동작하지 않았지만 이제는 기대한 대로 동작하는 것"으로 요약되어 여기서는 생략한 타입스크립트 기능이 많이 있습니다. 따라서 과거에 작동하지 않았던 기능이 있다면 지금 다시 시도해 보세요.

개요

  • 자바스크립트/ECMAScript (오래된 것 우선)
  • 타입스크립트 (오래된 것 우선)

ECMAScript

과거 (아직도 유효한 이전 방식)

  • 템플릿 리터럴 태그(Tagged template literals): 템플릿 리터럴 앞에 함수 이름을 붙이면 함수에 템플릿 리터럴과 템플릿 값의 일부가 전달됩니다. 이 기능에는 몇 가지 흥미로운 용도가 있습니다.
// 숫자가 포함된 임의의 문자열을 기록하되, 숫자의 형식을 지정하는 방법을 작성하고 싶다고 가정해 보겠습니다.
// 이를 위해 태그가 지정된 템플릿을 사용할 수 있습니다.
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.
  • 심볼(Symbols): 객체의 고유한 키로 내부적으로 사용됩니다. 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

  • 옵셔널 체이닝(Optional chaining): 인덱싱을 통해 정의되지 않은 객체의 값에 접근할 필요가 있을 때, 부모 객체 이름 뒤에 ?를 사용하여 옵셔널 체이닝을 사용할 수 있습니다. 이 기능은 인덱싱([...])이나 함수 호출에도 사용할 수 있습니다.
// 이전 방식
// 객체 변수 또는 다른 구조가 정의되어 있는지 확실하지 않은 경우,
// 프로퍼티에 쉽게 접근할 수 없습니다.
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?.();
  • 널 병합 연산자(Nullish coalescing operator) (??): 조건부 할당에 || 연산자를 사용하는 대신 새로운 ?? 연산자를 사용할 수 있습니다. 모든 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 ...과 비슷하지만 import하지만 런타임에 동작하고, 변수를 사용합니다.
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(-1)): 배열이나 문자열을 인덱싱할 때 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 {@link variable/type/link} 인라인 태그가 이제 지원되며, 에디터에서 표시되고 해결됩니다.
const originalValue = 1;
/**
  * {@link originalValue} 복사본
  */
const value = originalValue;

TypeScript 4.4

  • 정확한 선택적 프로퍼티 타입 (--exactOptionalPropertyTypes): 컴파일러 플래그 --exactOptionalPropertyTypes (또는 tsconfig.json에서)를 사용하면 암시적으로 undefined를 허용하는 프로퍼티에 대한 undefined 할당이 더 이상 허용되지 않습니다.(예: 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.
  • 클래스 내 메서드에 대한 코드 스니펫 완성: 클래스가 메서드 유형을 상속할 때 이제 에디터에서 해당 메서드가 코드 스니펫으로 제안됩니다.

Snippet Completions for Methods in Classes.gif

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): --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
  • 타입 파라미터에 대한 선택적 변성 주석: 제네릭은 "일치"를 확인할 때 서로 다른 동작을 가질 수 있으며, 예를 들어 Getter와 Setter의 상속 허용이 반대로 뒤집어질 수 있습니다. 이제 선택적으로 명확하게 지정할 수 있습니다.
// 다른 인터페이스를 확장하는 인터페이스/클래스가 있다고 가정해 보겠습니다.
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 파일을 선호합니다.

Go to Source Definition in editors1.gif

Go to Source Definition in editors2.gif

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 정렬'을 사용하면 가져오기를 더 쉽게 관리할 수 있습니다.

Remove Unused Imports and Sort Imports Commands for Editors.gif

profile
FrontEnd Developer

9개의 댓글

comment-user-thumbnail
2023년 3월 31일

TS 5버전이 나오면서 많은 변화가 일어났는데, 저는 이것들을 불편하고 의미 없는 요소라 생각했었습니다. 그러나 지금 제가 당연하게 쓰고 있는 기능들이 많은데, 비교적 최근에 추가된 기능이라는 것을 이 글을 통해 알게 되었습니다. 여러모로 많은 것을 생각하게 해주는 글입니다. 좋은 글 감사합니다

1개의 답글
comment-user-thumbnail
2023년 3월 31일

좋은 글 잘 읽었습니다!!

1개의 답글
comment-user-thumbnail
2023년 4월 3일

안녕하세요,

// 새로운 방식
// 대신 새로운 널 병합 연산자를 사용할 수 있습니다. 이 연산자는 오직 undefined와 null 값에만 적용됩니다.
const newValue = value ?? 'hello';
console.log(newValue) // 항상 "hello"

에서, 항상 "hello" 가 아닌 'test' 혹은 'hello' 인 것 같습니다.

1개의 답글
comment-user-thumbnail
2023년 4월 4일

I appreciate the information and advice you have shared.

ACE Flare Account Login

답글 달기
comment-user-thumbnail
2023년 6월 23일

Just love this idea! I hate Prettier because there are solid rules that can't be changed, but I'm forced to use them because they're not good enough for AST-based auto fixing and there's nothing else good about it besides Prettier. https://www.wikiproficiency.com/

답글 달기
comment-user-thumbnail
2023년 6월 26일

이것은 내가 이 주제에 대해 읽은 최고의 기사 중 하나입니다. 공유해 주셔서 감사합니다. slither io

답글 달기