TypeScript 3.9 공식문서 번역 및 정리
Promise.all
의 타입 추론 개선[// @ts-expect-error Comments]
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 버전에서는 이 이슈가 해결되었다.
TypeScript 3.9는 많은 부분에서 속도를 향상시켰다. 특히 material-ui
나 styled-components
같은 패키지들의 에디팅, 컴파일 속도가 현저히 느렸었는데 이 부분을 개선해 metariel-ui
의 경우 약 40%의 컴파일 시간을 단축시켰다.
에디터 부분에서는 file renaming에 걸리는 시간을 단축하기 위해서 파일 탐색을 위한 캐시의 내부 동작을 변경했다.
// @ts-expect-error
CommentsTypeScript로 라이브러리를 작성하고 있고, 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-expect-error
는 에러를 막아주는 기능을 한다는 점에서 //@ts-ignore
와 비슷하지만, //@ts-ignore
는 에러가 발생하지 않아도 아무런 일을 하지 않는다. 반면 //@ts-expect-error
는 에러가 발생하지 않으면 알려준다.
//@ts-expect-error
를 사용해야하는 경우:
//@ts-ignore
를 사용해야하는 경우:
//@ts-expect-error
와 //@ts-ignore
둘 중 뭘 쓰는 게 나을 지 모르겠을 때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);
}
}
TypeScript 컴파일러는 대부분의 주요 에디터에서 TypeScript 개발 환경을 강화할 뿐만 아니라 Visual Studio 에디터의 JavaScript 개발 환경도 강화한다. 에디터에서 TypeScript나 JavaScript의 새로운 기능을 사용하는 건 에디터가 어디까지 지원하는 지에 따라 다르지만, Visual Studio Code, Visual Studio 2017/2019, Sublime Text3 등등이 TypeScript 를 지원한다.
CommonJS를 사용하는JavaScript 파일에서의 자동 import 기능이 보완됐다.
이전 버전에서는 파일이 어떤 형식이든간에 ECMAScript 스타일의 import를 한다고 가정했기 때문에 코드 스타일 정리를 할 때 아래와 같은 형식으로 코드가 자동 정리됐다.
import * as fs from 'fs';
그러나 여전히 많은 사용자들이 아래와 같은 CommonJS 스타일의 require(...)
import를 사용한다.
const fs = require('fs');
따라서 TypeScript 3.9버전 부터는 파일의 코드 스타일을 정리할 때 CommonJS 스타일 import를 사용하더라도 import 스타일을 유지한다.
TypeScript의 refactoring과 quick fix를 할 때 공백 line을 삭제하곤 했었는데 TypeScript 3.9 버전부터는 코드 line을 유지해준다.
화살표 함수에 중괄호를 추가할 때 리턴하던 값을 return 문으로 바꾸는 걸 잊어버리는 실수를 할 때가 많다. (알고 있더라도 조금 귀찮음)
// before
let f1 = () => 42
// oops - not the same!
let f2 = () => { 42 }
TypeScript 3.9 버전부터 return
문을 추가하거나, 중괄호를 제거하거나, 화살표 함수에 괄호를 추가하는 상황에서 quick-fix 기능을 사용할 수 있다! 👏👏
최근 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 기능을 끊어버리기 때문에 foo
가 undefined
일 경우, (foo?.bar)
까지만 undefined
로 평가된다. 이때 baz
에 접근하면 baz
는 undefined
의 프로퍼티를 참조하는 것이므로 런타임 에러가 발생한다.
위 코드에서 !
연산자가 bar
의 타입이 null
과 undefined
가 아님을 명시하려고 사용되었다면 평가 시에 사라지는 게 직관적이다. 즉, 아래와 같이 해석되어야 한다.
foo?.bar.baz // TypeScript 3.9 버전의 해석 방법
foo
가 undefined
일 때 위 표현식은 undefined
로 평가된다.
만약 foo
가 null
/ undefined
가 아닐 때 bar
가 null
/ undefined
가 아니라는 걸 명시하고 싶다면 이제부터는
(foo?.bar)!.baz
이렇게 사용해야 한다!!
JSX 에서는 }
와 >
를 텍스트 포지션에 사용할 수 없게 했었는데, TypeScript와 Babel이 규칙을 완화하기로 했다.
>
, &rbrace
)를 사용하던가{'>'}
또는 {'}'}
처럼 쓰면 된다.<span> 2 {">"} 1 </div>
일반적으로, 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'.
가끔 존재하지 않는 값이라는 의미만 가지는 타입을 설정해야할 때가 있다. (고 한다.) 예를 들어,
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);
Circle
과 Square
의 kind
필드가 호환되지 않으므로 intersection을 만들 수 없는데 이전 버전 TypeScript에서는 위의 코드가 동작했다. kind
는 만들어질 수 없는 타입이므로 타입을 never
로 해서 만들었다.
TypeScript 3.9 버전부터는 타입 시스템이 더 엄격해진다. Circle
과 Square
의 kind
프로퍼티 때문에 intersect를 할 수 없다는 걸 인지한다. 따라서 Circle & Square
의 kind
프로퍼티의 타입을 never
로 만들지 않고 Circle & Square
자체를 never
타입으로 지정한다.
이전 버전 TypeScript에서 클래스 내부의 get
과 set
접근자는 enumerable하게 생성됐으나 ECMAScript 에서는 get과 set은 non-enumerable이라고 규정하고 있다. TypeScript 3.9버전 부터는 non-enumerable하게 만들어 ECMAScript 표준에 가까워졌다.
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 선언은 유지된다.