[Typescript] 조건부 타입

pds·2023년 8월 27일
0

Typescript Study

목록 보기
4/4

어느정도 사용은 해봤지만 명확하게 이해하고 사용하지 않았던 것 같고 익숙하지도 않다.

문서를 보며 조건부 타입에 대해 간단하게 정리해보고 이해 해보고 예시를 만들어보는 시간을 가졌다!


조건부 타입

주어진 제네릭 타입에 따라 타입을 결정 기능을 말함

T extends U ? X : Y

삼항연산자를 통해 T가 U에 할당될 수 있다면 X, 그렇지 않다면 Y가 된다는 뜻이다.

declare function func<T extends boolean>(x: T): T extends true ? string : number;

const x = func(Math.random() < 0.5); // string | number
Math.max(1, x); // error 
parseInt(x, 10); // error

const y = func(false); // number
Math.max(1, y); // ok
parseInt(y, 10); // error

const z = func(1 > 0); // string | number
Math.max(1, z); // error
parseInt(z, 10); // error

조건부 타입은 둘 중 하나의 타입으로 결정되거나 결정이 지연된다.

T가 항상 U에 할당될 수 있는지(위에서는 true) 충분히 정보를 가지고 있는지 여부로 결정하거나 지연한다.

interface Foo {
    propA: boolean;
    propB: boolean;
}

declare function f<T>(x: T): T extends Foo ? string : number;

function foo<U>(x: U) {
    // 'U extends Foo ? string : number' 타입을 가지고 있습니다
    const a = f(x);
    const b: string | number = a; // 이 할당은 허용됩니다!
    const c: string = a; // Type 'string | number' is not assignable to type 'string'.
    return a;
}
const res = foo(''); // number
const res2 = foo({propA: false}); // number
const res3 = foo({propA: false, propB: false}); // string

function bar(x: string) {
    const a = f(x); // number;
    const b: number = a;
}

foo 함수의 파라미터로 또다른 제네릭 타입 매개변수가 들어와 분기를 선택하지 못한 조건부 타입을 가지고 있어

조건부를 통해 얻을 수 있는 string | number 타입을 가지는 변수에는 할당이 가능하지만 하나의 조건에서 얻을 수 있는 타입으로는 할당이 불가능하다.


분산 조건부 타입

검사된 타입이 벗겨진 (naked) 타입 매개변수인 조건부 타입을 분산 조건부 타입이라고 합니다.
분산 조건부 타입은 인스턴스화 중에 자동으로 유니언 타입으로 분산됩니다

유니온으로 묶인 타입마다 조건부 타입 검사를 하고 그 결과값들을 묶어 다시 유니온으로 반환한다.

type A<T> = T extends number ? string : undefined;
type B = A<number | string>; // string | undefind
type C = A<number | 1>; // string;

B 타입을 예로 들면

number extends number ? string : undefined
string extends number ? string : undefined

와 같은 형태로 분리해서 타입을 검사하고 결과를 다시 유니온으로 반환한다.

number extends number ? string : undefined | string extends number ? string : undefined


근데 벗겨진(naked) 타입 매개변수가 뭘까?

스택오버플로우에서 알 수 있었다.

When they say naked here, they mean that the type parameter is present without being wrapped in another type, (i.e., an array, or a tuple, or a function, or a promise or any other generic type)

type NakedUsage<T> = T extends boolean ? "YES" : "NO"
type WrappedUsage<T> = [T] extends [boolean] ? "YES" : "NO"; // wrapped in a tuple

type Distributed = NakedUsage<number | boolean>; // "NO" | "YES"
type NotDistributed = WrappedUsage<number | boolean> // "NO"    
type NotDistributed2 = WrappedUsage<boolean > // "YES"

다른 타입이 wrapped 되지 않은 제네릭 T 같은 파라미터가 사용될 경우만 분산 조건부로 동작한다고 한다.


never로 필터링

분산 조건부 타입에서 never로 분기되어 분산되는 경우 해당 타입을 제거한다.

분산해서 타입을 검사하고 유니온하면서 never를 제거한다.

type T16<T> = T extends string ? T : never;
type T17 = T16<number | string | undefined>; // string;

type T18<T> = T extends string ? T : undefined;
type T19 = T18<number | string | undefined>; // string | undefined;

문서를 보니 세상에 이렇게도 사용할 수 있다.

type Diff<T, U> = T extends U ? never : T;  // U에 할당할 수 있는 타입을 T에서 제거
type Filter<T, U> = T extends U ? T : never;  // U에 할당할 수 없는 타입을 T에서 제거

type T30 = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "b" | "d"
type T31 = Filter<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "a" | "c"

Diff는 제외, Filter는 추출인데 이런 방식을 잘 숙지해두면 굉장히 유용할 것 같다.


Infer

조건부 타입의 extends 절 안에서, 이제 추론 될 타입 변수를 도입하는 infer 선언을 가지는 것이 가능합니다. 이렇게 추론된 타입 변수는 조건부 타입의 실제 분기에서 참조될 수 있습니다.

T extends infer U ? X : Y

infer 키워드는 런타임 시에 타입스크립트가 타입을 추론할 수 있게 해주고 이를 할당해준다.

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

function hello() {
    return {
        hello: 'world'
    };
}
type HelloType = ReturnType<typeof hello>; // { hello: string; }
const hell: ReturnType<typeof hello> = { hello: 'world' };

위의 코드에서 ReturnType은 함수의 반환 타입을 추출하는 유틸리티 타입이고 infer R 부분은 T가 함수 타입인 경우 해당 함수의 리턴 타입을 R로 추론하라는 의미이다.

이를 통해 함수의 리턴 타입을 추론하여 런타임에서 결정되는 타입을 얻어 사용할 수 있다.

type PromiseType<T> = T extends (...args: any) => Promise<infer U> ? U : never;
type GetMeReturnType = PromiseType<typeof userApi.getMe>;
const getMePromiseResponse: PromiseType<typeof userApi.getMe> = {
    name: '',
    email: '',
    // ...
}

이런 방식으로 Promise 객체 내부 값의 타입을 가져와 사용할 수도 있다.
리턴타입 뿐 아니라 (...args: any) 부분을 infer로 추론해 조건부 참일 때 얻게끔 해준다면 매개변수 타입도 얻을 수 있다.


사전에 정의된 조건부 타입

TypeScript 2.8에서 lib.d.ts에 미리 정의된 조건부 타입을 추가했습니다.

Exclude<T, U> -- U에 할당할 수 있는 타입은 T에서 제외.
Extract<T, U> -- U에 할당할 수 있는 타입을 T에서 추출
NonNullable<T> -- T에서 null과 undefined를 제외.
ReturnType<T> -- 함수 타입의 반환 타입을 얻기.
InstanceType<T> -- 생성자 함수 타입의 인스턴스 타입을 얻기.
type ExcludedTypeNotContain = Exclude<'a' | 'b', 'z'>;  // a | b
type ExcludedType = Exclude<'a' | 'b', 'a' | 'c' | 'z'>;  // b

type ExtractedNotContain = Extract<string | number | (() => void), typeof hello>;  // never
type ExtractedType = Extract<string | number | typeof hello, typeof hello>;  // hello

type NonNullableType = NonNullable<string | number | undefined | null | '' | 0 | never>; // string | number

type ReturnClassType = ReturnType<typeof AClass>; // never;
type ReturntFuncType = ReturnType<typeof hello>; // { hello: string };

type InstanceClassType = InstanceType<typeof AClass>; // AClass
type InstanceFuncType = InstanceType<typeof hello>; // error

여기서는 ReturnType만 알고 있었는데 굉장히 유용한 유틸리티 타입이 많은 것 같다.


References

profile
강해지고 싶은 주니어 프론트엔드 개발자

0개의 댓글