image

최근 TypeScript 4.0이 새로 발표되었다. 평소에 타입스크립트를 많이 활용하던 나로써는 주요 변경사항에 대해 알아가면 좋을 것 같아 간단하게 발표 내용을 요약, 번역하여 포스트에 작성하게 되었다.

전체적으로 새로운 문법과 추가된 옵션, 개선된 내용을 포함해 많은 내용을 발표 했는데 그 중에서 문법과 옵션, 추가적인 변경 사항으로 그룹을 나눠 정리해보려고 한다.

새로운 문법

가변 열거 타입

TypeScript 4.0에서는 가변 열거 타입에 대한 문법에서 몇몇 달라진 내용이 있다.

첫 번째로 달라진 내용은 열거 형식 구문의 스프레드에 제네릭이 적용된다. 이것은 우리가 실제 사용중인 타입을 모를 때에도 타입 추론이 가능하다는 것을 의미한다.

function tail<T extends any[]>(arr: readonly [any, ...T]) {
	const [_ignored, ...rest] = arr;
	return rest;
}

const myTuple = [1, 2, 3, 4] as const;
const myArray = ["hello", "world"];

// type [2, 3, 4]
const r1 = tail(myTuple);

// type [2, 3, 4, ...string[]]
const r2 = tail([...myTuple, ...myArray] as const);

두 번째로 달라진 내용은 나머지 요소가 배열의 끝이 아닌 어느곳에서나 적용된다.

type Strings = [string, string];
type Numbers = [number, number];

// [string, string, number, number, boolean]
type StrStrNumNumBool = [...Strings, ...Numbers, boolean];

이전에는 TypeScript에서 다음과 같은 오류를 발생시켰다.

A rest element must be last in a tuple type.

하지만 TypeScript 4.0에서는 이러한 제한이 완화되고 알려진 길이가 없는 유형에 나머지 요소가 사용되는 경우 다음에 정의될 타입이 나머지 요소 유형에 포함된다.

type Strings = [string, string];
type Numbers = number[];

// [string, string, ...Array<number | boolean>]
type Unbounded = [...Strings, ...Numbers, boolean];

배열 요소에 변수명 지정

TypeScript 4.0부터는 매개변수로 받는 배열의 인자에도 이름을 지정해 줄 수 있다.

function foo(...args: [string, number]): void {
    // ...
}

function foo(arg0: string, arg1: number): void {
    // ...
}

다음 두 함수 foo는 같은 역할을 한다. 두 함수 모두 첫 번째 매개변수로 string타입, 두 번째 매개변수로 number타입을 받는다. 그러나 가독성 측면에서는 차이점이 있다. 첫 번째 foo함수에서는 요소에 대한 이름이 없지만 두 번째 foo함수에서는 이름이 있다. 이는 동작 자체에는 영향을 미치지 않지만 변수 이름이 없다면 그 의도를 전달하기가 더 어려워진다.

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

// error! Tuple members must all have names or all not have names.
type Bar = [first: string, number];

하지만 이제 배열 요소에서도 변수 이름을 지정해 매개 변수를 받을 수 있다. 하지만 배열 요소에 이름을 지정할 때 배열의 다른 모든 요소에도 반드시 이름을 지정해야 한다.

constructor의 클래스 프로퍼티 추론

TypeScript 4.0에서는 클래스의 프로퍼티가 constructor에서 어떤 타입을 갖게 되는지 추론 가능한 경우에는 프로퍼티의 타입을 추론할 수 있다.

class Square {
  // Previously: implicit any!
  // Now: inferred to `number`!
  area;
  sideLength;

  constructor(sideLength: number) {
      this.sideLength = sideLength;
      this.area = sideLength ** 2;
  }
}

초기 클래스의 프로퍼티 선언 부분에서 따로 타입을 선언해 주지 않았음에도 불구하고 area, sideLength프로퍼티는 constructor에서 해당 프로퍼티가 어떤 타입을 갖게되는지 추론한다. 이전 버전에서는 암시적으로 any타입이 추론되었다.

class Square {
		// number | undefined
    sideLength;

    constructor(sideLength: number) {
        if (Math.random()) {
            this.sideLength = sideLength;
        }
    }

    get area() {
        return this.sideLength ** 2;
        //     ~~~~~~~~~~~~~~~
        // error! Object is possibly 'undefined'.
    }
}

하지만 다음과 같이 프로퍼티의 값이 확실하게 추론되지 않을 경우 타입은 잠재적으로 undefined 타입을 같이 가지게 된다.

새로운 복합 대입 연산자

복합 대입 연산자는 +=, -=와 같이 왼쪽의 피연산자를 오른쪽의 피연산자와 연산 후 다시 왼쪽의 피 연산자에게 대입하는 문법을 말한다.

a = a && b;
a = a || b;
a = a ?? b;

if (!a) {
    a = b;
}

TypeScript 4.0의 새로운 복합 대입 연산자들은 다음의 코드를 대체하는데 유용하다.

// Before
obj.prop = obj.prop || foo();
obj.prop = obj.prop && foo();
obj.prop = obj.prop ?? foo();

// After
obj.prop ||= foo();
obj.prop &&= foo();
obj.prop ??= foo();

이제 &&, ||, ??에도 다음과 같이 복합 대입 연산자를 사용해 간단하게 표현할 수 있다.

catch절의 변수 unknown으로 바인딩

// before
try {
    // ...
} catch (x) {
    // x has type 'any' - have fun!
    console.log(x.message);
    console.log(x.toUpperCase());
    x++;
    x.yadda.yadda.yadda();
}

// after
try {
    // ...
} catch (e: unknown) {
    // error!
    // Property 'toUpperCase' does not exist on type 'unknown'.
    console.log(e.toUpperCase());

    if (typeof e === "string") {
        // works!
        // We've narrowed 'e' down to the type 'string'.
        console.log(e.toUpperCase());
    }
}

기존의 TypeScript의 catch절 변수는 항상 any타입을 기본적으로 가지고 있었다. 하지만 any타입은 오류 처리 코드에서 더 많은 오류가 발생하지 않도록 방지하려는 의도와 맞지 않다.

그렇기 때문에 TypeScript 4.0에서는 catch절 변수에 unknown타입을 지정해 줄 수 있다. (any타입과 unknown타입만 지정 가능하다)

새로워진 옵션

커스텀 JSX Factories

JSX를 사용할 때 프레그먼트는 여러 자식 요소를 반환 할 수 있는 JSX요소 유형이다. 타입스크립트에서 처음으로 프레그먼트를 구현했을 때 다른 라이브러리에서 프레그먼트를 활용하는 방법에 대한 좋은 생각이 없었지만 TypeScript 4.0에서 사용자 새로운 —jsxFragmentFactory플래그를 통해 Fragment Factory를 사용자가 지정할 수 있다.

예를 들어 다음 tsconfig.json파일은 타입스크립트에 리액트와 호환되는 방식으로 JSX를 변환하도록 지시하지만 각 facotry 호출은 React.createElement대신 h로 전환하고 React.Fragment대신 Fragment를 사용한다.

{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "jsx": "react",
    "jsxFactory": "h",
    "jsxFragmentFactory": "Fragment"
  }
}
// Note: these pragma comments need to be written
// with a JSDoc-style multiline syntax to take effect.
/** @jsx h */
/** @jsxFrag Fragment */

import { h, Fragment } from "preact";

let stuff = <>
    <div>Hello</div>
</>;

파일 단위로 다른 JSX Factory가 필요한 경우 /** @jsxFrag */ 주석을 활용할 수 있다.

// Note: these pragma comments need to be written
// with a JSDoc-style multiline syntax to take effect.
/** @jsx h */
/** @jsxFrag Fragment */
import { h, Fragment } from "preact";
let stuff = h(Fragment, null,
    h("div", null, "Hello"));

이전 코드는 다음과 같은 자바스크립트 코드로 변환된다.

전체적으로 자주 사용하던 옵션이 아니었기 때문에 정확히 이해하기는 어려웠다.

noEmitOnError플래그 빌드 모드에서의 속도 향상

이전에는 —incremental 플래그의 오류가 있는 이전 컴파일 후 프로그램을 컴파일 하는 것이 --noEmitOnError옵션을 사용할 때 매우 느렸다. 마지막 컴파일의 정보가 —noEmitOnError 옵션으로 인해 .tsbuildinfo 파일에 캐시되지 않았기 때문이다.

TypeScript 4.0은 이를 변경하여 이러한 동작에서 속도를 크게 높이고 다시 빌드모드 동작을 크게 개선했다. —incremental는 이전 컴파일 정보를 읽거나 파일로 작성하여 증분 컴파일을 활성화 하는 옵션이다. —noEmitOnError는 에러 발생시 결과 파일에 대한 저장을 활성화 하는 옵션이다.

추가적인 변경 사항

에디터 개선

TypeScript 컴파일러는 대부분의 코드 에디터에서 TypeScript 자체의 편집 환경을 강화할 뿐만 아니라 Visual Studio 제품군 등에서 JavaScript 환경을 강화했다.

옵셔널 체이닝 변환

옵셔널 체이닝은 최근 많은 사랑을 받은 기술이기 때문에 TypeScript 4.0에서는 옵셔널 체이닝으로 손쉽게 변환할 수 있도록 새로운 리팩터링 을 제공한다.

deprecated 주석표시 지원

TypeScript 편집기에서 이제 /** @deprecated */으로 주석표시된 선언을 인식한다.

image

다음과 같이 /** @deprecated */로 표시된 선언은 VS Code와 같은 코드 에디터에서 취소 선 스타일로 표시된다.

로딩시 부분 시멘틱 모드

TypeScript 4.0에서는 부분적으로 시멘틱 모드를 적용하여 초기에 프로젝트가 로딩되는 지연시간을 약간 단축시켰다. 현재 이 모드를 지원하는 유일한 편집기는 VS Code이며 아직 UX 개선사항이 많이 필요하다고 생각해 개선사항에 포함시켜두고 있다.

더 똑똑해진 Auto-Imports

TypeScript 4.0은 이제 편집기에서 package.json에 종속된 패키지를 포함하기 위해 약간의 추가 작업을 수행한다. 이 패키지의 정보는 auto-imports를 개선하는 데만 사용되며 유형 검사와 같은 다른 사항은 변경하지 않는다. 이를 통해 node_modules 검색 비용을 발생시키지 않고 유형이 있는 모든 종속성에 대해 auto-import를 제공한다.

프로퍼티 접근자 오버라이딩 에러 표시

이전에는 프로퍼티 접근자를 오버라이딩하거나 접근자가 속성을 오버라이딩하는 경우에만 오류를 발생시켰다. 그러나 TypeScript 4.0부터는 기본 클래스의 getter, setter를 오바라이딩하는 프로퍼티를 파생 클래스에서 선언하는 경우 항상 오류를 발생시킨다.

class Base {
    get foo() {
        return 100;
    }
    set foo() {
        // ...
    }
}

class Derived extends Base {
    foo = 10;
//  ~~~
// error!
// 'foo' is defined as an accessor in class 'Base',
// but is overridden here in 'Derived' as an instance property.
}

이전 버전에서는 다음과 같이 fooBase클래스에서 접근자로 정의되었지만 Derived클래스에서 프로퍼티로 오버라이딩 되었다. 하지만 이런 경우에 에러를 발생시키지 않았다.

class Base {
    prop = 10;
}

class Derived extends Base {
    get prop() {
        return 100;
    }
}

하지만 TypeScript 4.0부터는 이런 오버라이딩 코드에서 에러를 발생시킨다. 해당 코드에서 propBase클래스의 프로퍼티로 정의되지만 Derived에서 접근자로 오버라이딩되기 때문에 에러가 발생한다.

delete의 피 연산자는 선택사항이여야 함

interface Thing {
    prop: string;
}

function f(x: Thing) {
    delete x.prop;
    //     ~~~~~~
    // error! The operand of a 'delete' operator must be optional.
}

strictNullChecks옵션이 활성화되어 있을때 delete연산자를 사용하면 피 연산자는 이제 any, unknown, never또는 정의되지않은 타입(undefined)을 포함해야 한다. 그렇지 않을때 delete연산자를 사용하면 에러가 발생한다. <br>

본 포스트는 다음 문서를 참고해 작성했습니다.

https://devblogs.microsoft.com/typescript/announcing-typescript-4-0/