유튜브 영상으로 보기: https://www.youtube.com/watch?v=4ey1bjAiQ1U
안녕하세요, 단테입니다. 24일 타입스크립트 5.2 RC 버전이 출시되었습니다.
변경점에 대해 말씀드리고 가장 중요하게 생각되는 업데이트에 대해 요약해보겠습니다.
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
지원이 추가되었습니다.
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'
간단히 이야기하면 .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로 변경된 상태이기에 에러가 발생하게 됩니다.
import * as b from "./b.ts"
작년 12월 14에 allowImportingTsExtensions
컴파일러 옵션이 추가되었고 이 옵션을 사용하기 위해서는 noEmit
flag 또한 true로 설정되어야 했습니다. 그래야 b.ts 가 tsc에 의해 b.js로 변경되지 않기 때문입니다.
5.2 부터 allowImportingTsExtensions: true
설정 없이도 파일 확장자로부터 타입 정보를 import 할 수 있게 되었습니다.
5.1.3 버전에서 타입 임포트가 에러를 발생시키는 모습
5.2 버전 부터는 타입 임포트가 에러를 발생시키지 않습니다.
ECMAScript stage 3 proposal eksrPdls Explicit Resource Management
에 using
키워드가 소개되었는데요 stage 4에 있지 않는 최신 문법을 사용하려면 babel, polyfill을 사용하던가 typescript 컴파일러를 사용해야 합니다. TSC에서 지원하는 ECMAScript 범위가 늘어났다고 이해하면 됩니다.
https://velog.io/@jay/Typescript-5.2-using
자칫 잊어버리거나 코드를 복잡하게 만들 수 있는 리소스 해제를 간편하게 해주는 문법입니다.
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);
}
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
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
속성 값은 가장 최근에 발생한 에러 값을 가지고 있습니다.
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 }
사용 예시를 보겠습니다.
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된 것을 확인했습니다.
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
union type으로 선언된 배열은 아래와 같이 Array.prototype.* 메소드 사용 시 타입에러를 발생시켰는데 타입 에러가 발생하지 않게 변경되었습니다.
5.1.6
5.2.2
5.2 부터 filter의 콜백으로 들어가는 함수의 인자가string |number
타입으로 변경되어 에러를 발생시키지 않습니다.
array
는 string[] | number[]
타입이었으나 filter 메소드가 반환하는 filteredArray는
string[] | number[]
타입이 아닌 (string | number)[]
타입이 되어 filteredArray를 사용하는 개발자는 타입 체킹을 해야 합니다.
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
symbol 타입이 WeakMap
과 WeakSet
의 키 값이 될 수 있게 변경되었습니다.
이에 매칭되는 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);
TS 5.3은 내년에 공개될 예정입니다. 패키지 사이즈가 5.2대비 80%로 경량화될 예정입니다.
오늘은 타입스크립트 5.2 정식버전에 담긴 주요 기능에 대해 알아봤습니다.
감사합니다!