타입스크립트 5.2 변경점

dante Yoon·2023년 9월 3일
12

js/ts

목록 보기
10/14
post-thumbnail

유튜브 영상으로 보기: https://www.youtube.com/watch?v=4ey1bjAiQ1U

23.8.24 Typescript 5.2 RC 출시

안녕하세요, 단테입니다. 24일 타입스크립트 5.2 RC 버전이 출시되었습니다.
변경점에 대해 말씀드리고 가장 중요하게 생각되는 업데이트에 대해 요약해보겠습니다.

Beta랑 뭐가 달라?

5.2 beta 버전은 6월 30일에 공개되었습니다. beta 버전 사용을 위해서는 설치 시 beta 버전 명시를 해야 하며

npm install -D typescript@beta

type-checking optimization , reference the paths of Typescript implementation files in type-only imports 지원이 추가되었습니다.

type-checking optimization

interface A {
    value: A;
    other: string;
}

interface B {
    value: B;
    other: number;
}

위 예제에서 타입스크립트는 인터페이스 A,B가 호환가능한지 확인할 때 A,B의 value의 타입이 호환가능한지 체크합니다. 이 때 재귀형식으로 타입이 선언되어있어 value 타입을 체크하는 시점에 타입 체킹이 더 깊숙하게 진행되면 안됩니다.

이렇게 재귀 형식의 타입을 사용하는 경우는 데이터 베이스의 테이블을 타입스크립트로 표현할때 발생할 수 있습니다.
velog 댓글처럼 댓글안에 또 댓글이 있는 구조가 예시가 될 수가 있겠네요.

타입스크립트 5.2에서는 Set 구조를 사용해 타입체크 퍼포먼스를 올렸습니다.

Benchmark 1: old
  Time (mean ± σ):      3.115 s ±  0.067 s    [User: 4.403 s, System: 0.124 s]
  Range (min … max):    3.018 s …  3.196 s    10 runs

Benchmark 2: new
  Time (mean ± σ):      2.072 s ±  0.050 s    [User: 3.355 s, System: 0.135 s]
  Range (min … max):    1.985 s …  2.150 s    10 runs

Summary
  'new' ran
    1.50 ± 0.05 times faster than 'old'

자세한 코드 변경사항은 PR에서 확인하세요.

supporting reference the paths of Typescript implementation files in type-only imports

간단히 이야기하면 .ts, .mts, .tsx와 같이 타입스크립트를 사용하기 위한 t* 확장자 파일을 임포트할 때 allowImportingTsExtensions를 사용하지 않아도 된다는 이야기입니다.

예시로 프로젝트를 하나 생성하고 type.ts 에 interface를 생성했습니다.

그리고 App.tsx에서 A,B 타입을 임포트합니다.

타입스크립트에서는 ESM 모듈을 사용할 때.ts 확장자가 없어야지 해당 모듈을 가져올 수 있습니다. 확장자를 포함하여 임포트 하려고 하면 tsconfig.json의 allowImportingTsExtensions를 true로 변경해야 합니다.

그렇지 않고 다른 파일에서 타입 정보를 가져올 때는 확장자가 없이 사용해야 합니다.

a.ts: import * as b from "./b.ts";
b.ts: export const b: number = 0;

좀 더 자세히 알아보면 위와 같이 a.ts, b.ts가 작성되어있다고 할 때 tsc는 a.ts가 컴파일될 때 import specifiers를 변경하지 않습니다. a.ts가 컴파일되어 a.js로 변경될 때
"./b.ts" 구문은 그대로 남아있다는 것입니다. 하지만 b.ts파일은 tsc에 의해 b.js로 변경된 상태이기에 에러가 발생하게 됩니다.

왜 --noEmit, --emitDeclarationOnly를 같이 사용해야 하나

import * as b from "./b.ts"

작년 12월 14allowImportingTsExtensions 컴파일러 옵션이 추가되었고 이 옵션을 사용하기 위해서는 noEmit flag 또한 true로 설정되어야 했습니다. 그래야 b.ts 가 tsc에 의해 b.js로 변경되지 않기 때문입니다.

5.2 부터 allowImportingTsExtensions: true 설정 없이도 파일 확장자로부터 타입 정보를 import 할 수 있게 되었습니다.

5.1.3 버전에서 타입 임포트가 에러를 발생시키는 모습

5.2 버전 부터는 타입 임포트가 에러를 발생시키지 않습니다.

using Declarations and Explicit Resource Management

ECMAScript stage 3 proposal eksrPdls Explicit Resource Managementusing 키워드가 소개되었는데요 stage 4에 있지 않는 최신 문법을 사용하려면 babel, polyfill을 사용하던가 typescript 컴파일러를 사용해야 합니다. TSC에서 지원하는 ECMAScript 범위가 늘어났다고 이해하면 됩니다.

using 키워드가 무엇인가

https://velog.io/@jay/Typescript-5.2-using
https://velog.io/@jay/Typescript-5.2-using

자칫 잊어버리거나 코드를 복잡하게 만들 수 있는 리소스 해제를 간편하게 해주는 문법입니다.

before

export function doSomeWork() {
    const path = ".some_temp_file";
    const file = fs.openSync(path, "w+");

    // use file...
    if (someCondition()) {
        // do some more work...

        // Close the file and delete it.
        fs.closeSync(file);
        fs.unlinkSync(path);
        return;
    }

    // Close the file and delete it.
    fs.closeSync(file);
    fs.unlinkSync(path);
}

after

export function doSomeWork() {
    using file = new TempFile(".some_temp_file");

    // use file...

    if (someCondition()) {
        // do some more work...
        return;
    }
}

function loggy(id: string): Disposable {
    console.log(`Creating ${id}`);

    return {
        [Symbol.dispose]() {
            console.log(`Disposing ${id}`);
        }
    }
}

function func() {
    using a = loggy("a");
    using b = loggy("b");
    {
        using c = loggy("c");
        using d = loggy("d");
    }
    using e = loggy("e");
    return;

    // Unreachable.
    // Never created, never disposed.
    using f = loggy("f");
}

func();
// Creating a
// Creating b
// Creating c
// Creating d
// Disposing d
// Disposing c
// Creating e
// Disposing e
// Disposing b
// Disposing a

Error의 서브타입인 SuppressedError

using가 선언된 함수 바디에서 에러를 발생할 때 Error의 서브타입인 SuppressedError가 발생합니다.

class ErrorA extends Error {
    name = "ErrorA";
}
class ErrorB extends Error {
    name = "ErrorB";
}

function throwy(id: string) {
    return {
        [Symbol.dispose]() {
            throw new ErrorA(`Error from ${id}`);
        }
    };
}

function func() {
    using a = throwy("a");
    throw new ErrorB("oops!")
}

try {
    func();
}
catch (e: any) {
    console.log(e.name); // SuppressedError
    console.log(e.message); // An error was suppressed during disposal.

    console.log(e.error.name); // ErrorA
    console.log(e.error.message); // Error from a

    console.log(e.suppressed.name); // ErrorB
    console.log(e.suppressed.message); // oops!
}

suppressed 속성 값은 함수 func 내부에서 발생한 마지막 에러 값을 가지고 있으며 error 속성 값은 가장 최근에 발생한 에러 값을 가지고 있습니다.

Decorator Metadata

ECMAScript Stage 3의 Decorator Metadata를 지원합니다.

SomeClass[Symbol.metadata]를 통해 클래스에 메타데이터를 저장하거나 읽을 수 있습니다.

interface Context {
    name: string;
    metadata: Record;
}

function setMetadata(_target: any, context: Context) {
    context.metadata[context.name] = true;
}

class SomeClass {
    @setMetadata
    foo = 123;

    @setMetadata
    accessor bar = "hello!";

    @setMetadata
    baz() { }
}

const ourMetadata = SomeClass[Symbol.metadata];

console.log(JSON.stringify(ourMetadata));
// { "bar": true, "baz": true, "foo": true }

사용 예시를 보겠습니다.

serialize

import { serialize, jsonify } from "./serializer";

class Person {
    firstName: string;
    lastName: string;

    @serialize
    age: number

    @serialize
    get fullName() {
        return `${this.firstName} ${this.lastName}`;
    }

    toJSON() {
        return jsonify(this)
    }

    constructor(firstName: string, lastName: string, age: number) {
        // ...
    }
}

예시코드에서 age, fullName은 읽히기 전에 꼭 serialize가 되어야 한다는 정보를 @serialize 데코레이터를 통해 전달합니다. Person 클래스는 toJSON 메소드를 사용해 @serialize 데코레이터가 붙은 필드들을 serialize를 합니다.

serialize 함수를 선언한 코드를 보겠습니다.

const serializables = Symbol();

type Context =
    | ClassAccessorDecoratorContext
    | ClassGetterDecoratorContext
    | ClassFieldDecoratorContext
    ;

export function serialize(_target: any, context: Context): void {
    if (context.static || context.private) {
        throw new Error("Can only serialize public instance members.")
    }
    if (typeof context.name === "symbol") {
        throw new Error("Cannot serialize symbol-named properties.");
    }

    const propNames =
        (context.metadata[serializables] as string[] | undefined) ??= [];
    propNames.push(context.name);
}

export function jsonify(instance: object): string {
    const metadata = instance.constructor[Symbol.metadata];
    const propNames = metadata?.[serializables] as string[] | undefined;
    if (!propNames) {
        throw new Error("No members marked with @serialize.");
    }

    const pairStrings = propNames.map(key => {
        const strKey = JSON.stringify(key);
        const strValue = JSON.stringify((instance as any)[key]);
        return `${strKey}: ${strValue}`;
    });

    return `{ ${pairStrings.join(", ")} }`;
}

위 모듈은 serializables Symbol을 사용해 @serialize 데코레이터가 붙은

예시 코드 실행

const person = new Person("firstName", "lastName", 23)

console.log(person.age) // 23 
console.log(person.fullName) // "firstName lastName" 
console.log(person.lastName) // "lastName" 
console.log(person.toJSON()) // "{ "fullName": "firstName lastName", "age": 23 }" 

데코레이터를 사용한 Person 클래스의 인스턴스를 생성해 각 멤버 필드를 출력해봤습니다.
toJSON 메소드 호출을 통해 @serialize 데코레이터가 붙은 멤버 필드만 serialize된 것을 확인했습니다.

Named and Anonymous Tuple Elements

5.2 버전 미만에서의 아래 예시 코드는 에러를 발생시켰습니다.
이전 버전의 타입스크립트는 labeled, unlabeld를 혼용해서 사용할 수 없었습니다.

// ✅ fine - no labels
type Pair1 = [T, T];

// ✅ fine - all fully labeled
type Pair2 = [first: T, second: T];

// ❌ previously an error
type Pair3 = [first: T, T];
//                         ~
// Tuple members must all have names
// or all not have names.

5.1.6

5.2.2

그리고 5.2 버전 이전에는 labeld, unlabeled tuple을 spread oerator를 통해 합쳤을 때 label 정보가 사라졌습니다.

5.1.6

5.2.2

Easier Method Usage for Unions of Arrays

union type으로 선언된 배열은 아래와 같이 Array.prototype.* 메소드 사용 시 타입에러를 발생시켰는데 타입 에러가 발생하지 않게 변경되었습니다.

5.1.6

5.2.2
5.2 부터 filter의 콜백으로 들어가는 함수의 인자가 string |number 타입으로 변경되어 에러를 발생시키지 않습니다.

주의해야 하는 점

arraystring[] | number[] 타입이었으나 filter 메소드가 반환하는 filteredArray는
string[] | number[] 타입이 아닌 (string | number)[]타입이 되어 filteredArray를 사용하는 개발자는 타입 체킹을 해야 합니다.

Copying Array Methods

ES2022 에 추가된 아래 Array.prototype 이하 메소드들에 대한 타입 지원이 추가되었습니다.

Array.prototype.toReversed() -> Array
Array.prototype.toSorted(compareFn) -> Array
Array.prototype.toSpliced(start, deleteCount, ...items) -> Array
Array.prototype.with(index, value) -> Array

symbols as WeakMap and WeakSet Keys

symbol 타입이 WeakMapWeakSet의 키 값이 될 수 있게 변경되었습니다.
이에 매칭되는 javascript 기능은 ECMAScript stage3 단계로 등재되어있으며 다음 ES버전에 추가됩니다.

const weak = new WeakMap();

// Pun not intended: being a symbol makes it become a more symbolic key
const key = Symbol('my ref');
const someObject = { /* data data data */ };

weak.set(key, someObject);

5.3 버전에는 뭐가 달라질까?

TS 5.3은 내년에 공개될 예정입니다. 패키지 사이즈가 5.2대비 80%로 경량화될 예정입니다.

글을 마치며

오늘은 타입스크립트 5.2 정식버전에 담긴 주요 기능에 대해 알아봤습니다.

감사합니다!

profile
성장을 향한 작은 몸부림의 흔적들

0개의 댓글