TypeScript 3.9 변경사항

Subin·2020년 5월 22일
0

TypeScript 3.9

TypeScript 3.9 공식문서 번역 및 정리

  • Promise.all의 타입 추론 개선
  • 속도 향상
  • [// @ts-expect-error Comments]
  • 조건문에서 호출되지 않는 함수 체크
  • Editor 개선사항
    • 자바스크립트에서 Common-JS 자동 import
    • Code Actions Preserve Newlines
    • Quick Fixes for Missing Return Expressions
    • Solution Style의 tsconfig.json 파일 지원
  • ✨ 주요 변경 사항 ✨

Promise.all의 타입 추론 개선 Improvements in Inference and Promise.all

이전 버전인 TypeScript (3.7버전) 에서 Promise.all과 Promise.race 같은 함수가 업데이트 됐는데, 타입 지정 시 null / undefined 와 다른 값을 함께 쓸 때 몇 가지 문제가 있었다.

interface Lion {
    roar(): void
}

interface Seal {
    singKissFromARose(): void
}

async function visitZoo(lionExhibit: Promise<Lion>, sealExhibit: Promise<Seal | undefined>) {
    let [lion, seal] = await Promise.all([lionExhibit, sealExhibit]);
    lion.roar(); // uh oh
//  ~~~~
	// lion 객체까지 'undefined' 타입이 될 수 있음.
}

sealExhibit의 타입에만 undefined가 포함되어 있는데, lion이 undefined가 되어도 타입 추론을 통한 에러를 발생시키지 않아 결과적으로 lion의 타입에도 undefined가 포함된 것처럼 동작한다. TypeScript 3.9 버전에서는 이 이슈가 해결되었다.

속도 향상 Speed Improvements

TypeScript 3.9는 많은 부분에서 속도를 향상시켰다. 특히 material-uistyled-components같은 패키지들의 에디팅, 컴파일 속도가 현저히 느렸었는데 이 부분을 개선해 metariel-ui의 경우 약 40%의 컴파일 시간을 단축시켰다.

에디터 부분에서는 file renaming에 걸리는 시간을 단축하기 위해서 파일 탐색을 위한 캐시의 내부 동작을 변경했다.

// @ts-expect-error Comments

TypeScript로 라이브러리를 작성하고 있고, doStuff라는 함수를 public API로 export하는 상황을 가정해보자. 두 개의 string을 인자로 받는 함수의 타입 선언을 하면, TypeScript 사용자는 타입 체크를 통한 에러를 받을 수 있을 뿐만 아니라 런타임 체크를 통해 JavaScript 사용자에게도 에러를 줄 수 있다.

function doStuff(abc: string, xyz: string) {
	assert(typeof abc === 'string');
	assert(typeof xyz === 'string);
	// do some stuff
}

따라서 이 함수를 잘못 사용하는 TypsScript 사용자는 빨간 엑스 표시와 함께 에러 메시지를, JavaScript 사용자는 assertion error를 받을 것이다.

AssertionError는 Node.js에서 유닛 테스트를 위해 사용되는 Assert라는 모듈에 포함된 메소드이다.

위 사실을 확인하기 위해서 유닛 테스트 코드를 작성해보자.

expect(() => {
	doStuff(123, 456);
//        ~~~
// error: Type 'number' is not assignable to type 'string'.
}).toThrow();

그러나 위의 코드는 TypeScript로 작성되었기 때문에 TypeScript가 에러를 발생시켜버려서 테스트 코드의 결과를 확인할 수가 없다!

이런 상황에 대처하기 위해서 TypeScript 3.9 버전에서 //@ts-expect-error 주석이 도입되었다. //@ts-expect-error 로 시작되는 코드가 있을 때 TypeScript는 발생되는 에러를 무시하고, 에러가 발생하지 않는다면 //@ts-expect-error 가 필요하지 않다는 메시지를 띄운다.

// @ts-expect-error
console.log(47 * "octopus");

위 코드는 발생하는 에러를 억제하는 기능이 제대로 작동하고,

// @ts-expect-error
console.log(1 + 1);

// Unused '@ts-expect-error' directive.

는 에러를 억제해야 하는데 에러가 나지 않는 코드이므로 Unused '@ts-expect-error' directive. 를 발생시킨다.

개발자가 명시적으로 에러가 발생시키는 코드를 작성해서 결과를 확인하고 싶을 때 사용하면 좋을 것 같다.

ts-ignore 와 ts-expect-error

//@ts-expect-error 는 에러를 막아주는 기능을 한다는 점에서 //@ts-ignore 와 비슷하지만, //@ts-ignore 는 에러가 발생하지 않아도 아무런 일을 하지 않는다. 반면 //@ts-expect-error 는 에러가 발생하지 않으면 알려준다.

//@ts-expect-error 를 사용해야하는 경우:

  • 작업 중 타입 에러가 발생하도록 테스트 코드를 작성하는 경우
  • 빠르게 고칠 수 있는 에러라서 당장 사용할 해결책이 필요한 경우
  • 보통 규모의 프로젝트를 할 때, suppression comments를 달아놓은 코드들이 유효해지자 마자 주석을 일괄 제거해야하는 경우

//@ts-ignore 를 사용해야하는 경우:

  • 큰 규모의 프로젝트를 할 때, 원인을 알 수 없는 에러가 발생하는 경우
  • 두 개의 TypeScript 버전 사이의 버전을 사용하고 있을 때 한 개의 버전에서만 에러가 나는 경우
  • 솔직히 //@ts-expect-error//@ts-ignore 둘 중 뭘 쓰는 게 나을 지 모르겠을 때

조건문에서 호출되지 않는 함수 체크 Uncalled Function Checks in Conditional Expressions

TypeScript 3.7에서 함수 호출을 잊은 경우에 발생하는 uncalled function check 에러가 도입됐다.

function hasImportantPermissions(): boolean {
    // ...
}

// Oops!
if (hasImportantPermissions) {
//  ~~~~~~~~~~~~~~~~~~~~~~~
// This condition will always return true since the function is always defined.
// Did you mean to call it instead?
    deleteAllTheImportantFiles();
}

해당 에러는 if 문에서만 동작했었는데 TypeScript 3.9 부터는 삼항 조건 연산자도 지원한다.

declare function listFilesOfDirectory(dirPath: string): string[];
declare function isDirectory(): boolean;

function getAllFiles(startFileName: string) {
    const result: string[] = [];
    traverse(startFileName);
    return result;

    function traverse(currentPath: string) {
        return isDirectory ?
        //     ~~~~~~~~~~~
        // This condition will always return true
        // since the function is always defined.
        // Did you mean to call it instead?
            listFilesOfDirectory(currentPath).forEach(traverse) :
            result.push(currentPath);
    }
}

Editor 개선사항

TypeScript 컴파일러는 대부분의 주요 에디터에서 TypeScript 개발 환경을 강화할 뿐만 아니라 Visual Studio 에디터의 JavaScript 개발 환경도 강화한다. 에디터에서 TypeScript나 JavaScript의 새로운 기능을 사용하는 건 에디터가 어디까지 지원하는 지에 따라 다르지만, Visual Studio Code, Visual Studio 2017/2019, Sublime Text3 등등이 TypeScript 를 지원한다.

JavaScript에서 CommonJS 자동 import

CommonJS를 사용하는JavaScript 파일에서의 자동 import 기능이 보완됐다.

이전 버전에서는 파일이 어떤 형식이든간에 ECMAScript 스타일의 import를 한다고 가정했기 때문에 코드 스타일 정리를 할 때 아래와 같은 형식으로 코드가 자동 정리됐다.

import * as fs from 'fs';

그러나 여전히 많은 사용자들이 아래와 같은 CommonJS 스타일의 require(...) import를 사용한다.

const fs = require('fs');

따라서 TypeScript 3.9버전 부터는 파일의 코드 스타일을 정리할 때 CommonJS 스타일 import를 사용하더라도 import 스타일을 유지한다.

Code Actions Preserve Newlines

TypeScript의 refactoring과 quick fix를 할 때 공백 line을 삭제하곤 했었는데 TypeScript 3.9 버전부터는 코드 line을 유지해준다.

https://devblogs.microsoft.com/typescript/wp-content/uploads/sites/11/2020/03/printSquaresWithoutNewlines-3.9.gif.gif

Quick Fixes for Missing Return Expressions

화살표 함수에 중괄호를 추가할 때 리턴하던 값을 return 문으로 바꾸는 걸 잊어버리는 실수를 할 때가 많다. (알고 있더라도 조금 귀찮음)

// before
let f1 = () => 42

// oops - not the same!
let f2 = () => { 42 }

TypeScript 3.9 버전부터 return 문을 추가하거나, 중괄호를 제거하거나, 화살표 함수에 괄호를 추가하는 상황에서 quick-fix 기능을 사용할 수 있다! 👏👏

https://devblogs.microsoft.com/typescript/wp-content/uploads/sites/11/2020/04/missingReturnValue-3-9.gif

주요 변경사항

Parsing Differences in Optional Chaining and Non-Null Assertions

최근 TypeScript는 optional chaining 연산자를 도입했지만 사용자들로부터 non-null assertion 연산자 (!)optional chaining 연산자 (?.) 가 함께 쓰일 때 동작이 직관적이지 않다는 지적이 있었다.

Non-Null Assertion Operator !
: post-fix 연산자인 ! 는 앞의 값이 확실히 null이나 undefined가 아니라는 걸 알리려고 할 때 쓴다. 주로 컴파일 시 --strictNullChecks 라는 null 체크 모드와 함께 쓰인다.

Optional Chaning ?.
: let x = foo?.bar.baz();
foo가 null 또는 undefined일 경우, 표현식의 실행을 멈추고 undefined를 리턴한다. foo가 정의되어있으면 정상적으로 표현식을 평가해 값을 계산한다.

특히 이전 버전에서는, 다음 코드가

foo?.bar!.baz  // 논란의 코드

아래와 동일하게 해석됐다.

(foo?.bar).baz  // 이전 버전 해석 방법

위의 코드에서 괄호가 optional chaning 기능을 끊어버리기 때문fooundefined일 경우, (foo?.bar) 까지만 undefined로 평가된다. 이때 baz에 접근하면 bazundefined의 프로퍼티를 참조하는 것이므로 런타임 에러가 발생한다.

위 코드에서 ! 연산자가 bar의 타입이 nullundefined 가 아님을 명시하려고 사용되었다면 평가 시에 사라지는 게 직관적이다. 즉, 아래와 같이 해석되어야 한다.

foo?.bar.baz  // TypeScript 3.9 버전의 해석 방법

fooundefined일 때 위 표현식은 undefined로 평가된다.

만약 foonull / undefined 가 아닐 때 barnull / undefined가 아니라는 걸 명시하고 싶다면 이제부터는

(foo?.bar)!.baz

이렇게 사용해야 한다!!

JSX Text 문자열 > 와 } 허용

JSX 에서는 }> 를 텍스트 포지션에 사용할 수 없게 했었는데, TypeScript와 Babel이 규칙을 완화하기로 했다.

  1. HTML 특수문자 (&gt, &rbrace)를 사용하던가
  2. {'>'} 또는 {'}'} 처럼 쓰면 된다.
<span> 2 {">"} 1 </div>

Stricter Checks on Intersections and Optional Properties

일반적으로, A & B 같은 교차 타입은 A 타입 또는 B 타입이 C 타입에 할당 가능하면 C 타입에 할당이 가능하다.

교차 타입 (Intersection Types) 이란?

다양한 타입을 하나로 결합해서 모든 기능을 갖춘 단일 타입을 얻는 방식이다.
예를 들어 A & B & C 타입은 A, B, C 모두의 멤버를 가진다.

따라서 아래와 같은 예시를 보면

interface A {
    a: number; // notice this is 'number'
}

interface B {
    b: string;
}

interface C {
    a?: boolean; // notice this is 'boolean'
    b: string;
}

declare let x: A & B;
declare let y: C;

y = x;

이전 버전 TypeScript에서는 A 타입은 C 타입과 완전히 호환되지 않지만 B 타입이 C 타입과 호환되므로 위와 같은 할당( y = x; )이 허용됐다.

TypeScript 3.9 버전부터는 intersection이 객체 타입인 경우 타입 시스템이 모든 프로퍼티를 한 번에 검사한다. 즉, TypeScript는 A & B 타입이 C 타입과 호환되지 않음을 알 수 있다.

Type 'A & B' is not assignable to type 'C'.
  Types of property 'a' are incompatible.
    Type 'number' is not assignable to type 'boolean | undefined'.

Intersections Reduced By Discriminant Properties

가끔 존재하지 않는 값이라는 의미만 가지는 타입을 설정해야할 때가 있다. (고 한다.) 예를 들어,

declare function smushObjects<T, U>(x: T, y: U): T & U;

interface Circle {
    kind: "circle";
    radius: number;
}

interface Square {
    kind: "square";
    sideLength: number;
}

declare let x: Circle;
declare let y: Square;

let z = smushObjects(x, y);
console.log(z.kind);

CircleSquarekind 필드가 호환되지 않으므로 intersection을 만들 수 없는데 이전 버전 TypeScript에서는 위의 코드가 동작했다. kind는 만들어질 수 없는 타입이므로 타입을 never로 해서 만들었다.

TypeScript 3.9 버전부터는 타입 시스템이 더 엄격해진다. CircleSquarekind 프로퍼티 때문에 intersect를 할 수 없다는 걸 인지한다. 따라서 Circle & Squarekind 프로퍼티의 타입을 never로 만들지 않고 Circle & Square 자체를 never 타입으로 지정한다.

  • 이전 버전 - z.kind : never
  • 3.9 버전부터 - z : never

Getters/Setters are No Longer Enumerable

이전 버전 TypeScript에서 클래스 내부의 getset 접근자는 enumerable하게 생성됐으나 ECMAScript 에서는 get과 set은 non-enumerable이라고 규정하고 있다. TypeScript 3.9버전 부터는 non-enumerable하게 만들어 ECMAScript 표준에 가까워졌다.

Type Parameters That Extend any No Longer Act as any

이전 버전 TypeScript에서 any를 확장한 타입은 any 타입처럼 취급됐다.

function foo<T extends any>(arg: T) {
    arg.spfjgerijghoied; // no error!
}

TypeScript 3.9 버전 부터는 any처럼 취급하지 않는다!

function foo<T extends any>(arg: T) {
    arg.spfjgerijghoied;
    //  ~~~~~~~~~~~~~~~
    // Property 'spfjgerijghoied' does not exist on type 'T'.
}}

export * is Always Retained

이전 버전 TypeScript에서는 foo가 아무 값도 export 하지 않을 경우 컴파일된 JavaScript output 파일에서 export * from 'foo' 선언이 삭제됐다. TypeScript 3.9버전 부터는 아무 것도 export하지 않더라도 export 선언은 유지된다.

profile
주니어 프론트엔드 개발자

0개의 댓글

관련 채용 정보