원문 : https://levelup.gitconnected.com/using-typescript-infer-like-a-pro-f30ab8ab41c7
타입스크립트 마스터링 시리즈에 오신 것을 환영합니다. 이 시리즈는 애니메이션 형식으로 타입스크립트의 핵심 지식과 기술을 소개합니다. 함께 배워요! 이전 글들은 다음과 같습니다.
- 타입스크립트 제네릭에서 K, T 및 V는 무엇인가요?
- 전문가처럼 타입스크립트 매핑 타입 사용하기
- 전문가처럼 타입스크립트 조건부 타입 사용하기
- 전문가처럼 타입스크립트 추론 사용하기
- 전문가처럼 타입스크립트 템플릿 리터럴 타입 사용하기
- 타입스크립트 시각화: 가장 많이 사용되는 15가지 유틸리티 타입
T0 배열 타입의 엘리먼트 타입과 T1 함수 타입의 반환 값 타입을 가져오는 방법을 알고 있나요? 몇 초 동안 생각해보세요.
type T0 = string[];
type T1 = () => string;
타입스크립트에서 제공하는 타입 패턴 일치 기술인 조건부 타입 과 infer가 이전 질문의 답이 될 수 있습니다.
조건부 타입을 사용하면 두 타입 간의 관계를 발견할 수 있으며 조건부 타입을 통해 두 타입이 호환되는지 여부를 확인할 수 있습니다. Infer는 패턴 일치 중에 캡처된 타입을 저장할 타입 변수를 선언하는 데 사용됩니다.
T0 배열 타입에서 엘리먼트 타입을 캡처하는 방법을 살펴보겠습니다.
type UnpackedArray<T> = T extends (infer U)[] ? U : T
type U0 = UnpackedArray<T0> // string
위의 코드에서 T extends (infer U)[] ? U : T
는 조건부 타입의 구문이며, extends 절의 infer U
는 유추된(inferred) 타입을 저장할 새로운 타입 변수 U를 도입합니다.
이해를 돕기 위해 UnpackedArray 유틸리티 타입의 실행 흐름을 보여드리겠습니다.
)
infer는 조건부 타입의 extends 절에서만 사용할 수 있고, infer로 선언된 타입 변수는 조건부 타입의 true 분기에서만 사용할 수 있다는 것을 유의해야 합니다.
type Wrong1<T extends (infer U)[]> = T[0] // 에러
type Wrong2<T> = (infer U)[] extends T ? U : T // 에러
type Wrong3<T> = T extends (infer U)[] ? T : U // 에러
이 지식을 바탕으로 T1 함수 타입의 반환 타입을 가져오는 방법을 살펴보겠습니다.
type UnpackedFn<T> = T extends (...args: any[]) => infer U ? U : T;
type U1 = UnpackedFn<T1>; // string
UnpackedFn 유틸리티 타입의 구현을 보면 매우 간단해 보이지 않나요? 함수를 오버로딩 한 경우 타입스크립트는 타입 추론에 마지막 호출 시그니처를 사용합니다.
)
타입스크립트 조건부 타입에 익숙하지 않다면 이 글을 읽어보세요.
https://javascript.plainenglish.io/use-typescript-conditional-types-like-a-pro-7baea0ad05c5
위의 글에서는 더 강력한 Unpacked 유틸리티 타입을 구현할 수 있는 조건부 체이닝을 소개했습니다.
type Unpacked<T> =
T extends (infer U)[] ? U :
T extends (...args: any[]) => infer U ? U :
T extends Promise<infer U> ? U :
T;
type T0 = Unpacked<string>; // string
type T1 = Unpacked<string[]>; // string
type T2 = Unpacked<() => string>; // string
type T3 = Unpacked<Promise<string>>; // string
type T4 = Unpacked<Promise<string>[]>; // Promise<string>
type T5 = Unpacked<Unpacked<Promise<string>[]>>; // string
위의 코드에서 Unpacked 유틸리티 타입은 조건부 타입과 조건부 체인을 사용해서 배열 타입의 엘리먼트 타입, 함수 타입의 반환 값 타입, 그리고 Promise 타입의 반환 값 타입을 쉽게 유추합니다.
사실, 조건부 타입과 infer를 사용해서 객체 타입에서 키 타입을 추론할 수도 있습니다. 다음으로 구체적인 예를 들어보겠습니다.
type User = {
id: number;
name: string;
}
type PropertyType<T> = T extends { id: infer U, name: infer R } ? [U, R] : T
type U3 = PropertyType<User> // [number, string]
PropertyType 유틸리티 타입에서 infer를 사용해서 object 타입의 id와 name 프로퍼티 타입을 각각 나타내는 두 개의 타입 변수 U 및 R을 선언합니다. 타입이 일치하면 id 및 name 프로퍼티의 타입을 튜플로 반환합니다.
그럼 이제 PropertyType 유틸리티 타입에서 타입 변수 U 하나만 선언된다면 결과는 어떻게 될까요? 다음을 확인해보세요.
type PropertyType<T> = T extends { id: infer U, name: infer U } ? U : T
type U4 = PropertyType<User> // string | number
위의 코드에서 알 수 있듯이 U4 타입은 문자열과 숫자 타입의 union을 반환합니다. 왜 그런 결과를 반환할까요? 공변(covariant) 위치에 동일한 타입 변수에 대한 후보가 여러 개 있는 경우에는 최종 타입이 union 타입으로 유추되기 때문입니다.
그러나 반공변(contravariant) 위치에서 동일한 타입 변수에 대한 후보가 여러 개 있는 경우 최종 타입이 intersection 타입으로 유추됩니다. 이번에도 실제로 확인해보세요.
type Bar<T> = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never;
type U5 = Bar<{ a: (x: string) => void, b: (x: number) => void }>; // string & number
위의 코드에서 U5 타입은 문자열과 숫자 타입으로 구성된 intersection 타입을 반환합니다. 즉, 최종 타입은 never 타입입니다.
마지막으로 타입스크립트 4.7에 도입된 새로운 infer 관련 기능을 소개하겠습니다. 이는 infer 타입 추론을 더 간결하게 만듭니다. infer와 관련된 새로운 기능을 소개하기 전에 예를 살펴보겠습니다.
type FirstIfString<T> = T extends [infer S, ...unknown[]]
? S extends string
? S
: never
: never;
위 코드에서 FirstIfString 유틸리티 타입은 타입스크립트의 조건부 타입, 조건부 체인 및 infer 타입 추론을 사용합니다. 첫 번째 조건부 타입에서는 타입 매개변수 T의 실제 타입이 비어 있지 않은 튜플 타입인지 확인하고 패턴 일치 중에 캡처된 튜플 타입의 첫 번째 엘리먼트 타입을 저장하기 위해 infer를 사용해서 타입 변수 S를 선언합니다.
두 번째 조건부 타입에서는 타입 변수 S가 문자열 타입의 서브타입(subtype)인지 여부를 확인하고 조건이 충족되면 타입 변수 S에 해당하는 타입을 반환하고, 그렇지 않으면 조건부 타입의 모든 false 분기가 never 타입을 반환합니다.
FirstIfString 유틸리티 타입이 하는 일을 소개한 후 기능을 확인해 보겠습니다.
위의 결과에서 볼 수 있듯이 FirstIfString 유틸리티 타입은 잘 작동합니다. 여기서 의문이 생깁니다. 유틸리티 타입은 내부적으로 두 가지 조건부 타입을 사용했는데, 하나의 조건부 타입을 사용해도 동일한 기능을 만들 수 있을까요? 타입스크립트 4.7을 사용하면 infer 타입에 선택적 extends 절을 추가해서 타입 변수에 대한 명시적 제약 조건을 지정할 수 있습니다.
type FirstIfString<T> =
T extends [infer S extends string, ...unknown[]]
? S
: never;
이전 코드와 비교하면 훨씬 깔끔한 느낌이 들지 않나요? 이 글을 읽은 후에는 조건부 타입과 infer의 목적을 충분히 이해했다고 생각합니다. UnionToIntersection 유틸리티 타입의 특정 구현부를 이해할 수 있나요?
type UnionToIntersection<U> = (
U extends any ? (arg: U) => void : never
) extends (arg: infer R) => void
? R
: never
애니메이션 형식으로 타입스크립트를 배우고 싶다면 Medium 또는 Twitter에서 저를 팔로우하면 TS와 JS에 대해 더 많은 것을 읽을 수 있습니다!
'아티클 번역' 카테고리의 다른 글
[번역] Ecma 인터네셔널에서 ECMAScript 2022를 승인했습니다. 새로운 기능은 무엇인가요? (0) | 2022.12.11 |
---|---|
[번역] 자바스크립트 번들러 만들기 (0) | 2022.12.11 |
[번역] 문 vs 표현식 (0) | 2022.12.11 |
[번역] JavaScript 패키지 매니저 비교 - npm, Yarn 또는 pnpm? (0) | 2022.12.11 |
[번역] React에 SOLID 원칙 적용하기 (0) | 2022.12.11 |