TypeScript, 생성(구성) 시그니처(Construct Signature)

생성 시그니처는 클래스의 생성자에 대한 함수 타입을 정의합니다. 예제 코드를 보면 …

임의의 클래스에 대한 생성자의 생성 시그니처를 정의합니다.

interface PersonConstructor {
  new (name: string): Person;
}

상기 생성 시그니처는 Person 클래스의 객체를 생성하는 생성자 함수의 타입이며 생성자는 name이라는 문자열 타입을 받습니다. 이제 이 생성 시그니처를 통해 생성될 객체의 클래스를 정의합니다.

class Person {
    constructor(public name: string) {}
}

이제 앞서 정의한 생성 시그니처를 통해 Person 클래스의 객체를 생성하는 함수를 정의합니다.

function createPerson(
    ctor: PersonConstructor,
    name: string
): Person {
    return new ctor(name);
}

사용 코드는 다음과 같습니다.

const person = createPerson(Person, "Alice");

console.log(person.name); // Alice

생성 시그니처는 제니릭으로 많이 작성되는데.. 예는 다음과 같습니다.

class Person {
    constructor(public name: string) {}
}

class Student {
    constructor(public name: string) {}
}

function createInstance<T>(
    ctor: new (name: string) => T,
    name: string
): T {
    return new ctor(name);
}

const p = createInstance(Person, "Alice");
const s = createInstance(Student, "Bob");

console.log(p); // Person
console.log(s); // Student

TypeScript, 객체의 프로퍼티들을 상수화(정확히는 리터럴 타입화) 시키는 as const

const req = { url: "https://example.com", method: "GET" };
req.method = "XXX";

위의 코드는 유효합니다. req는 비록 상수 객체로 선언되어있지만 그 구성 프로퍼티들은 변경이 가능한 string 타입입니다. 하지만 이렇게 될 경우 문제가 있을 수 있습니다. 예를들어서 다음과 같은 함수가 있다고 해보겠습니다.

function requestURL(url: string, method: "GET" | "POST") {
  // ..
}

requestURL(req.url, req.method);

requestURL 함수의 두번째 인자는 유니언 타입입니다. 그러나 req.method는 string 타입이구요. 즉, req.method는 언제든 GET이나 POST가 아닌 다른 문자열 값이 될 수 있습니다.

해결 방법은 as const를 사용하는 것입니다.

const req = { url: "https://example.com", method: "GET" } as const;
// req.method = "XXX"; -> ERROR

as const에 의해 req의 프로퍼티들은 더 이상 string이 아닌 리터릴 타입이 됩니다. 앞서 봤던 requestURL 함수 호출도 더 이상 문제가 되지 않습니다.

TypeScript, Capitalize에 대하여

Capitalize의 사용 예시 코드로 다음이 있습니다.

type Getters<Type> = {
  [Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
};

interface Person {
  name: string;
  age: number;
  location: string;
}

type LazyPerson = Getters<Person>;

위의 코드는 LazyPerson에 대한 타입을 다음처럼 자동으로 구성해 줍니다.

type LazyPerson = {
  getName: () => string;
  getAge: () => number;
  getLocation: () => string;
}

위의 코드에서 설명할 내용이 산더미 같지만 일단 집중하고자 하는 것은 Capitalize 입니다. Capitalize는 TypeScript로 작성되지 않은 Native 코드입니다. Capitalize가 Native로 작성되었지만 TypeScript로도 작성될 수 있는데요. TypeScript로 작성된 Capitalize은 다음과 같습니다.

type UpperMap = {
  a: "A";
  b: "B";
  c: "C";
  d: "D";
  e: "E";
  f: "F";
  g: "G";
  h: "H";
  i: "I";
  j: "J";
  k: "K";
  l: "L";
  m: "M";
  n: "N";
  o: "O";
  p: "P";
  q: "Q";
  r: "R";
  s: "S";
  t: "T";
  u: "U";
  v: "V";
  w: "W";
  x: "X";
  y: "Y";
  z: "Z";
};

type MyCapitalize<S extends string> =
  S extends `${infer First}${infer Rest}`
  ? `${First extends keyof UpperMap ? UpperMap[First] : First}${Rest}`
  : S;

type A = MyCapitalize<"hello">;      // "Hello"
type B = MyCapitalize<"typescript">; // "Typescript"
type C = MyCapitalize<"안녕?">;     // "안녕?"
type D = MyCapitalize<"Hello">;      // "Hello"

TypeScript, 조건부 타입에 대한 분산 처리

아래의 코드는 주어진 타입에 대한 배열 타입을 만들어주는 코드입니다. 주어진 타입이 유니언일 경우 각 타입이 하나씩 분산 처리되어 유니온화 됩니다.

type ToArray<Type> = Type extends any ? Type[] : never;
type StrArrOrNumArr = ToArray<string | number>;
// type StrArrOrNumArr = string[] | number[]

만약 주어진 타입을 분산 처리하지 않고 한번에 처리하기를 원한다는 다음처럼 작성하면 됩니다.

type ToArray<Type> = [Type] extends [any] ? Type[] : never;
type StrArrOrNumArr = ToArray<string | number>;
// type StrArrOrNumArr = (string | number)[]

TypeScript, 배열 타입에서 조건부 타입과 추론

배열에서 조건부 타입과 추론에 대해 의미있는 코드가 있어 정리해 봅니다. 배열을 구성하는 항목의 타입을 얻기 위해 다음과 같은 코드가 가능합니다.

type Flatten<T> = T extends any[] ? T[number] : T;

type Str = Flatten<string[]>;
type Num = Flatten<number>;

T[number]가 배열을 구성하는 항목의 타입을 나타낸다는 점이 포인트입니다. 위의 코드는 조건부 타입에서 추론을 사용하면 다음처럼 동일하게 작성될 수 있습니다.

type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;