반응형
250x250
Notice
Recent Posts
Recent Comments
Link
관리 메뉴

짧은코딩

infer로 타입 추론(with 컨디셔널 타입) 본문

TS/TS(with ZeroCho)

infer로 타입 추론(with 컨디셔널 타입)

5_hyun 2023. 11. 11. 00:26

infer란?

infer로 타입 추론을 극한으로 활용할 수 있다.

컨디셔널 타입과 같이 사용할 수 있다.

컨디셔널 타입(Conditional Type)이란?

type A1 = string;
type B1 = A1 extends string ? number : boolean; // number

type A2 = number;
type B2 = A2 extends string ? number : boolean; // boolean
특정 타입 extends 다른 타입 ? 참일 때 타입 : 거짓일 때 타입

특정 타입이 다른 타입의 부분집합일 때 참이 된다.

infer의 예시들

배열의 요소 타입을 얻고 싶은 경우

type El<T> = T extends (infer E) [] ? E : never;
type Str = El<string[]>; // string
type NumOrBool = El<(number | boolean)[]>; // number | boolean

여기서는 E가 타입 변수이다.

타입 추론을 맡기고 싶은 부분을 "infer {타입 변수}"로 표시하면 된다.

유의할 점으로는 컨디셔널 타입에서 타입 변수는 참 부분에서만 쓸 수 있다. 거짓 부분에서 사용하면 에러가 발생한다.

매개변수, 생성자 매개변수, 반환값, 인스턴스 타입을 추론하는 경우

// (...args: any) => any는 임의의 함수를 타이핑 하는 부분
type MyParaneters<T> = T extends (...args: infer P) => any ? P : never;
type P = MyParaneters<(a: string, b: number) => string>; // [a: string, b: number]

// abstract new (...args: any) => any는 임의의 생성자를 타이핑하는 부분
type MyConstructorParameters<T> = T extends abstract new (...args: infer P) => any ? P : never;
type CP = MyConstructorParameters<new (a: string, b: number) => {}>; // [a: string, b: number]

type MyReturnType<T> = T extends (...args: any) => infer R ? R : any;
type R = MyReturnType<(a: string, b: number) => string>; // string

type MyInstanceType<T> = T extends abstract new (...args: any) => infer R ? R : any;
type I = MyInstanceType<new (a: string, b: number) => {}> // {}

여기서 타입 P와 CP는 [a: string, b: number]로 표시가 된다. 

"[string, number]로 표시되어야 하는 것이 아닌가?"하는 의문을 가질 수 있다. 결론부터 말하면 [string, number]와 같은 의미이며, 튜플의 각 자리에 이름을 붙인 것이다.

서로 다른 타입 변수를 여러 개 사용하기

type MyPAndR<T> = T extends (...args: infer P) => infer R ? [P, R] : never;
type PR = MyPAndR<(a: string, b: number) => string>; // [[a: string, b: number], string]

매개변수는 P 타입 변수로, 반환값은 R 타입 변수로 추론했다.

같은 타입 변수를 여러 개 사용하기

type Union<T> = T extends {a : infer U, b: infer U} ? U : never;
type Result1 = Union<{a: 1 | 2, b: 2 | 3}>; // 1 | 2 | 3

type Intersection<T> = T extends {
  a: (pa: infer U) => void,
  b: (pb: infer U) => void,
} ? U : never;
type Result2 = Intersection<{a(pa: 1 | 2): void, b(pb: 2 | 3): void}>; // 2

Result1은 Union 타입을 사용하여 1 | 2 | 3이 되었다.

하지만 Result2는 좀 다르게 봐야한다. 매개변수는 반공변성을 갖기 때문에 매개변수에서는 인터섹션이 된다. 따라서 2가 된다.

타입 변수 중에서 하나가 매개변수고, 하나가 반환값인 경우(실무에서는 거의 못 봄)

type ReturnAndParam<T> = T extends {
  a: () => infer U,
  b: (pb: infer U) => void
} ? U : never;
type Result3 = ReturnAndParam<{a: () => 1 | 2, b(pb: 1 | 2 | 3): void}>; // 1 | 2
type Result4 = ReturnAndParam<{a: () => 1 | 2, b(pb: 2 | 3): void}>; // never

코드만 보면 어떤 규칙으로 결과가 나오는지 헷갈린다.

반환값의 타입이 매개변수의 타입의 부분집합인 경우에만 그 둘의 교집합이된다. 그 외에는 모두 never가 된다.

유니언을 인터섹션으로 만드는 타입

이 방법에서는 매개변수에 같은 타입 변수를 선언하면 인터섹션이 된다는 가정이 있어야 한다.

type UnionToIntersection<U>
= (U extends any ? (P: U) => void : never) extends (p: infer I) => void
? I
: never;
type Result5 = UnionToIntersection<{a: number} | {b: string}>; // {a: number;} & {b: string;}
type Result6 = UnionToIntersection<boolean | true>; // never

U는 제네릭이자 유니언이라서 컨디셔널 타입에서 분배법칙이 실행된다.

 

-Result5

Result5는 UnionToIntersection<{a: number}> | UnionToIntersection<{b: number}>이 된다.

그렇기에 UnionToIntersection<{a: number}>는 U extends any ? (p: U) => void : never)에서 (p: {a: never}) => void로 바뀐다. 이 타입은 (p: infer I) => void니까 I는 {a: never}이 된다. 

이 규칙을 UnionToIntersection<{b: number}에도 적용하면 {b: string}이 된다.

결과적으로 {a: number} & {b: string}이 되고, 이는 유니언을 인터섹션으로 바꾼 셈이 된다.

 

-Result6

Result6는 UnionToIntersection<boolean | true>가 UnionToIntersection<true | false | true>가 된다.

그리고 true & false & true는 never라서 never가 된다.

728x90
반응형
Comments