제네릭 활용하기 with extends & infer

camel·2024년 7월 20일
0
post-thumbnail

✅ 개요

최근 '인동댕'이라는 유튜브 영상을 보고, TypeScript의 제네릭에 대해 깊이 있게 생각해볼 기회가 있었습니다.

이 글에서는 제네릭의 개념을 JavaScript와 비교하며 살펴보고, extendsinfer 키워드의 활용법에 대해 알아보겠습니다.

이 개념들을 이해하면 이전보다 유연하고 안전한 코드를 작성할 수 있습니다.


💡 JavaScript와 TypeScript 제네릭 비교하기

먼저 JavaScript 함수와 TypeScript 제네릭을 비교해 보며 제네릭의 기본 개념을 살펴보겠습니다.

JavaScript:

const test = (a) => a;
const res = test(2); // res는 2

이 JavaScript 함수는 인자를 받아 그대로 반환합니다.

TypeScript로 표현한다면?

type Test<T> = T;
type CC = Test<2>; // CC의 타입은 2

여기서 Test<T>는 JavaScript의 test 함수와 유사한 역할을 합니다.
제네릭 T는 함수의 매개변수와 비슷한 개념으로, 어떤 타입이든 받아 그대로 반환할 수 있습니다.

🔍 제네릭 사용 예시:

제네릭을 사용하면 다양한 타입에 대해 재사용 가능한 타입을 만들 수 있습니다.

function identity<T>(arg: T): T {
    return arg;
}

const output1 = identity<string>("myString");  // 타입은 'string'
const output2 = identity<number>(100);         // 타입은 'number'

이렇게 하면 함수를 다양한 타입에 대해 유연하게 사용할 수 있습니다.
하지만 너무 많은 범위의 제네릭을 사용한다면, 안정적인 측면에서 우려할 수 있는데요. 이를 타입 범위를 좁히기 위해 사용하는 extends에 대해 알아보겠습니다.


🛠 extends 키워드 이해하기

extends 키워드는 제네릭에 들어올 수 있는 타입을 제한하는 강력한 도구입니다. 이를 통해 타입 안전성을 높이고, 특정 조건을 만족하는 타입만 허용할 수 있습니다.

기본 사용법:

type Test<T extends number> = T;
type CC = Test<1>; // 정상
type DD = Test<'4'>; // 오류: '4'는 'number' 타입에 할당할 수 없습니다.
TypeScript 제네릭 이미지

여기서 T extends numberTnumber 타입의 부분집합이어야 함을 의미합니다.
extends를 사용한다면 T의 범위를 제한하여 사용할 수 있습니다.

🎭 extends를 조건문처럼 사용하기

extends는 조건부 타입을 만드는 데도 사용할 수 있어, 타입 수준에서의 "if-else" 구문과 유사한 기능을 제공합니다:

type Test<T extends number> = T extends 2 ? 3 : 4;
type CC = Test<2>; // CC의 타입은 3
type DD = Test<5>; // DD의 타입은 4

이 예제에서는 T extends 2 ? 3 : 4는 "T가 2와 같으면 3, 아니면 4"라는 조건을 표현합니다.

🔍 extends 사용 예시: extends를 사용해 복잡한 타입 조건을 만들 수 있습니다:

type IsArray<T> = T extends any[] ? true : false;
type A = IsArray<number[]>; // true
type B = IsArray<string>;   // false

type ElementType<T> = T extends (infer U)[] ? U : never;
type C = ElementType<number[]>; // number
type D = ElementType<string>;   // never

🕵️ infer 키워드 이해하기

infer 키워드는 조건부 타입 내에서 타입을 추론하고 추출하는 데 사용되는 강력한 도구입니다. 복잡한 타입에서 특정 부분을 추출하거나 변형할 때 매우 유용합니다.

기본 사용법:

type Head<T extends any[]> = T extends [infer A, ...any[]] ? A : undefined;

type CC = Head<[1, 2, 3, 4]>; // CC의 타입은 1
type AA = Head<["하이", "바이"]>; // AA의 타입은 "하이"
type DD = Head<[]>; // DD의 타입은 undefined

infer를 사용하면 여러 타입 중, 특정 타입을 추출하여 사용할 수 있습니다.
위에서는 배열의 첫 번째 요소 타입을 추출하여 infer를 사용하였습니다.

🎨 extends와 infer 사용 예시:

//1번
type SecondElement<T extends [any, any, ...any[]]> = T extends [any, infer U, ...any[]] ? U : never;
type MyTuple = [number, string, boolean];
type SecondElementType = SecondElement<MyTuple>; // SecondElementType의 타입은 string

//2번
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type ResolvedString = UnwrapPromise<Promise<string>>; // ResolvedString의 타입은 string
type NumberValue = UnwrapPromise<number>; // NumberValue의 타입은 number

1번 케이스에서는 배열의 2번째 타입을 가져오기 위해 사용하였습니다.
extends에 있는 타입이 일치한다면 infer 타입을 가져오도록 작성하였습니다.
만약에 infer을 사용할줄 몰랐다면, 특정 타입을 추출하기 어려웠을 것입니다.

2번에서는 Promise의 제네릭 타입을 추출하여 사용하였습니다.
만약에 Promise 유형이 아닐 경우에는 기존의 타입을 추출하는데 사용하였습니다.

🔍 함수 반환 타입 추출하기: infer를 사용해 함수의 반환 타입을 추출할 수 있습니다.

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

function foo() { return { a: 1, b: "hello" }; }
type FooReturn = ReturnType<typeof foo>; // { a: number, b: string }

위에서 반환하는 타입이 있을 경우에는 이 타입을 return 값으로 설정하였습니다.


🎉 마무리

이 글에서는 TypeScript의 제네릭, extends, 그리고 infer 키워드에 대해 살펴보았습니다.

제가 타입 스크립트를 공부할 때는 extends와 infer 개념을 완벽하게 이해가 안되어서 고생했었는데, 이 글을 보고 더 쉽게 이해해주셨으면 좋겠습니다.

아래에는 제가 본 영상 링크를 첨부하였습니다 감사합니다.

참고 자료: 인동댕 YouTube 채널 (타입 잘 다루기)

profile
개발과 일을 잘하고 싶은 사람

0개의 댓글