티스토리 뷰

카테고리 없음

Typescript 3.8 변경사항

박스여우 2020. 6. 14. 16:35

Type-Only Imports and Exports


이 기능은 대부분의 경우 필요하지 않은 기능이지만, 만약 --isolatedModules 옵션이나 타입스크립트, babeltranspileModule API를 사용해 컴파일 할 때 이슈가 발생한다면, 이 기능이 필요할 수 있습니다.

 

Typescript는 타입을 참조하기 위해 JavaScript의 import 문법을 재사용 사용합니다. 예를 들어, 아래의 예제에서 우리는 순수 TypeScript 타입인 Options과 함께 JavaScript 변수인 doThing을 import 합니다.

// ./foo.ts
interface Options {
    // ...
}

export function doThing(options: Options) {
    // ...
}

// ./bar.ts
import { doThing, Options } from "./foo.js";

function doThingBetter(options: Options) {
    // do something twice as good
    doThing(options);
    doThing(options);
}

 

TypeScript는 import elision 기능을 제공하여 JavaScript로 컴파일 될 때 타입으로 사용되는 것들을 모두 지워버립니다. JavaScript에서는 type이 없기 때문이죠. 위의 예제의 경우 Options는 타입으로써 사용되는 것을 확인하고, 자동적으로 import를 지워버립니다. 따라서 위 예제의 컴파일 결과는 아래와 같습니다.

// ./foo.js
export function doThing(options: Options) {
    // ...
}

// ./bar.js
import { doThing } from "./foo.js";

function doThingBetter(options: Options) {
    // do something twice as good
    doThing(options);
    doThing(options);
}

이 기능은 대부분의 경우 훌륭하게 사용되지만 몇몇의 경우 문제가 됩니다.

 

가장 먼저, export 되는 것이 변수인지 타입인지 모호한 경우가 몇가지 있습니다. 아래 예시의 경우 MyThing이 변수인지 타입인지 알 수 없습니다.

import { MyThing } from "./some-module.js";

export { MyThing };

우리가 위 예제에서 export 된 MyThing이 변수로 사용되는지, 타입으로 사용되는지 알 수 있는 방법이 없습니다. Babel과 TypeScript의 transpileModule API 도 마찬가지로 MyThing이 타입일 경우 제대로 동작하지 않는 코드를 만들어 낼 것 입니다. 그리고 TypeScript의 isolatedModules 플래그는 이것이 문제가 될 것이라고 알려줄 것 입니다. 진짜 문제는 "아니야, 이건 타입으로만 사용될 거니까 지워져야해" 라고 알려줄 방법이 없다는 것 입니다. 따라서 import elision 기능만으로는 부족한 부분이 있습니다.

 

또 다른 이슈는 TypeScript의 import elision 기능은 타입을 import 하는 구문이라면 모두 제거해버립니다. 이는 부수적인 효과를 가지고 있는 모듈을 import 할 때 문제가 발생할 수 있습니다. 따라서 유저들은 원하는 동작을 위해 두 번째 import 선언을 추가해야 합니다.

// This statement will get erased because of import elision.
import { SomeTypeFoo, SomeOtherTypeBar } from "./module-with-side-effects";

// This statement always sticks around.
import "./module-with-side-effects";

 

구체적인 예시로는 Angular.js 같은 프레임워크에서 찾아볼 수 있습니다. Angular.js에서 services 는 전역적으로 등록되고, 사용하는 쪽에서 등록된 서비스를 주입받게 됩니다. 사용하는 쪽에서 이 서비스들은 타입으로 사용되기 위해 import 합니다. 결과적으로  ./service.js 는 절대로 실행되지 않고 런타임에 문제가 발생합니다.

// ./service.ts
export class Service {
    // ...
}
register("globalServiceId", Service);

// ./consumer.ts
import { Service } from "./service.js";

inject("globalServiceId", function (service: Service) {
    // do stuff with Service
});

 

이러한 이슈가 발생하는 것들을 해결하기 위해 어떤 것들이 import 되고 지워져야 하는지 유저들이 세밀한 조작을 할 수 있는 문법이 추가되었습니다.

 

TypeScript 3.8에서는 이에 대한 해결책으로 type-only imports and exports 문법이 추가되었습니다.

import type { SomeThing } from "./some-module.js";

export type { SomeThing };

import type 은 타입 type annoations과 declarations 을 위해 사용되는 정의만 import 하게 됩니다. 이는 항상 지워지고, 런타임에 남은 잔재물이 없습니다. export type 도 비슷하게 타입을 추론할 때에만 사용되는 export만 제공합니다. 그리고 이 또한 컴파일할 때 지워집니다.

 

이는 기억해야할 중요한 점입니다. 클래스는 런타임에는 변수를 가지고 design-time에는 타입을 가집니다. 그리고 이를 상황에 맞게 사용합니다. 만약 클래스를 import 할 때 import type 을 사용한다면, 상속과 같은 것들을 할 수 없습니다.

import type { Component } from "react";

interface ButtonProps {
    // ...
}

class Button extends Component<ButtonProps> {
    //               ~~~~~~~~~
    // error! 'Component' only refers to a type, but is being used as a value here.

    // ...
}

만약 Flow를 사용해본 경험이 있다면, 이 문법은 상당히 비슷하게 느껴질 것입니다. 한 가지 다른 차이점은 모호하게 만들 수 있는 코드를 피하기 위해 몇가지 제한 사항을 추가했다는 것입니다.

// Is only 'Foo' a type? Or every declaration in the import?
// We just give an error because it's not clear.

import type Foo, { Bar, Baz } from "some-module";
//     ~~~~~~~~~~~~~~~~~~~~~~
// error! A type-only import can specify a default import or named bindings, but not both.

런타임에 사용되지 않는 imports 를 제어하기 위한 새로운 컴파일러 importsNotUsedAsValues flag도 추가되었습니다. 이 플래그는 3개의 설정 값 중 하나를 사용할 수 있습니다.

  • remove : 기존 컴파일러의 동작과 동일하게 런타임에 사용되지 않는 imports 들을 제거합니다.
  • preserve : 타입 정보는 제거 되지만, 해당 모듈의 imports 는 유지합니다. 따라서 위의 imports/side-effects 문제상황을 해결할 수 있습니다.
  • error : 이는 preserve 옵션과 동일하게 동작하지만 타입으로써만 import를 사용했을 때에 오류를 발생시킵니다. 만약 변수가 아닌 것들이 우연하게 import 되는 것을 보호하고 싶을 때 유용할 것 입니다.

해당 기능에 대한 더 많은 정보를 위해서 pull request 와 연관된 변경사항 등을 살펴볼 수 있습니다.

 

ECMAScript Private Fields


TypeScript 3.8은 ECMAScript의 private fields 를 지원합니다. (state-3 class fields proposal)

class Person {
    #name: string

    constructor(name: string) {
        this.#name = name;
    }

    greet() {
        console.log(`Hello, my name is ${this.#name}!`);
    }
}

let jeremy = new Person("Jeremy Bearimy");

jeremy.#name
//     ~~~~~
// Property '#name' is not accessible outside class 'Person'
// because it has a private identifier.

private fields 는 몇가지 규칙이 있습니다.

  • Private field의 이름은 # 으로 시작해야 합니다.
  • 모든 private field name은 해당 private field를 가지고 있는 class에 고유하게 범위가 지정됩니다.
  • public, private 같은 TypeScript의 접근 제한자는 private fields에 사용할 수 없습니다.
  • private fields 해당 클래스의 외부에서 접근되거나, 감지조차 될 수 없습니다. -- JS 유저들이라 하더라도! 이를 hard privacy 라고 부릅니다.

private field가 주는 이점은 hard privacy 외에도, 해당 private field 를 가지고 있는 class에 고유하게 범위가 지정된다는 점 입니다. 예시로 일반적인 프로퍼티 선언은 자식 클래스에서 덮어씌워지기 쉽습니다.

class C {
    foo = 10;

    cHelper() {
        return this.foo;
    }
}

class D extends C {
    foo = 20;

    dHelper() {
        return this.foo;
    }
}

let instance = new D();
// 'this.foo' refers to the same property on each instance.
console.log(instance.cHelper()); // prints '20'
console.log(instance.dHelper()); // prints '20'

하지만 private fields를 사용한다면 이에 대해 걱정할 필요가 없어집니다. 각 private field의 이름은 해당하는 클래스에서 고유하게 유지됩니다.

class C {
    #foo = 10;

    cHelper() {
        return this.#foo;
    }
}

class D extends C {
    #foo = 20;

    dHelper() {
        return this.#foo;
    }
}

let instance = new D();
// 'this.#foo' refers to a different field within each class.
console.log(instance.cHelper()); // prints '10'
console.log(instance.dHelper()); // prints '20'

주의 깊게 봐야할 또 다른 점은 다른 타입의 private field에 접근한다면 TypeError가 발생합니다. (private field 가 있는 타입끼리는 덕타이핑이 안된다)

class Square {
    #sideLength: number;

    constructor(sideLength: number) {
        this.#sideLength = sideLength;
    }

    equals(other: any) {
        return this.#sideLength === other.#sideLength;
    }
}

const a = new Square(100);
const b = { sideLength: 100 };

// Boom!
// TypeError: attempted to get private field on non-instance
// This fails because 'b' is not an instance of 'Square'.
console.log(a.equals(b));

마지막으로, 순수 .js 파일에서 사용할 경우 private fields는 항상 할당 되기 전에 선언되어야 합니다.

class C {
    // No declaration for '#foo'
    // :(

    constructor(foo: number) {
        // SyntaxError!
        // '#foo' needs to be declared before writing to it.
        this.#foo = foo;
    }
}

JavaScript는 항상 선언되지 않은 프로퍼티에 접근할 수 있도록 해주었고, TypeScript는 항상 속성을 정의해야 했습니다. 하지만 private fields를 사용한다면 JavaScript, TypeScript 상관 없이 항상 선언이 필요합니다.

class C {
    /** @type {number} */
    #foo;

    constructor(foo: number) {
        // This works.
        this.#foo = foo;
    }
}

더 많은 정보를 위해 pull request 를 살펴볼 수 있습니다.

 

그럼 private 키워드와 hash(#)중 무엇을 사용해야 하나?


상황에 따라 적절하게 골라서 사용해야 합니다.

 

TypeScript의 private 접근자는 항상 지워집니다. 따라서 런타임에는 일반적인 property 과 동일하게 행동하며, 이 프로퍼티가 private 접근자로 선언되었다고 알려줄 방법이 없습니다. private 키워드를 사용한다면 privacy는 compile-time/design-time에서만 적용됩니다. 컴파일 이후에는 JavaScript 사용자의 의도에 맡기게 됩니다.

class C {
    private foo = 10;
}

// This is an error at compile time,
// but when TypeScript outputs .js files,
// it'll run fine and print '10'.
console.log(new C().foo);    // prints '10'
//                  ~~~
// error! Property 'foo' is private and only accessible within class 'C'.

// TypeScript allows this at compile-time
// as a "work-around" to avoid the error.
console.log(new C()["foo"]); // prints '10'

이러한 "soft privacy" 는 일시적으로 특정 API에 접근 권한이 없는 유저들에게 기능을 제공하거나, 모든 런타임에서 동작할 수 있도록 할 때 유용합니다.

 

반면에, ECMAScript의 hash(#) 는 완벽하게 클래스 외부에서 해당 프로퍼티에 접근할 수 없도록 만들어줍니다.

class C {
    #foo = 10;
}

console.log(new C().#foo); // SyntaxError
//                  ~~~~
// TypeScript reports an error *and*
// this won't work at runtime!

console.log(new C()["#foo"]); // prints undefined
//          ~~~~~~~~~~~~~~~
// TypeScript reports an error under 'noImplicitAny',
// and this prints 'undefined'.

이러한 Hard privacy는 아무도 내부에 어떠한 것도 가져갈 수 없다는 것을 강력하게 보장할 때 유용합니다. 만약 당신이 라이브러리의 개발자라면 private field를 지우거나 이름을 바꾸는 것이 사용자들에게는 어떠한 충돌도 발생시키지 않는 다는 것 입니다.

 

위에서 언급했듯이, 또 다른 이점은 hash(#)를 사용함으로써 서브클래싱이 더 쉬워질 수 있습니다. hash(#)를 사용한 private fields들은 하위 클래스에서 이름이 충돌할 걱정을 하지 않아도 됩니다. TypeScript의 private 제한자로 선언되었다면, 하위 클래스를 선언할 때에 여전히 상위 클래스의 프로퍼티를 재정의하진 않았는지 주의해야 합니다.

 

한가지 더 생각해봐야 할 점은 어디에서 당신의 코드가 실행되는지 입니다. TypeScript는 ECMAScript 2015(ES6) 또는 그 이상을 타겟으로 하지 않으면 hash(#) private 기능을 지원할 수 없습니다. 이는 hash(#)을 사용할 때 ECMAScript 내부적으로 ES6이상 부터 추가된 WeakMap사용하여 private field를 보호하기 때문입니다. WeakMap하위 버전(ES6 이하)에서 메모리 릭을 발생시키지 않으면서 대체(polyfilled)할 방법이 없습니다. 반면에 TypeScript의 private 키워드 선언 프로퍼티는 모든 타겟에서 동작할 수 있습니다. ECMAScript 3일지라도!

 

마지막 고려사항은 속도에 관한 문제입니다: TypeScript의 private 제한자로 정의된 프로퍼티는 다른 일반적인 프로퍼티와 다르지 않습니다. 따라서 이에 접근할 때 다른 프로퍼티에 접근 할 때와 같이 빠릅니다. 대조적으로 hash(#) private field는 내부적으로 WeakMap을 사용하기 때문에 사용할 때 비교적 느립니다. 몇몇 런타임에서는 # private field의 구현을 최적화 시켰고, WeekMap 구현체보다는 빠를 수 있지만 모든 런타임에 해당하는 이야기는 아닙니다.

 

export * as ns 문법


특정 모듈의 모든 멤버를 하나의 멤버처럼 만들고 export 할 때 종종 아래와 같은 코드를 작성합니다.

import * as utilities from "./utilities.js";
export { utilities };

이러한 패턴을 지원하기 위해 최근 ECMAScript 2020 에 새로운 문법이 추가되었습니다.

export * as utilities from "./utilities.js";

TypeScript 3.8에서도 해당 문법이 추가되었습니다. 만약 당신의 모듈의 컴파일 target이 es2020보다 하위 버전이라면, 타입스크립트 컴파일 결과로는 첫 번째 예시와 같이 출력됩니다. 

 

추가적인 정보는 pull request 에서 확인해볼 수 있습니다.

 

Top-Level await


대부분의 현대 JavaScript환경은 I/O와 같은 작업을 비동기방식으로 제공합니다. 이러한 API들은 이러한 비동기 작업들을 Promise로 반환합니다. 작업들을 non-blocking으로 만듬으로써 많은 장점을 얻을 수 있기 때문입니다. 

fetch("...")
    .then(response => response.text())
    .then(greeting => { console.log(greeting) });

JavaScript 개발자들은 Promises 를 사용할 때 .then 체인을 기피하기 위해 async 함수 안에서 await 을 사용하도록 정의합니다. 그리고 그 함수를 즉시 호출합니다.

async function main() {
    const response = await fetch("...");
    const greeting = await response.text();
    console.log(greeting);
}

main()
    .catch(e => console.error(e))

하지만 TypeScript 3.8 부터는 async 함수로 감싸는 것도 필요하지 않습니다. ECMAScript에 새롭게 추가되는 편리한 기능인 "top-level await" 를 사용할 수 있기 때문입니다.

 

이전의 JavaScript에서는 (비슷한 기능을 가진 대부분의 다른 언어들도 마찬가지로), await은 오직 async 함수 내부에서만 사용될 수 있었습니다. 하지만 top-level await은 모듈의 top level 에서 await를 사용 할 수 있게 해 줍니다.

const response = await fetch("...");
const greeting = await response.text();
console.log(greeting);

// Make sure we're a module
export {};

top-level await 은 오직 모듈의 top level에서만 동작합니다. 그리고 파일들은 오직 import, export 를 통해 발견되었을 때에만 모듈로 간주됩니다.

 

Top level await 는 모든 환경에서 동작하지 않습니다. 현재로써는 compile targetes2017 혹은 그 이상으로, module esnext 혹은 system으로 지정 해야 사용할 수 있습니다.

 

원본 pull request

JSDoc Property Modifiers


TypeScript 3.8은 allowJs flag를 사용함으로써 JavaScript 파일을 지원합니다. 또한 checkJs 옵션 이나 //@ts-check 주석을 통해 JavaScript 파일의 타입 체킹을 할 수 있습니다.

 

JavaScript 파일은 타입 체킹을 위한 전용 문법을 가지고 있지 않기 때문에 TypeScript는 JSDoc을 활용합니다. TypeScript 3.8은 프로퍼티를 위해 사용되는 몇가지 JSDoc의 태그를 이해합니다.

 

첫 번째는 접근 제한자 입니다: @public, @private, @protected 이 태그들은 각각 TypeScript에서 public, private, protected 접근 제한자와 동일하게 동작합니다.

// @ts-check

class Foo {
    constructor() {
        /** @private */
        this.stuff = 100;
    }

    printStuff() {
        console.log(this.stuff);
    }
}

new Foo().stuff;
//        ~~~~~
// error! Property 'stuff' is private and only accessible within class 'Foo'.

 

다음으로, @readonly 제한자도 추가되었습니다.

// @ts-check

class Foo {
    constructor() {
        /** @readonly */
        this.stuff = 100;
    }

    writeToStuff() {
        this.stuff = 200;
        //   ~~~~~
        // Cannot assign to 'stuff' because it is a read-only property.
    }
}

new Foo().stuff++;
//        ~~~~~
// Cannot assign to 'stuff' because it is a read-only property.

 

이 외에 추가적인 변경사항은 아래에 원문을 참조하시면 됩니다.

원문


 

Announcing TypeScript 3.8 | TypeScript

Today we’re proud to release TypeScript 3.8! For those unfamiliar with TypeScript, it’s a language that adds syntax for types on top of JavaScript which can be analyzed through a process called static type-checking. This type-checking can tell us about err

devblogs.microsoft.com

 

댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함