[TS] extends 키워드 활용

장동균·2022년 12월 20일
1

종류


1. 타입 extends 타입

타입의 확장으로 사용된다.

interface Animal {
  live(): void;
}

interface Dog extends Animal {
  woof(): void;
}

Dog 라는 interface는 live 함수와 woof 함수 2개의 프로퍼티를 가지게 된다.

2. T extends 타입

제네릭 타입을 연산자 우측 타입의 하위 타입으로 제한한다.

function stringOrNumber<T extends string | number>(arg: T) {
  return arg;
}

stringOrNumber<boolean>(true)  // 에러! Type 'boolean' does not satisfy the constraint 'string | number'

T로 전달되는 타입은 string | number 타입의 하위 타입이어야한다.

type MessageOf<T extends { message: unknown }> = T["message"];
 
interface Email {
  message: string;
}
 
type EmailMessageContents = MessageOf<Email>;
// type EmailMessageContents = string

T로 전달되는 타입은 반드시 message를 프로퍼티로 가지고 있어야 한다.

3. T extends 타입 ? A : B

제네릭 타입이 연산자 우측 타입의 하위 타입인 경우 A, 그렇지 않은 경우 B

type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
 
interface Email {
  message: string;
}
 
interface Dog {
  bark(): void;
}
 
type EmailMessageContents = MessageOf<Email>;
// type EmailMessageContents = string
 
type DogMessageContents = MessageOf<Dog>;
// type DogMessageContents = never

예제

type MessageOf<T> = T["message"]

"Type 'message' cannot be used to index type 'T'".

제네릭 인자 T로 전해질 객체에 message라는 프로퍼티가 반드시 존재하는데 이를 타입스크립트에서 모르는 경우

이런 경우 다음과 같이 T의 타입이 message 프로퍼티를 가지도록 제한할 수 있다.

type MessageOf<T extends { message: unknown }> = T["message"]

interface Email {
  message: string;
}

type EmailMessageContents = MessageOf<Email>;
// type EmailMessageContents = string

위의 경우와 비슷하지만 T로 전해질 객체에 message라는 프로퍼티가 존재하지 않을 수 있으며, 그 경우 never 타입을 반환받고 싶다면 다음과 같이 작성할 수 있다.

type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
 
interface Email {
  message: string;
}
 
interface Dog {
  bark(): void;
}
 
type EmailMessageContents = MessageOf<Email>;
// type EmailMessageContents = string
 
type DogMessageContents = MessageOf<Dog>;
// type DogMessageContents = never

infer와의 활용

원시 타입의 경우 해당 원시 타입을, 배열의 경우 원소의 타입을 얻는 유틸 함수를 작성한다면 다음과 같이 작성해볼 수 있다. (배열의 인자 타입으로 number를 넣으면 원소의 타입을 얻을 수 있다. index의 타입이 number 이기 떄문에)

type Flatten<T> = T extends any[] ? T[number] : T;
 
// Extracts out the element type.
type Str = Flatten<string[]>;
// type Str = string
 
// Leaves the type alone.
type Num = Flatten<number>;
// type Num = number

하지만, any가 있는 것이 꽤나 껄끄럽다. 이런 경우 infer 키워드를 활용해볼 수 있다.

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

infer 키워드는 any와 비슷하다고 생각해도 좋을 것 같다. 그 어떠한 타입도 모두 들어올 수 있다. 다만 any와의 차이점은 해당 타입을 특정 인자로 기억하는지에 대한 여부이다.

RetunType 또한 다음과 같이 만들어볼 수 있겠다.

type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
  ? Return
  : never;
 
type Num = GetReturnType<() => number>;
// type Num = number
 
type Str = GetReturnType<(x: string) => string>;
// type Str = string
 
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>;
// type Bools = boolean[]

type OnlyNumber = GetReturnType<1234>
// type OnlyNumber = never

Distributive Conditional Types

제네릭과 유니온 타입이 함께 쓰이는 경우 distributive하게 동작한다.

type ToArray<Type> = Type extends any ? Type[] : never;

type StrArrOrNumArr = ToArray<string | number>;
// type StrArrOrNumArr = string[] | number[]

다음과 같은 결과가 나타나는 이유는 제네릭 타입으로 유니온 타입이 들어오는 경우 나뉘어 지기 때문이다.

ToArray<string | number> => ToArray<string> | ToArray<number> => string[] | number[]

이러한 distributive한 동작을 피하고 싶다면 대괄호를 이용해야한다.

type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
 
// 'StrArrOrNumArr' is no longer a union.
type StrArrOrNumArr = ToArrayNonDist<string | number>;
// type StrArrOrNumArr = (string | number)[]

참고문헌

https://www.typescriptlang.org/docs/handbook/2/conditional-types.html

profile
프론트 개발자가 되고 싶어요

1개의 댓글

comment-user-thumbnail
2022년 12월 21일

^^

답글 달기