타입스크립트 3.6이 나왔다!


원문: https://devblogs.microsoft.com/typescript/announcing-typescript-3-6/

타입스크립트 3.6이 배포되었다. 유용하게 사용할 수 있는 타입스크립트 3.6의 기능을 소개한다.

타입스크립트는 선택적 정적 타입(optional static types) 개념이 추가된 자바스크립트 기반 언어이다. 타입은 타입스크립트 컴파일러를 통해 검사되는데, 맞춤법 오류와 잘못된 방식으로 호출된 함수와 같이 프로그램에서 발생될 수 있는 일반적인 오류들을 검출한다. 타입스크립트 컴파일러와 바벨과 같은 도구들을 사용하면, 최신 표준 기능을 사용하여 작성된 타입스크립트 코드를 모든 브라우저와 런타임(ES3 또는 ES5를 지원하는 훨씬 오래된 코드)에서 작동하는 호환성있는 표준 ECMAScript 코드로 변환할 수 있다.

타입스크립트는 단순히 타입을 검사하고, 새로운 ECMAScript 기능을 가지는 것 외에도 다른 강력한 기능을 제공한다. 편집 도구 설정(tooling)은 타입스크립트 프로젝트에서 중요한 핵심 부분이다. 다양한 편집기에서 코드 자동 완성 기능, 리팩토링 및 빠른 해결책을 제공한다. 비주얼 스튜디오/비주얼 스튜디오 코드에서 자바스크립트 파일을 수정해 보았다면, 이미 타입스크립트를 사용하고 있었을 수도 있다. 실제로 타입스크립트에 의해 타입 정보가 제공되기 때문이다.

자세한 내용은 타입스크립트 웹사이트에서 확인할 수 있다. 바로 시작하려면 NuGet을 통해 얻거나 npm 명령어를 사용하여 설치할 수 있다.

npm install -g typescript

아래 링크를 통해 편집기에서 타입스크립트에 대한 지원 정보를 확인할 수 있다.

조만간 다른 편집기에 대한 지원도 시작될 것이다.

3.6의 내용을 살펴보자!

더 엄격해진 제너레이터

타입스크립트 3.6에서는 이터레이터와 제너레이터 함수에 대해 좀 더 엄격한 타입 검사가 도입되었다. 이전 버전에서는 제너레이터 사용자가 제너레이터 함수에서 반환된 값인지, yield가 반환한 값인지 구별할 방법이 없었다.

function* foo() {
  if (Math.random() < 0.5) yield 100;

  return "Finished";
}

let iter = foo();
let curr = iter.next();
if (curr.done) {
  // 타입스크립트 3.5 이하 버전에서 작업할 때는 curr.value가 '문자열'이거나 '숫자'라고 생각했다.
  // 'done'이 '참'일 때 curr.value는 '문자열'임을 알고 있어야 한다!
  curr.value
}

또한, 제너레이터는 yield의 타입을 항상 any라고 가정했다.

function *bar() {
  let x: { hello(): void } = yield;
  x.hello();
}

let iter = bar();
iter.next();
iter.next(123); // 앗! 런타임 오류 발생!

타입스크립트 3.6에서 검사기는 첫번째 예제의 curr.value의 올바른 타입이 string임을 알고 있으며, 두번째 예제의 마지막 라인에 next(123) 호출 시 올바르게 오류를 발생할 것이다. 몇 개의 매개변수 타입 정보을 포함하고 있는 IteratorIteratorResult 타입 선언에 변경 사항이 있었으며, 타입스크립트가 제너레이터를 표현하기 위한 새로운 Generator타입도 추가되었다.

Iterator 타입은 yield로 반환된 타입, 함수에서 반환된 타입, next에서 받을 수 있는 매개 변수의 타입을 지정할 수 있다.

interface Iterator<T, TReturn = any, TNext = undefined> {
    // 0 또는 1 개의 인자를 가진다 - undefined는 허용하지 않음
    next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
    return?(value?: TReturn): IteratorResult<T, TReturn>;
    throw?(e?: any): IteratorResult<T, TReturn>;
}

새로운 Generator타입은 Iterator이며, returnthrow 둘 다 항상 가지고 있다. 또한 반복가능(iterable)한 특성을 가진다.

interface Generator<T = unknown, TReturn = any, TNext = unknown> extends Iterator<T, TReturn, TNext> {
    next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
    return(value: TReturn): IteratorResult<T, TReturn>;
    throw(e: any): IteratorResult<T, TReturn>;
    [Symbol.iterator](): Generator<T, TReturn, TNext>;
}

타입스크립트 3.6에서는, 함수에서 반환된 값과 yield에서 반환된 값을 허용하도록 IteratorResult 타입을 union타입으로 변환하였다.

type IteratorResult<T, TReturn = any> = IteratorYieldResult<T> | IteratorReturnResult<TReturn>;

interface IteratorYieldResult<TYield> {
    done?: false;
    value: TYield;
}

interface IteratorReturnResult<TReturn> {
    done: true;
    value: TReturn;
}

즉, 제너레이터를 다룰 때 이터레이터 값의 타입을 적절히 나눠서 처리할 수 있다.

또한 타입스크립트 3.6에서는, next()호출하면서 제너레이터 함수에 전달할 수 있는 타입을 올바르게 검사하기 위해 제너레이터 함수 본문 내에서 yield사용 시 타입을 추론할 수 있도록 한다.

function* foo() {
    let x: string = yield;
    console.log(x.toUpperCase());
}

let x = foo();
x.next(); // 첫번째 'next' 호출은 항상 무시됨.
x.next(42); // 오류! '숫자'는 '문자열'로 할당될 수 없음

명시적인 작성법을 선호한다면, 명시적 반환 타입을 사용하여 반환된 값, yield 표현식으로부터 평가된 값, yield에서 반환된 값의 타입을 명시적으로 지정할 수 있다. 아래 코드는, next()boolean값으로만 호출할 수 있으며, done값에 따라 value값이 문자열이나 숫자가 될 수 있다.

/**
 * - yields numbers (yield 숫자 반환)
 * - returns strings (함수의 반환 타입은 문자열)
 * - can be passed in booleans (next의 매개변수 타입은 boolean)
 */
function* counter(): Generator<number, string, boolean> {
    let i = 0;
    while (true) {
        if (yield i++) {
            break;
        }
    }
    return "done!";
}

var iter = counter();
var curr = iter.next()
while (!curr.done) {
    console.log(curr.value);
    curr = iter.next(curr.value === 5)
}
console.log(curr.value.toUpperCase());

// prints:
//
// 0
// 1
// 2
// 3
// 4
// 5
// DONE!

좀 더 자세한 변경 사항을 보고싶다면, 깃헙 PR에서 확인할 수 있다.

더 정확해진 배열에서 전개(Spread) 연산자 사용

대상(target)이 ES2015 이전으로 설정된 환경에서, for/of루프나 배열 전개(Array Spread)로 작성된 코드를 JS로 내보내면(emit) 코드량이 많아져 무거워질 수 있다. 이 때문에, 타입스크립트에서는 기본적인 배열 타입만 지원하여 JS로 내보내며, --downlevelIteration 플래그를 사용하여 다른 타입의 이터레이팅을 지원한다. --downlevelIteration를 사용하면 정확하게 코드를 내보낼 수 있지만 코드량이 엄청나게 커진다.

--downlevelIteration옵션은 기본적으로 비활성화되어있다. ES5를 대상으로 하는 대부분의 사용자들은 일반적인 반복 기능을 사용하여 배열을 사용할테니 문제 없이 잘 동작할 것이다. 그러나 경우에 따라서는, 작성한 배열 코드가 의도하지 않는 동작으로 내보내어질 때가 있었다.

예를 들어, 아래처럼 작성한 코드가 있다.

[...Array(5)]

다음과 같이 평가될 것이라 예상했다.

[undefined, undefined, undefined, undefined, undefined]

그러나 타입스크립트 3.5 이전 버전에서 [...Array(5)]를 아래와 같이 변환한다.

Array(5).slice()

Array(5).slice()를 하면 엘리먼트가 배치되는 슬롯은 정의되지 않고, 길이가 5인 배열을 만들게 된다.

1 in [undefined, undefined, undefined] // true
1 in Array(3) // false

타입스크립트에서 slice()를 호출하면, 인덱스가 설정되어 있지 않는 배열을 만든다.

조금 이해하기 어려울 수 있는데, 실제로 많은 사용자들이 의도하지 않는 동작으로 어려움을 겪고 있었다.

타입스크립트 3.6에서는 내장된 slice()를 사용하는 대신, 새로운 __spreadArrays헬퍼 함수가 도입되었다. 배열에서 전개 연산자를 사용할 때 ECMAScript 2015에서 보여지는 결과와 정확히 같게 나올 것이다. --downlevelIteration.__spreadArrays을 사용하지 않는 더 오래된 대상(target)으로 설정된 환경에서도 tslib__spreadArrays를 사용할 수 있다.(작은 크기의 번들을 원한다면 tslib를 확인해보는 것도 도움이 될 것이다.)

자세한 내용은 해당 작업의 PR을 참조해라.

Promise 관련 개선된 UX 제공

Promise는 비동기 데이터를 다루는 가장 흔한 방법 중 하나이다. 하지만 Promise지향의 API를 사용하는 것은 종종 사용자들에게 혼란을 준다. 타입스크립트 3.6에서는 Promise를 잘못 사용했을 때를 위한 몇 가지 개선 사항을 알려준다.

예를 들어, Promise의 응답 객체를 다른 함수로 전달하기 전에 .then() 하거나 await 하는 것을 잊어버릴 수 있다. 타입스크립트의 오류 메세지는 전문화 되어, 이러한 사용자들에게 await 키워드 사용을 고려해야 한다고 알려준다.

interface User {
    name: string;
    age: number;
    location: string;
}

declare function getUserData(): Promise<User>;
declare function displayUser(user: User): void;

async function f() {
    displayUser(getUserData());
//              ~~~~~~~~~~~~~
//  getUserData()에서 반환된 'Promise<User>'타입은 displayUser의 파라미터 타입(User)으로 넘길 수 없다.
//   ...
// 'await' 사용을 잊었습니까?
}

또 흔한 실수 중의 하나는 Promiseawait 또는 .then()을 받기 전에 응답 객체의 메서드에 접근을 시도하는 것이다.

async function getCuteAnimals() {
    fetch("https://reddit.com/r/aww.json")
        .json()
    //   ~~~~
    //'Promise<Response>'타입에 'json'속성이 존재하지 않는다.
    //
    // 'await' 사용을 잊었습니까?
}

위에서 타입스크립트의 오류 메세지는 적어도 사용자가 await을 몰랐더라도 어떤 액션을 취해야할지 알려준다.

Promise의 오류 메세지 개선 외에도 빠른 해결책을 제공하는 등 개발 편의성을 향상시킨다.

image

자세한 내용은 이슈를 확인한다음, 해당 이슈에 연결된 PR을 통해 알아보자.

식별자에 대한 더 나은 유니코드 지원

타입스크립트 3.6에서는 대상(target)이 ES2015이상이면 식별자에 사용된 유니코드 문자열을 더욱 잘 지원한다.

// 이전에는 허용되지 않았음.
// target이 es2015이상일 때 허용 (예: '--target es2015')
const 𝓱𝓮𝓵𝓵𝓸 = "world";

SystemJS에서 import.meta 지원

타입스크립트 3.6에서는 tsconfig.json파일 compilerOptions에서 "module": "system"인 경우 import.metacontext.meta로 변환할 수 있도록 지원한다.

// 모듈:

console.log(import.meta.url)

// 다음과 같이 변함:

System.register([], function (exports, context) {
  return {
    setters: [],
    execute: function () {
      console.log(context.meta.url);
    }
  };
});

앰비언트(Ambient) 컨텍스트에서 getset접근자 허용

이전 버전의 타입스크립트에서는, 앰비언트 컨텍스트(일반적으로 .d.ts파일 또는 declare class [클래스 명]를 의미)의 get, set 접근자를 허용하지 않았다. get, set 접근자를 사용하면 해당 속성을 읽거나 쓰는것이 구별되지 않았다. 그러나 ECMAScript의 클래스 필드 스펙과는 다르게 기존 타입스크립트 버전에서 동작할 수 있다. 이렇게 다르게 동작하는 것을 서브클래스에 알려주고 적절한 오류를 표시해야 한다고 판단했다.

이제, 타입스크립트 3.6에서는 앰비언트 컨텍스트의 getter와 setter로 작성할 수 있다.

declare class Foo {
    // 3.6 이상에서 허용.
    get x(): number;
    set x(val: number): void;
}

타입스크립트 3.7에서는 컴파일러 자체에서 이 기능을 이용하여, 생성된 .d.ts파일 내용에는 사용된 get/set 접근자도 포함될 것이다.

앰비언트 클래스와 함수의 병합

타입스크립트 이전 버전에서는, 클래스와 함수의 타입을 병합할 수 없었다. 같은 이름으로 function과 class 타입을 선언하면 무조건 오류가 표시되었지만 3.6에서는 앰비언트 클래스와 함수(declare수식어가 붙은 클래스나 함수 또는 .d.ts파일을 의미)를 병합할 수 있다. 아래와 같이 사용해도 오류를 발생하지 않는다.

export declare function Point2D(x: number, y: number): Point2D;
export declare class Point2D {
    x: number;
    y: number;
    constructor(x: number, y: number);
}

이전에는 아래 코드처럼 작성했을 것이다.

export interface Point2D {
    x: number;
    y: number;
}
export declare var Point2D: {
    (x: number, y: number): Point2D;
    new (x: number, y: number): Point2D;
}

같은 이름으로 함수와 클래스 선언이 가능해지면서 호출 가능한 생성자 패턴이 쉽게 표현될 수 있으며 네임스페이스가 이러한 선언들과 병합될 수 있는 이점이 있다. (var로 선언된 타입은 같은 이름으로 선언된 다른 타입과 병합 불가하다.)

타입스크립트 3.7에서, 컴파일러는 이 기능을 활용할 것이다. .js파일에서 생성된 .d.ts파일은 생성자를 사용하거나 호출 가능한 클래스형 함수를 적절하게 찾을 수 있을 것이다.

좀 더 자세한 내용을 알고 싶다면 깃헙 PR을 확인해라.

--build--incremental 지원 API

타입스크립트 3.0에서는 다른 프로젝트를 참조하고, 참조된 프로젝들의 빌드 시간을 향상시키기 위해 --build플래그를 도입했다. 또한, 타입스크립트 3.4에서 --incremental플래그가 도입되었는데, 이전 컴파일 정보는 저장되고 특정 파일에 대해서만 다시 빌드할 수 있는 기능을 제공하였다. --build--incremental플래그를 사용하면, 프로젝트를 좀 더 유연하게 만들고, 빌드하는 속도가 크게 개선되는 이점이 있다. 그러나 이러한 플래그는 걸프(Gulp)와 웹팩(Webpack)과 같은 써드 파티 빌드 도구와 함께 동작할 수 없었다. 타입스크립트 3.6에서는 프로젝트를 참조하고 증분 빌드(incremental build - 반복적인 빌드 과정에서 변경된 소스코드에 의존성이 있는 대상들만 모아 다시 빌드하는 기능)를 위한 두 세트의 API를 공개한다.

--incremental빌드를 하기 위해서는, 사용자는 createIncrementalProgramcreateIncrementalCompilerHost API를 활용할 수 있다. 새로 공개된 readBuilderProgram함수를 사용하여 생성된 .tsbuildinfo파일에서 이전 프로그램에서 만들어진 인스턴스 정보를 재공급할 수 있다. 이는 새 프로그램 생성 시에만 사용될 수 있다. 즉, 다른 create*Program함수에서 oldProgram파라미터에 사용되고,반환된 인스턴스는 수정할 수 없다.

프로젝트 참조(project references) 기능을 활용하기 위해 새로운 createSolutionBuilder함수가 공개되었으며, 이 함수는 새로운 타입의 SolutionBuilder 인스턴스를 반환한다.

API에 대한 자세한 내용은 깃헙 PR에서 확인할 수 있다.

새 타입스크립트 플레이그라운드

새 플레이그라운드는 커뮤니티 구성원들이 점점 더 많이 사용하고 있는 아르템 타이루린(Artem Tyurin)타입스크립트 플레이그라운드를 포크해서 사용했다. 편리한 새 기능들을 지원할 수 있게 되었으며 감사를 표한다.

새 옵션들을 지원한다.

  • target옵션 지원 (사용자가 es5에서 es3, es2015, esnext 등으로 전환할 수 있도록 허용한다)
  • strict옵션을 포함하여 모든 엄격(strictness) 모드에 관한 옵션 사용 가능
  • 일반 자바스크립트 파일 지원(allowJs와 선택적으로 checkJs옵션 사용 가능)

이러한 옵션은 플레이그라운드에서 작성한 예제를 링크로 공유할 때도 유지된다. 작성자가 다른사람에게 공유할 때 따로 옵션 사항에 대해 말하지 않아도 안정적으로 예제를 공유할 수 있다. 예를 들어, 다른 사람에게 예제를 공유할 때 noImplicitAny옵션을 켜서 확인해야한다고 따로 말하지 않아도 된다.

조만간, 플레이그라운드의 예제 코드들을 수정하고, JSX 지원 기능을 추가할 것이다. 또한 자동 완성 기능까지 지원할 계획이다. 개개인의 편집기를 사용할 때와 비슷한 느낌을 받을 것이다.

플레이그라운드와 타입스크립트 웹사이트가 꾸준히 개선될 수 있도록, 사이트에 대한 피드백과 PR을 남겨주길 바란다.

편집기에서 세미콜론 자동 삽입 개선

비주얼 스튜디오와 비주얼 스튜디오 코드와 같은 편집기를 사용하면 다른 모듈에서 값을 Auto-Import하고, 빠른 해결책 및 리팩토링 기능이 제공되면서 쉽게 수정할 수 있다.

이러한 제공되는 기능중에 하나로, 이전 타입스크립트에서는 모든 명령문 끝에 무조건 세미콜론을 추가해주었다. 그러나, 많은 사용자들이 명령문 끝이 무조건 세미콜론을 추가해주는 방식의 스타일 가이드를 동의하지 않았으며, 세미콜론을 편집기에서 자동으로 삽입해주는 것에 대해 불만을 가졌다.

이제 타입스크립트는 파일에서 세미콜론 사용 여부를 판단할 수 있다. 해당 파일이 세미콜론을 사용하지 않는다면 타입스크립트는 세미콜론을 추가하지 않을 것이다.

자세한 내용은 해당 작업의 PR을 통해 확인할 수 있다.

더 똑똑해진 Auto-Imports

자바스크립트는 ECMAScript 표준 모듈 시스템, Node 환경에서 지원하는 CommonJS, 그리고 AMD, System.js 등 다양한 모듈 구문과 규칙(convetions)을 가지고 있다. 타입스크립트는 ECMAScript 모듈 구문을 사용하여 Auto-Import 하도록 기본으로 설정되어있다. 그러나 다른 컴파일러 설정을 사용하는 타입스크립트 프로젝트이거나 일반 자바스크립트와 require호출을 사용하는 Node 프로젝트에서는 적합하지 않을 수도 있다.

타입스크립트 3.6은 더욱 똑똑해져서 현재 사용 중인 Import 방법에 따라 다른 모듈을 Auto-Import하는 방법을 결정한다. Auto-Import에 대한 자세한 내용은 여기에서 확인할 수 있다.

주요 변경 사항

클래스 멤버로 작성된 "constructor"메서드는 생성자 함수이다

ECMAScript 스펙에 따라, constructor메서드를 사용하여 선언한 클래스는 문자열("constructor")이든 식별자(constructor)를 사용하여 선언하든 생성자 함수이다.

class C {
    "constructor"() {
        console.log("생성자 함수이다.");
    }
}

단, "constructor"가 계산된 속성으로 사용되면 일반 메서드(plain method)가 된다.

class D {
    ["constructor"]() {
        console.log("생성자 함수가 아니고 일반 메서드이다!");
    }
}

DOM 업데이트

lib.dom.d.ts파일 내 선언되어 있는 많은 타입들이 제거되거나 변경되었다. 변경 내역을 확인해보자.

  • 전역 window는 더이상 Window 타입으로 정의되지 않는다. Window & typeof globalThis타입으로 정의 된다. 경우에 따라, typeof window 타입으로 참조할 수 있다.
  • GlobalFetch는 제거되었다. 대신에, WindowOrWorkerGlobalScope를 사용해라.
  • Navigator에서 몇 가지 비표준 속성이 제거되었다.
  • experimental-webgl 컨텍스트가 제거되었다. 대신에, webgl 또는 webgl2를 사용해라.

잘못된 변경이라고 생각되면 이슈로 제기해주길 바란다.

병합되지 않는 JSDoc 주석

자바스크립트 파일에서 타입스크립트는 선언된 유형을 파악하기 위해 바로 앞의 JSDoc 주석만 참조한다.

/**
 * @param {string} arg
 */
/**
 * 또 다른 주석 추가
 */
function whoWritesFunctionsLikeThis(arg) {
    // 'arg'는 'any'타입을 가진다. (string 타입이 아님)
}

키워드는 이스케이프 시퀀스를 포함할 수 없다.

이전에는 이스케이프 시퀀스가 포함될 수 있었다. 그러나 타입스크립트 3.6에서는 허용하지 않는다.

while (true) {
    \u0063ontinue;
//  ~~~~~~~~~~~~~
//  오류! 키워드는 이스케이프 문자열을 포함할 수 없다.
}

향후 계획

타입스크립트 팀에서 앞으로 진행할 작업에 대해 알고 싶다면 이번 년도 7월부터 12월까지의 6개월 로드맵을 확인해라.

언제나 그렇듯이, 이번 타입스크립트 릴리즈로 코딩 환경을 개선하고 즐겁게 개발할 수 있기를 기대한다. 제안할 것이 있거나 문제가 발생한다면 깃헙 이슈로 언제든지 문의하길 바란다.

-Daniel Rosenwasser 와 TypeScript 팀

조정은2019.09.09
Back to list