본문 바로가기

Frontend/Typescript

조건부 타입과 유틸리티 타입

조건부 타입

조건부 타입삼항 연산자를 사용해서 타입을 정의하는 타입이다.

/**
 * 조건부 타입
 */

type A = number extends string ? string : number;  // number 타입

type ObjA = {
    a: number;
}

type ObjB = {
    a: number;
    b: number;
}

type B = ObjB extends ObjA ? number : string;  // number 타입

/**
 * 제네릭과 조건부 타입
 */

type StringNumberSwitch<T> = T extends number ? string : number;

let varA: StringNumberSwitch<number>;  // string 타입
let varB: StringNumberSwitch<string>;  // number 타입

function removeSpaces<T>(text: T): T extends string ? string : undefined;

function removeSpaces(text: any) {
    if (typeof text === 'string') {
        return text.replaceAll(' ', '');
    } else {
        return undefined;
    }
}

let result1 = removeSpaces('hi I am winterlood');
result1.toUpperCase();

let result2 = removeSpaces(undefined);

removeSpaces 함수의 경우, 함수 오버로딩을 활용해서 반환값의 타입을 조건부 타입으로 지정해주었다.

함수 오버로딩을 사용하지 않으면 제네릭 타입을 함수 내부에서 unknown 타입으로 인식하기 때문에 제대로 된 타입을 가질 수 없다.

 

분산적인 조건부 타입

분산적인 조건부 타입유니온과 함께 쓸 때 나타나는 타입이다.

/**
 * 분산적인 조건부 타입
 */

type StringNumberSwitch<T> = T extends number ? string : number;

let c: StringNumberSwitch<number | string>;
// StringNumberSwitch<number> | StringNumberSwitch<string>
// string | number 타입

type Exclude<T, U> = T extends U ? never : T;

type A = Exclude<number | string | boolean, string>;
// Exclude<number, string> | Exclude<string, string> | Exclude<boolean, string>
// number | never | boolean
// number | boolean

type Extract<T, U> = T extends U ? T : never;

type B = Extract<number | string | boolean, string>;
// Extract<number, string> | Extract<string, string> | Extract<boolean, string>
// never | string | never
// string

타입 변수에 유니온 타입을 넣어주게 되면, 분리되어서 동작하게 된다.

 

다음과 같이 작성하면 분산적인 조건부 타입으로 작동하지 않게 할 수 있다.

type StringNumberSwitch<T> = [T] extends [number] ? string : number;

let c: StringNumberSwitch<number | string>;  // number 타입

 

infer - 조건부 타입 내에서 타입 추론하기

/**
 * infer
 * inference -> 추론하다
 */

type FuncA = () => string;

type FuncB = () => number;

type ReturnType<T> = T extends () => infer R ? R : never;

type A = ReturnType<FuncA>;  // string 타입

type B = ReturnType<FuncB>;  // number 타입

type C = ReturnType<number>;  // 추론 불가 => never 타입

type PromiseUnpack<T> = T extends Promise<infer R> ? R : never;

type PromiseA = PromiseUnpack<Promise<number>>;  // number 타입

type PromiseB = PromiseUnpack<Promise<string>>;  // string 타입

infer R는 R이라고 생각하면 된다.

type A를 예시로 설명해보자.

() ⇒ string과 () ⇒ infer R의 관계를 따진다.

이때, R은 참이 되도록 동작을 하기 때문에, R은 string 타입이 된다.

 

유틸리티 타입

유틸리티 타입은 제네릭, 맵드 타입, 조건부 타입 등의 타입 조작 기능을 이용해 실무에서 자주 사용되는 타입을 미리 만들어 놓은 것이다.

 

아래 사진에 나온 유틸리티 타입에 대해 알아보자.

이 유틸리티 타입들은 자주 사용되는 것들을 모아놓은 것이다.

 

Partial<T>

Partial<T>는 타입 변수로 전달한 T의 모든 프로퍼티를 선택적 프로퍼티로 만드는 유틸리티 타입이다.

T의 프로퍼티 중 몇 가지만 가져와서 타입으로 정의해주고 싶은 경우 사용한다.

/**
 * Partial<T>
 * -> 부분적인, 일부분의
 * -> 특정 객체 타입의 모든 프로퍼티를 선택적 프로퍼티로 바꿔주는 타입
 */

interface Post {
    title: string;
    tags: string[];
    content: string;
    thumbnailURL?: string;
}

const draft: Partial<Post> = {
    title: '1',
    content: '초안'
}

 

Partial<T> 타입 직접 구현해보기

type Partial<T> = {
    [key in keyof T]?: T[key];
}

T의 모든 프로퍼티를 선택적 프로퍼티로 변환해주는 것이다.

 

Required<T>

/**
 * Required<T>
 * -> 필수의, 필수적인
 * -> 특정 객체 타입의 모든 프로퍼티를 필수 프로퍼티로 바꿔주는 타입
 */

interface Post {
    title: string;
    tags: string[];
    content: string;
    thumbnailURL?: string;
}

const withThumbnailPost: Required<Post> = {
    title: '타입스크립트',
    tags: ['ts'],
    content: '',
    thumbnailURL: 'https://...'
}

우리가 Post 타입을 정의할 때 thumbnailURL은 선택적 프로퍼티로 정의해서 Post 타입 객체 선언 시 thumbnailURL 없이도 오류가 발생하지 않는데, 타입을 Required<T>로 선언하게 되면, T의 모든 프로퍼티를 필수 프로퍼티로 변경해준다.

 

Required<T> 타입 직접 구현하기

type Required<T> = {
    [key in keyof T]-?: T[key];
}

 

Readonly<T>

/**
 * Readonly<T>
 * -> 읽기 전용, 수정 불가
 * -> 특정 객체 타입에서 모든 프로퍼티를 읽기 전용 프로퍼티로 만들어주는 타입
 */

const readonlyPost: Readonly<Post> = {
    title: '보호된 게시글입니다.',
    tags: [],
    content: ''
}

 

Readonly<T> 타입 직접 구현해보기

type Readonly<T> = {
    readonly [key in keyof T]: T[key];
}

 

Pick<T, K>

/**
 * Pick<T, K>
 * -> 뽑다, 고르다
 * -> 객체 타입으로부터 특정 프로퍼티만 골라내는 타입
 */

interface Post {
    title: string;
    tags: string[];
    content: string;
    thumbnailURL?: string;
}

const legacyPost: Pick<Post, 'title' | 'content'> = {
    title: '옛날 글',
    content: '옛날 컨텐츠'
}

 

Pick<T, K> 직접 구현해보기

type Pick<T, K extends keyof T> = {
    [key in K]: T[key]; 
}

 

Omit<T, K>

Omit<T, K> 타입은 Pick<T, K>의 정확히 반대 기능을 수행하는 타입이다.

/**
 * Omit<T, K>
 * -> 생략하다, 빼다
 * -> 객체 타입으로부터 특정 프로퍼티를 제거하는 타입
 */

interface Post {
    title: string;
    tags: string[];
    content: string;
    thumbnailURL?: string;
}

const noTitlePost: Omit<Post, 'title'> = {
    content: '',
    tags: [],
    thumbnailURL: ''
}

 

Omit<T, K> 직접 구현해보기

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

 

Record<K, V>

Record<K, V> 타입은 객체 타입을 만들어주는 유틸리티 타입인데, 첫 번째 타입 변수로는 객체의 프로퍼티 키를 유니온으로 받고, 두 번째 타입 변수로는 해당 키의 값의 타입을 받는다.

/**
 * Record<K, V>
 */

type ThumbnailLegacy = {
    large: {
        url: string;
    },
    medium: {
        url: string;
    },
    small: {
        url: string;
    }
}

type Thumbnail = Record<'large' | 'medium' | 'small', { url: string }>;

 

Record<K, V> 직접 구현해보기

type Record<K extends keyof any, V> = {
    [key in K]: V
}

 

Exclude<T, U>

/**
 * Exclude<T, U>
 * -> 제외하다, 추방하다
 * -> T에서 U를 제거하는 타입
 */

type A = Exclude<string | boolean, boolean>;  // string 타입

 

Exclude<T, U> 직접 구현해보기

type Exclude<T, U> = T extends U ? never : T;

// Exclude<string, boolean> |
// Exclude<boolean, boolean>

// string |
// never

// string

 

Extract<T, U>

Extract<T, U>는 Exclude<T, U>의 반대 역할을 수행하는 타입이다.

/**
 * Extract<T, U>
 * -> T에서 U를 추출하는 타입
 */

type B = Extract<string | boolean, boolean>;  // boolean 타입

 

Extract<T, U> 직접 구현해보기

type Extract<T, U> = T extends U ? T : never;

 

ReturnType<T>

/**
 * ReturnType<T>
 * -> 함수의 반환값 타입을 추출하는 타입
 */

function funcA() {
    return 'hello';
}

function funcB() {
    return 10;
}

type ReturnA = ReturnType<typeof funcA>;  // string 타입

type ReturnB = ReturnType<typeof funcB>;  // number 타입

 

ReturnType<T> 직접 구현해보기

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