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;
type Num = Flatten;

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

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

TypeScript, 오버로드 시그니쳐 vs 조건부 타입

다음과 같은 타입이 있다고 할때 …

interface IdLabel {
  id: number;
}

interface NameLabel {
  name: string;
}

숫자형 id 인자나 문자열 name 인자중 어떤 인자를 받느냐에 따라 구현이 달라지는 함수를 정의하기 위해 오버로드 시그니쳐 방식으로 다음처럼 함수를 정의할 수 있습니다.

function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
  throw "unimplemented";
}

코드가 중복적이고 장황합니다. 이를 조건부 타입 방식으로 완전히 동일하게 작동하는 함수를 정의하면 다음과 같습니다.

type NameOrId<T extends number | string> = T extends number
  ? IdLabel
  : NameLabel;

function createLabel2<T extends number | string>(idOrName: T): NameOrId<T> {
  throw "unimplemented";
}