타입스크립트를 사용할 때 타입을 조금 더 편하게 사용할 수 있는 팁들을 준비해보았습니다.

Roll 타입으로 예쁘게 펼쳐보기

type Roll<T> = {
  [K in keyof T]: T[K];
};
// & {};

Matt Pocock이 고안한 Roll이라는 타입은 네이버의 infer, never만 보면 두려워지는 당신을 위한 타입 추론에서 알게된 핵입니다. 다음과 같이 복잡한 타입을 사용하면 개발자는 해당 객체에 들어있는 타입이 정확히 무엇인지 확인하기 힘든데요. Roll을 이용하면 객체를 해부하여 볼 수 있게 됩니다.

type A = {
    a: string;
    b: number;
    c: boolean;
}

type B = {
    d: boolean;
    e: number;
    f: string[];
}

type C = A & B; // type C = A & B

type RolledC = Roll<A & B>
// type RolledC = {
//    a: string;
//    b: number;
// }

타입스크립트 플레이그라운드에서 보기

함수의 인자에 unkown과 never을 올바르게 사용하기

공변성과 반공변성을 가지는 지에 따라 unkown과 never을 올바르게 작성하여야 합니다. 함수의 반환값과 변수의 타입은 uknown타이베 다른 모든 타입을 사용가능하지만 함수의 인자는 반공변성을 가지기 때문에 never타입에 모든 타입을 사용가능합니다. 자세한 내용은 타입스크립트 심층 분석 4를 참고하세요.

type A = (a: any) => any;
type U = (u: unknown) => unknown;
type N = (n: never) => never;

const a: A = (a: string) => ""; // 인자와 반환값이 (never 제외)어떤 타입이라도 대입이 된다.
const u: U = (u: string) => ""; // Type 'unknown' is not assignable to type 'string'. 인자에서 문제가 발생한다.
const n: N = (n: string) => ""; // Type 'string' is not assignable to type 'never'. 반환값에서 문제가 발생한다.

타입가드 함수

function isNonNullable<T>(value: T): value is NonNullable<T> {
    return value !== null && value !== undefined;
}

타입가드 함수는 특정 타입을 좁히는 역할을 하는 함수입니다. 타입스크립트에서는 특정 조건을 만족할 때 그 값을 명시적으로 추론할 수 있도록 돕는 함수들이 타입가드 함수입니다. 타입가드 함수는 주로 is 키워드를 사용하는 형태로 작성되어 타입스크립트에게 함수가 특정 타입으로 좁혀진다고 알릴 수 있습니다. 조건문에서 해당 함수가 true를 반환하면 해당 조건 내부에서 인자로 들어간 값은 is 키워드로 명시된 타입이 되고 그렇지 않다면 is 키워드로 명시된 타입이 아닌 다른 타입이라고 알릴 수 있습니다.

유니언 타입을 인터섹션 타입으로 전환하기

type UnionToIntersection<T> = (
    T extends any
    ? (_: T) => void: never) extends (_: infer S) => void
    ? S
    : never

T extends any ? ... : ...에서, T가 유니온 타입이라면 조건부 타입이 각 멤버에 대해 분산되어 각각의 멤버 타입에 대해 함수 타입을 생성합니다. 예를 들어, T{a: number}|{b:string}이라면 (_: {a: number}) => void | (_: {b: string}) => void;가 됩니다. 이때 (_: T) => voidextends (_: infer S) => void에 의해 평가되면서 infer S{a: number}가 되거나 {b: string}이 됩니다.

 

그런데 infer와 조건부 타입을 사용할 때, infer가 반공변성을 가지는 자리(함수의 인자 자리)에서 사용되면 infer S는 해당 값들이 모두 교차(인터섹션) 되는 타입을 추론하게 됩니다. 왜냐하면 해당 함수타입이 들어가는 자리에는 모든 인자를 가진 함수가 필요하기 때문입니다. 그래서 만약 infer로 받게 되는 타입들이 결합될 수 없으면 추론을 실패하게 되어 never가 됩니다. 따라서 extends (_: infer S) => void를 평가하는 과정에서 두 파라미터는 결합되어 최종적으로 인터섹션 타입으로 바뀌게 됩니다.

 

슬픈 이야기는 인터섹션 타입을 유니온 타입으로 만드는 건 불가능하다는 것입니다.

type A = {a: number};
type B = {b: string, c: number};

type G = UnionToIntersection<A | B>; // type G = A & B

타입스크립트 플레이그라운드에서 보기

함수 인자로 들어오는 내용에 따라 올바른 타입 매칭하기

function changeTypeBiaType<T extends Types["type"]>(
  type: T,
  value: Extract<Types, { type: T }>['value'],
): typeof value {
  // 실제 로직 추가
  return value;
}

위의 함수는 첫 번째 인자인 type에 따라 올바른 타입을 매칭시켜주는 함수입니다. type을 받은 뒤, 들어올 수 있는 타입 중 해당 type과 같은 값을 가진 타입을 가지고와 value와 매칭합니다. 다음과 같이 사용할 수 있습니다.

// 올바른 예시
const resultA = changeTypeBiaType("A", "Hello"); // string
const resultB = changeTypeBiaType("B", ["42", "world"]); // string[]
const resultC = changeTypeBiaType("C", 1); // number
const resultD = changeTypeBiaType("D", [1, 2, 3]); // number[]

// 잘못된 예시 - 오류 발생
const resultInvalidA = changeTypeBiaType("A", ["42", "world"]); // 오류: "A"는 string이어야 함
const resultInvalidB = changeTypeBiaType("B", "Hello"); // 오류: "B"는 string[]이어야 함
const resultInvalidC = changeTypeBiaType("W", "d") // 오류 type은 "A" | "B" | "C" | "D"이어야 함

타입스크립트 플레이그라운드에서 보기

객체 조작하기

readonly 속성 제거하기

type Mutable<T> = {
    -readonly [P in keyof T]: T[P];
};

-readonly는 읽기 전용(readonly) 속성을 제거하는 데 사용됩니다.

type ReadonlyA = {
    readonly a: number;
    readonly b: string;
}

type A = Mutable<ReadonlyA>;

타입스크립트 플레이그라운드에서 보기

반대로 객체를 읽기 전용으로 만들려면 내장 제네릭인 Readonly<T>를 사용할 수 있습니다.

객체의 모든 메서드 추출하기

type Method<T> = {
    [K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];

객체의 모든 키를 순회하면서 값이 Funtion타입을 가지는 경우를 모운 뒤 다시 키를 이용해 순회하면서 메서드의 타입을 가져올 수 있습니다.

type Person = {
  name: string;
  age: number;
  greet: () => void;
  sayGoodbye: (message: string) => void;
};

type FunctionKeys = Method<Person>;

타입스크립트 플레이그라운드에서 보기

중첩된 객체의 모든 값을 optional로 만들기

type DeepPartial<T> = {
    [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

extends object를 통해 특정 값이 객체면 재귀적으로 순회할 수 있습니다.

객체에서 특정 키들만 필수로 만들기

type RequiredPick<T, K extends keyof T> = Required<Pick<T, K>> & Omit<T, K>;

참고자료

NAVER ENGINEERING DAY 2024, infer, never만 보면 두려워지는 당신을 위한 타입 추론 - 응용 문제
Matt Pocock, Roll
typescript-challenges, issuecomment-1817784406
stackoverflow, Transform union type to intersection type

+ Recent posts