클래스 속성의 쓰기 전용 속성 분리
자바스크립트에서는 일반적으로 서버에 값들을 전달하기 전, 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() {
// ...
}
}
SpecializedComponent
는 SomeComponent
의 자식 클래스입니다. 그리고 show
와 hide
를 재정의했습니다. 여기서show
와 hide
를 다른 함수로 바꾸면 어떻게 될까요?
class SomeComponent {
setVisible(value: boolean) {
// ...
}
}
class SpecializedComponent extends SomeComponent {
show() {
// ...
}
hide() {
// ...
}
}
이런! 우리의SpecializedComponent
는 업데이트되지 않았네요! 이제 부모 클래스에서 show
와 hide
가 없기 때문에 호출되지 않는 함수가 될 수 있습니다.
여기서 이슈는 개발자가 새 메소드를 추가하려는 것인지 재정의를 하려는건지 명확히 알 수 없다는 것입니다. 이것이 타입스크립트 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>;