infer와 친해지기

YEONGHUN KO·2024년 4월 26일
2

TYPESCRIPT - BASICS

목록 보기
7/8
post-thumbnail

타입챌린지를 하던 도중 infer라는 키워드를 발견하게 되었다.

infer 라는 단어의 영영 사전 뜻을 찾아보면은

to form(an opinion) from evidence, to reach (a conclusion) based on knwon facts.

라고 되어있다. 이미 알려진 정보들을 가지고 결론에 다다른다는 것이다.

일단, 처음 보는 녀석이다. 그럼 말을 걸어 봐야지. 공문과 블로그를 보게 되었고 어느정도 알게 되어 여기 정리해보려고 한다.

정의를 먼저 살펴보는 것도 좋지만 일단 바로 4가지 예시를 통해 dive하려고 한다.

conditional extends 구문안에서 사용될때

infer의 뜻에서도 알 수 있듯이, 어떤 값이 들어올지 알 수 없을때 주어진 조건을 고려하여 추론한다. 주로 조건문의 참인경우에 사용된다. 일단 바로 코드를 보자. ReturnType이라는 타입스크립트의 유틸리티 타입인데 아주 적절한 예시로 공문에서 활용된다.

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

// usage
function cb(a:number,b:number) {
  return a+b
}

let c:ReturnType<typeof cb> // number

여기서 R은 "new generic type variable" 이다. 새로운 제네릭 변수가 만들어진다.

즉 T처럼 타입변수로 들어올 함수가 어떤 값을 리턴할지 알수 없어서 일단 R이라는 변수로 담아놓고 추론하는 것이다.

그래서 함수형태가 T로 들어올 경우 함수의 리턴타입을 알아서 fetch out하는 것이고 그게 아니면 any를 리턴한다.

여기서 참인 경우에만 사용되는 이유는 어찌 보면 당연하다. 추론하여 참일 경우 추론된 값을 리턴하지만 거짓인 경우 추론이 틀렸기 때문에 무엇인지 예측불가능 이다. 그래서 any를 리턴하는 것.

객체안에 들어갈 타입을 extract할때

위의 예시에 확장된 개념이다. 객체 안에 들어갈 element도 infer할 수 있다. 그럼 바로 코드로 만나보자.

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

// or
// type Flatten<Type> = Type extends (infer Item)[] ? Item : Type;

Array라는 객체안에 무슨값이 들어갈지 모른다. 그래서 Item이라는 타입변수를 만든다. Item은 배열안에 들어갈 element이다. 그 element를 extract한다.

infer가 없었다면 T[number]를 이용했을 것이다.

배열의 나머지

...rest라는 expression을 본적이 있을 것이다. 보통 함수의 args에서 처음을 제외한 그 뒤에 달려오는 인자를 받고 싶을 때 사용한다. typescript에서도 가능하다.

type TailOf<T> = T extends [unknown, ...infer U] ? U : []

type A = TailOf<[string,boolean,number]>

const a:A = ['hello',3] // error

그럼 이걸 이용해서 특정배열의 index가 어디까지 증가할 수 있는지 알아내는 타입을 짜보자.

infer이 typechallenge를 걸어왔다! 두두둥

IndexOfTuple

// 0 | 1 | 2 | 3 | 4
type AvailableIndex = IndexOf<[1,2,3,4,5]>

우선 여러가지 방법이 있겠지만 재귀를 이용해서 해결해보려고 한다.

아니 잠깐, 타입스크립트에도 재귀가 있나??

응!

말그대로 재귀이다. 자기 자신을 호출하는 타입이다 간단하게 일단 알아보자.

const data = [
  { label: '기본', value: 'basic' },
  {
    label: '시간',
    sub: [
      { label: '월별', value: 'month' },
      { label: '주별', value: 'week' },
      { label: '일별', value: 'day' },
    ],
  },
]

type DataType = {
  label: string
  value?: string
  sub?: Array<DataType>
}

type DataInterface = Array<DataType>

const data: DataInterface = [
  { label: '기본', value: 'basic' },
  {
    label: '시간',
    sub: [
      { label: '월별', value: 'month' },
      { label: '주별', value: 'week' },
      { label: '일별', value: 'day' },
    ],
  },
]

요런식으로 sub안에 DataType이 들어갈 수 있을때 사용된다.

그럼 다시 돌아와서, IndexOfTuple을 만들어보자


type IndexOfTuple<Tuple extends readonly any[]> = 
  Tuple extends readonly [unknown,... infer Tail] ? 
    Tail extends any[] 
    ? Tail['length'] | IndexOfTuple<Tail> 
    : never
  : never

// 0 | 3 | 2 | 1
type Indexes = IndexOfTuple<[false,'1',30,()=>{}]>

Tail을 잘라가면서 length를 이어 붙이는 식이다.

IdxOfTuple<[0,0,0]>
= 2 | IdxOfTuple<[0,0]>
= 2 | 1 | IdxOfTuple<[0]>
= 2 | 1 | 0 | IdxOfTuple<[]>
= 2 | 1 | 0

요런식으로 말이다.

응용하면?

type TupleToObject<
  Tuple extends readonly string[],
  Index extends IndexOfTuple<Tuple>
> = {
  [key in Tuple[Index]]:key
}


const arr =['a','b','c','d']

type Result = TupleToObject<typeof arr,2> 

//{2:2}

그럼 타입챌린지 끝!

두개의 infer

extends 문에서 infer를 여러번 할 수 있다.

이건 좀 어려울 수 있다. 일단 이런게 있다 정도만 알고 넘어가자.

// 매개변수 타입과 리턴 타입을 추론해서 튜플에 담아 리턴해 준다.
type ParamAndReturn<T>=T extends (...args: infer P) => infer R ? [P, R] : never;

// a의 타입과 b의 타입이 합쳐져서 리턴된다. 
type InferUnion<T> = T extends { a: infer U; b: infer U } ? U : never;
// a의 타입과 b의 타입의 intersection이 리턴된다. 매개변수는 반공변성을 가지고 있기 때문이다

type InferIntersection<T> = T extends { a: (x: infer U) => void; b: (y: infer U) => void } ? U : never;

출처

witch

공문

Dat

profile
'과연 이게 최선일까?' 끊임없이 생각하기

0개의 댓글