TypeScript, 인자값에 대한 문자열 형식 강제하기

예를들어 다음과 같은 예제코드가 있습니다.

setPadding('10px'); // ✅
setPadding('2.5em'); // ✅
// setPadding('10');   // ❌ 에러: 'px', 'em', '%' 단위가 빠졌습니다.
// setPadding('tenpx'); // ❌ 에러: 숫자가 아닙니다.

setPadding이라는 함수는 길이에 대한 값을 받는데 숫자값과 단위가 조합된 하나의 문자열 값입니다. 이를 위해 setPadding 함수는 다음처럼 정의될 수 있습니다.

type CSSSize = `${number}px` | `${number}em` | `${number}%`;

function setPadding(padding: CSSSize) {
  console.log(padding);
  return { padding };
}

TypeScript, 함수 오버로딩을 스마트하게 대체하는 “매개변수 동적 추론”

하나이 함수에 대해서 첫번째로 입력받는 인자의 값에 따라 이후 인자의 타입을 결정할 수 있는 방법입니다. 함수 오버로딩이 아닌 TypeScript 본연의 타입 기능을 이용하는 방법으로 함수 오버로딩보다 더욱 작성하기 쉽고 똑똑합니다. 예를들어 emit라는 함수를 다음처럼 정의합니다.

type EventMap = {
  click: { x: number; y: number };
  hover: { elementId: string };
  keyPress: { key: string; code: number };
};

// Key를 제네릭으로 받아 EventMap[Key]를 Payload 타입으로 자동 추론합니다.
function emit(eventName: K, payload: EventMap[K]) {
  console.log(`Event: ${eventName}`, payload);
}

emit의 첫번째 값에 따라 두번째 인자의 타입이 다르게 결정됩니다. 해당 타입들은 위의 EventMap 타입에 정의되어 있습니다. 이제 다음처럼 emit 함수를 이용할 수 있습니다.

emit('click', { x: 10, y: 20 });        // ✅ 정상 작동
emit('click', { elementId: 'btn' });    // ❌ 타입 에러! click은 x, y가 필수입니다.
emit('hover', { elementId: 'header' }); // ✅ 정상 작동 (elementId 오타 시 감지)

그러나 emit 함수 구현부에서 첫번째 인자인 eventName의 값에 따라 payload의 타입을 결정해서 사용할 수 있어야 합니다. 예를들어 eventName이 ‘click’이라면 payload.x가 가능해야 합니다. 하지만 위의 방법으로는 안됩니다. 다른 방법은 다음과 같으며 TypeScript 코드로써 보다 더 쉽고 직관적입니다.

type EventMap =
  | {
    eventName: "click";
    payload: { x: number; y: number };
  }
  | {
    eventName: "hover";
    payload: { elementId: string };
  }
  | {
    eventName: "keyPress";
    payload: { key: string; code: number };
  };

function emit(event: EventMap) {
  if (event.eventName === "click") {
    event.payload.x; // ✅
    event.payload.y;
  }

  if (event.eventName === "hover") {
    event.payload.elementId; // ✅
  }
}

emit({ eventName: 'click', payload: { x: 10, y: 20 } });

결국 함수 오버로딩을 스마트하게 대체하는 “매개변수 동적 추론” 방식은 크게 의미가 없습니다. 🙂

TypeScript, 유니온 타입에서 타입 융합을 방지하는 방법

타입 융합이란, 예를들어 'red' | string 과 같은 타입의 경우 'red'string에 포함되는 타입이므로 결국 string 하나로 융합되는 것을 의미합니다. 이런 융합을 막기 위한 방법은 string 대신 string & {}을 사용하는 것입니다. 관련된 예제 코드는 다음과 같습니다.

// string & {} 를 교차(Intersection)시키면 타입 융합(Type Flattening)을 방지합니다.
type Color = 'red' | 'blue' | 'green' | (string & {});

function setColor(color: Color) {
  console.log(`Setting color to ${color}`);
}

setColor("red")
// 💡 효과:
// 1. setColor('') 입력 시 'red', 'blue', 'green' 팝업 자동완성이 뜹니다.
// 2. setColor('#123456') 처럼 임의의 문자열을 넣어도 타입 에러가 발생하지 않습니다.

TypeScript, 재귀 호출을 통한 타입 정의

타입스크립트는 타입 정의에서 재귀 호출이 가능합니다. 재귀 호출이 적용된 아래의 코드가 그 예입니다.

type Flatten<T> =
    T extends (infer U)[]
        ? Flatten<U>
        : T;

Flatten 타입을 정의하고 있는데, 재귀적으로 다시 Flatten을 호출하고 있습니다. 위의 Flatten 타입의 사용 예는 다음과 같습니다.

type A = Flatten<number[][][][]>; // number

위 코드 예시의 Flatten은 재귀적으로 다음처럼 수행하여 최종적인 number 타입을 정의하고 있습니다.

number[][][][] → number[][][] → number[][] → number[] → number

TypeScript, 문자열 패턴 매칭

다음과 같은 문자열 리터럴 타입이 있습니다.

type url = "user/profile";

위의 타입에서 "/" 문자 다음의 문자열인 "person"을 문자열 패턴 매칭을 통해 얻는 코드는 다음과 같습니다.

type Last<T> =
    T extends `${string}/${infer U}`
        ? U
        : never;

type A = Last<url>; // "profile"