TypeScript로 클래스가 아닌 것 상속하기
기존 클래스 방식
원래 JavaScript에는 클래스가 없었고, 생성자 함수로 클래스를 구현했었죠.
현재의 class
문법도 사실은 약간의 차이만 있을 뿐, 옛날 방식의 설탕 문법(syntax sugar)입니다.
TypeScript에서도 일부 타입을 조금 속이면 이 방법으로 클래스를 만들 수 있습니다.
const Foo: {
new (greeting?: string): Foo;
} = function (this: Foo, greeting?: string) {
this.greeting = greeting || "Hello";
} as any;
Foo.prototype.greet = function () {
console.log(`${this.greeting}, world!`);
};
interface Foo {
greeting: string;
greet(): void;
}
new Foo().greet(); // => Hello, world!
이 경우, TypeScript는 Foo
가 클래스인지 아닌지 모르고, 단순히 "new 시그니처를 가진 객체"로 인식합니다.
기존 클래스의 상속
JavaScript에서는 이런 기존 방식의 클래스를 class
문법으로 상속할 수 있습니다.
TypeScript도 여기에 타입을 적용할 수 있습니다.
class Bar extends Foo {
constructor() {
// 여기도 타입이 적용됨
super("Good evening");
}
}
// 이 호출에도 타입이 적용됨
new Bar().greet(); // => Good evening, world!
생성자 시그니처의 반환 타입 제약
생성자 시그니처의 반환 타입 자체에는 특별한 제약이 없지만, 이상한 타입의 클래스를 상속하려고 하면 문제가 발생합니다. (TypeScript 4.1.2 기준)
interface User {
type: "user";
userId: number;
}
interface Admin {
type: "admin";
adminId: number;
}
type Entity = User | Admin;
const Entity: new () => Entity = function (): Entity {
return { type: "user", userId: 1 };
} as any;
// 에러: 기본 생성자의 반환 타입 'Entity'는 객체 타입이거나
// 정적으로 알려진 멤버를 가진 객체 타입의 교차점이어야 합니다. (2509)
class ExtendedEntity extends Entity {}
즉, 상속을 시도할 때 생성자 시그니처의 반환 타입이 "interface와 유사한 타입"이어야 한다는 것입니다.
또한, 생성자 시그니처가 오버로드된 경우, 모든 타입이 동일해야 합니다.
const WeirdClass: {
new (isAdmin: false): { type: "user" };
new (isAdmin: true): { type: "admin" };
} = function (isAdmin: boolean): { type: "user" } | { type: "admin" } {
if (isAdmin) {
return { type: "user" };
} else {
return { type: "admin" };
}
} as any;
// 에러: 기본 생성자는 모두 동일한 반환 타입을 가져야 합니다. (2510)
class SubClass extends WeirdClass {}
다형성 생성자
생성자 시그니처도 호출 시그니처처럼 다형성으로 만들 수 있습니다.
다형성 생성자 함수를 그대로 상속하려고 하면 타입 에러가 발생합니다.
const IdClass: {
new<T>(value: T): T;
} = function<T>(value: T): T {
return value;
} as any;
// 에러: 지정된 수의 타입 인수를 가진 기본 생성자가 없습니다. (2508)
class ExtendedUser extends IdClass {}
이런 경우, extends
바로 뒤의 식에 제네릭 인수를 추가하여 타입을 확정해야 합니다.
문법적으로는 일반적인 상속, 예를 들어 class MyComponent extends React.Component<Props>
와 동일합니다.
const IdClass: {
new<T>(value: T): T;
} = function<T>(value: T): T {
return value;
} as any;
class ExtendedUser extends IdClass<{ type: "user" }> {}
제네릭 인수의 개수가 다른 생성자 시그니처는 공존 가능하며, extends
에 지정한 개수와 일치하는 생성자 시그니처가 선택됩니다.
const WeirdlyOverloadedClass: {
new (): { greeting: string };
new <T>(value: T): T;
} = function<T>(value?: T): T | { greeting: string } {
if (value) return value;
return { greeting: "Hello" };
} as any;
class ExtendedGreeting extends WeirdlyOverloadedClass {}
class ExtendedUser extends WeirdlyOverloadedClass<{ type: "user" }> {}
Record
가 Map<string, any>
의 서브클래스라는 것은 Record
의 본질적인 기능인 프로퍼티 접근자의 타입이 전혀 포함되지 않는다는 것을 의미합니다.
이는 타입을 적용하는 의미가 반감되므로, Record
에 더 적절한 타입을 적용하는 방법을 모색하게 되었습니다.
자세한 내용은 생략하지만, Record
는 클래스를 생성하는 함수이고, Record(defaultValue)
를 상속함으로써 자체 클래스를 정의하는 방식입니다.
또한, 동적으로 프로퍼티 접근자가 생성되므로, 일반적인 class
선언으로 Record
의 타입을 적용하기 어렵고, 생성자 시그니처를 직접 작성해야 했습니다.
'Javascript' 카테고리의 다른 글
TypeScript의 불편함을 해결하는 @total-typescript/ts-reset 도입하기 (1) | 2024.11.09 |
---|---|
reduce로 날짜별 정보 배열 만들기 (0) | 2024.11.09 |
실무에서 바로 쓸 수 있는 TypeScript 타입 8선 (1) | 2024.11.09 |
자바스크립트 얕은 복사 vs 깊은 복사, 완벽 정리! (0) | 2024.10.30 |
Next.js 15, 달라진 점 싹 다 훑어보기! (1) | 2024.10.30 |