TypeScript 4.3 업데이트 훑어보기

mirrous
10 min readJun 7, 2021

--

작년에 TypeScript 4.0을 리뷰하는 포스트를 작성했습니다. 그 이후 1년이 지난 지금 TypeScript 4.3이 출시되었습니다. 메이저 버전이 업데이트된 건 아니지만, 어떤 업데이트들이 있었는지 하나씩 살펴보도록 하겠습니다.

클래스 속성의 쓰기 전용 속성 분리

자바스크립트에서는 일반적으로 서버에 값들을 전달하기 전, API가 이해할 수 있도록 값을 변환합니다. 이런 현상은 getter와 setter에서도 동일하게 발생합니다. 예를 들어, number 타입의 값을 특정 private field에 저장하기 위해 setter를 거쳐 변환된다고 생각해봅시다.

class Thing {
#size = 0;
get size() {
return this.#size;
}
set size(value) {
let num = Number(value);
// NaN이나 Infinite와 같은 유효하지 않은 숫자는 허용하지 않습니다.
if (!Number.isFinite(num)) {
this.#size = 0;
return;
}
this.#size = num;
}
}

위와 같은 자바스크립트 코드를 타입스크립트로 어떻게 바꿀 수 있을까요? 사실 우리가 특별히 기술적으로 더 작업할 필요가 없습니다. 타입스크립트에서는 알아서 숫자임을 파악하고 있기 때문입니다.

다만, setter의 value가 number 뿐만 아니라 다른 타입들을 더 받게 된다면 타입 추론에 문제가 생기게 됩니다. 이 문제를 임시로 해결하기 위해 getter에서 unknown이나 any 타입을 붙일 수 있습니다.

class Thing {
// ...
get size(): unknown {
return this.#size;
}
}

하지만 unknown와 any 타입은 타입 추론에 도움이 되지 않고 실수를 바로잡아 주지 않습니다. 그래서 값을 변환하는 API를 모델링하는 경우에는 정확성(값을 읽기 쉽게 만들어주지만 작성하기 어렵다)이나 융통성(작성하기는 쉽지만 추론하기 힘들다) 중 하나를 선택해야 했습니다.

하지만 이번에 업데이트되는 타입스크립트 4.3에서는 값을 읽거나 쓰는데에 특정 타입을 따로 정해줄 수 있게 되었습니다.

class Thing {
#size = 0;
get size(): number {
return this.#size;
}
set size(value: string | number | boolean) {
let num = Number(value);
// NaN이나 Infinite와 같은 유효하지 않은 숫자는 허용하지 않습니다.
if (!Number.isFinite(num)) {
this.#size = 0;
return;
}
this.#size = num;
}
}

다음 예시를 통해 값을 넣을 때는 문자열, 불리언, 숫자 타입을 사용했지만 값을 가져올 때는 항상 숫자로만 가져올 수 있음을 에러 없이 보장받습니다.

let thing = new Thing();thing.size = "hello";
thing.size = true;
thing.size = 42;
let mySize: number = thing.size;

이 패턴은 클래스에서만 한정되지 않고 객체 리터럴에서도 분리된 타입의 getter와 setter를 사용할 수 있습니다.

function makeThing(): Thing {
let size = 0;
return {
get size(): number {
return size;
},
set size(value: string | number | boolean) {
let num = Number(value);
if (!Number.isFinite(num)) {
size = 0;
return;
}
size = num;
}
}
}

메소드 재정의와 --noImplicitOverride 플래그

자바스크립트에서 클래스를 상속할 때는 언어 자체에서 매우 쉽게 메소드를 재정의할 수 있게 만들어주었습니다. 다만 그 과정에서 몇 가지 실수를 만들 수 있습니다.

하나의 큰 실수는 이름이 변경된 것을 놓쳤을 때입니다.

class SomeComponent {
show() {
// ...
}
hide() {
// ...
}
}

class SpecializedComponent extends SomeComponent {
show() {
// ...
}
hide() {
// ...
}
}

SpecializedComponentSomeComponent 의 자식 클래스입니다. 그리고 showhide 를 재정의했습니다. 여기서showhide 를 다른 함수로 바꾸면 어떻게 될까요?

class SomeComponent {
setVisible(value: boolean) {
// ...
}
}
class SpecializedComponent extends SomeComponent {
show() {
// ...
}
hide() {
// ...
}
}

이런! 우리의SpecializedComponent 는 업데이트되지 않았네요! 이제 부모 클래스에서 showhide 가 없기 때문에 호출되지 않는 함수가 될 수 있습니다.

여기서 이슈는 개발자가 새 메소드를 추가하려는 것인지 재정의를 하려는건지 명확히 알 수 없다는 것입니다. 이것이 타입스크립트 4.3에서 override 키워드가 추가되는 이유입니다.

class SpecializedComponent extends SomeComponent {
override show() {
// ...
}
override hide() {
// ...
}
}

메소드 앞에 override 키워드를 추가하면 타입스크립트는 같은 이름이 부모 클래스에 존재하는지의 여부를 항상 추적합니다.

class SomeComponent {
setVisible(value: boolean) {
// ...
}
}
class SpecializedComponent extends SomeComponent {
override show() {
// ~~~~~~~~
// `override`가 붙은 show가 부모 클래스에 없기 때문에 에러가 발생합니다.
// ...
}

// ...
}

이것은 큰 발전이지만 당신이 override를 함수 앞에 붙이는 것을 잊을 수 있습니다. 이런 문제를 해결하기 위해 --noImplicitOverride 플래그를 제공합니다. 이 옵션을 킨다면, 부모 클래스와 자식 클래스에 같은 이름의 함수가 있지만 override 키워드가 없는 경우 에러를 던집니다.

템플릿 문자열 타입 개선

최근 버전(TypeScript 4.1)에서 타입스크립트는 새로운 템플릿 문자열 타입을 소개했었습니다. 이 타입은 다른 타입들을 연결하여 새로운 문자열 유형을 생성하는 타입입니다.

type Color = "red" | "blue";
type Quantity = "one" | "two";

type SeussFish = `${Quantity | Color} fish`;
// same as
// type SeussFish = "one fish" | "two fish"
// | "red fish" | "blue fish";

다른 문자열 타입과 검사도 가능합니다.

declare let s1: `${number}-${number}-${number}`;
declare let s2: `1-2-3`;

// Works!
s1 = s2;

템플릿 문자열 타입에 대한 첫 번째 개선으로 템플릿 문자열이 비슷한 유형에 의해 문맥적으로 입력될 때, 표현식에 템플릿 유형을 제공합니다.

function bar(s: string): `hello ${string}` {
// 이전엔 에러였으나 지금은 동작합니다!
return `hello ${s}`;
}

타입을 유추할 때와 string을 확장하는 제너럴 타입에서도 동일하게 동작합니다.

declare let s: string;
declare function f<T extends string>(x: T): T;

// 이전: string
// 현재: `hello-${string}`
let x2 = f(`hello ${s}`);

다음 변화로는 비슷한 서로 다른 템플릿 문자열 타입들을 더 잘 추론할 수 있게 되었습니다.

declare let s1: `${number}-${number}-${number}`;
declare let s2: `1-2-3`;
declare let s3: `${number}-2-3`;

s1 = s2;
s1 = s3; // 기존 버전에서는 에러

기존 버전에서 s1 = s2 는 잘 동작했지만, 비슷한 타입으로 보임에도 불구하고 s1 = s3 는 동작하지 않았습니다.

이번 버전에서는 이런 이슈를 해결하여 템플릿 문자열 타입의 각 부분이 일치하는지 증명하는 작업을 더 잘 수행하게 되었습니다.

declare let s1: `${number}-${number}-${number}`;
declare let s2: `1-2-3`;
declare let s3: `${number}-2-3`;
declare let s4: `1-${number}-3`;
declare let s5: `1-2-${number}`;
declare let s6: `${number}-2-${number}`;

// 모두 잘 동작합니다!
s1 = s2;
s1 = s3;
s1 = s4;
s1 = s5;
s1 = s6;

ECMAScript #private Class Elements

타입스크립트 4.3에서는 #가 붙은 private 식별자가 포함된 이름들을 런타임 환경에서 정말 숨겨진 것으로 동작하게 만들어줍니다. 속성 외에도 메소드와 접근자에 private 식별자를 추가할 수 있습니다.

class Foo {
#someMethod() {
//...
}

get #someValue() {
return 100;
}

publicMethod() {
// These work.
// We can access private-named members inside this class.
this.#someMethod();
return this.#someValue;
}
}

new Foo().#someMethod();
// ~~~~~~~~~~~
// error!
// Property '#someMethod' is not accessible
// outside class 'Foo' because it has a private identifier.

new Foo().#someValue;
// ~~~~~~~~~~
// error!
// Property '#someValue' is not accessible
// outside class 'Foo' because it has a private identifier.

뿐만 아니라 static이 붙은 변수나 함수에도 private 접근자를 추가할 수 있습니다.

class Foo {
static #someMethod() {
// ...
}
}

Foo.#someMethod();
// ~~~~~~~~~~~
// error!
// Property '#someMethod' is not accessible
// outside class 'Foo' because it has a private identifier.

추상 클래스에서도 ConstructorParameters 타입이 동작

타입스크립트 4.3에서는 클래스의 생성자를 추론하는 타입인ConstructorParameters 타입이 추상 클래스에서도 작동할 수 있게 되었습니다.

abstract class C {
constructor(a: string, b: number) {
// ...
}
}

// CParams는 '[a: string, b: number]'을 가지게 됩니다.
type CParams = ConstructorParameters<typeof C>;

--

--

No responses yet