📢 어렵고 정석적인 개념 설명보다는 저같은 초보자도 이해하기 쉽게 정리하는 것을 원칙으로 포스팅하고 있습니다. 😄

[TypeScript] 타입스크립트의 유틸리티 타입 정리

유틸리티 타입이란?

유틸리티 타입이란, 제네릭, 맵드 타입, 조건부 타입 등의 타입 조작 기능을 이용해서 타입 스크립트 코드에서 타입을 더욱 강력하고 유연하게 다룰 수 있도록 도와주는 기능들을 말한다. 즉, 모든 프로퍼티들을 선택적 프로퍼티로 바꿔주거나 특정 프로퍼티만 골라내는 등의 자주 사용되는 타입들을 마치 내장 함수처럼 타입스크립트 자체에서 제공해 주는 타입인 것이다.

 

들어가기 전, 이 글에서는 타입스크립트에 있는 모든 유틸리티 타입을 정리하지 않을 것이다. 유틸리티 타입 중에서도 자주 사용되는 타입들만 정리할 예정이며 이외에도 다른 유틸리티 타입이 궁금하다면 타입스크립트 공식문서를 참고하길 바란다.

 

객체 타입에서 사용하는 유틸리티 타입

Partial<T> / Required<T> : 필수 프로퍼티 ⇔ 선택적 프로퍼티

interface Person {
    name: string;
    age: number;
    job: string;
    address: string;
    hobby: string;
}

// Error가 나는 경우
const personA: Person = {
    name: 'Shawn',
    age: 30
    // name과 age만 쓰고 싶을 땐?
}

// Partial 타입을 쓰면 Error가 나지 않음
const personB: Partial<Person> = {
    name: 'Shawn',
    age: 30
}

// Partial 타입을 직접 구현해보기
type Partial<T> = {
    [key in keyof T]?: T[key];
}

객체의 타입이 정의된 interface가 있고, 거기에 맞춰서 객체를 하나 생성하려고 한다.

interface에 정의된 모든 프로퍼티를 가지고 있는 객체를 생성할 경우에는 문제가 될 건 없지만, 만약 interface의 프로퍼티 중 일부 프로퍼티만 있는 객체를 생성하게 될 경우에는 오류가 발생하게 된다. 이럴 때는 유틸리티 타입인 Partial<T>를 사용하면 객체 타입의 모든 프로퍼티를 선택적 프로퍼티로 바꿔주기 때문에 해당 객체 타입의 일부 프로퍼티만 갖고 있는 객체를 생성할 수 있게 된다.

const person: Required<Person> = {
    name: 'Shawn',
    age: 30,
    // Error! Required 타입은 모든 프로퍼티가 있어야 함
}

Partial 타입의 반대 타입인 Required 타입도 있다. Partial이 모든 프로퍼티를 선택적 프로퍼티로 바꿔주었다면, Required 타입은 모든 프로퍼티를 필수 프로퍼티로 바꿔준다.

선택적 프로퍼티를 가지고 있는 interface의 프로퍼티를 모두 필수 프로퍼티로 바꾸고 싶다면, Required 타입을 사용하면 된다.

 

Readonly<T> : 프로퍼티 → 읽기 전용 프로퍼티

const readonlyA: Readonly<Person> = {
    name: 'Lee',
    age: 25,
    ...
}

readonlyA.name = 'Kim'; // 읽기 전용이므로 Error!

Readonly 타입은 말 그대로 모든 프로퍼티를 읽기 전용 프로퍼티로 바꿔주는 타입이다.

읽기 전용이 되면 특정 프로퍼티에 새로운 값을 재할당시킬 수 없다.

 

Pick<T, U> : 특정 프로퍼티만 골라내는 타입

interface Person {
    name: string;
    age: number;
    job: string;
    address: string;
    hobby: string;
}

const pickA: Pick<Person, 'name' | 'age'> = {
    name: 'Shawn',
    age: 30
    // 'name'과 'age'만 골라냈기 때문에 에러가 발생하지 않음
}

// Pick 타입 구현해보기
type Pick<T, K extends keyof T> = {
    [key in K]: T[key];
}

Pick 타입은 선언된 객체 타입의 특정 프로퍼티만을 가져오고 싶을 때 사용하는 타입이다.

예를 들어, 위 코드처럼 Person 타입의 프로퍼티 중에 name과 age의 타입만 가져오고 싶은 경우가 있을 수 있다. 이럴 때, Pick<T, U> 타입을 사용해서 T에 객체 타입, U에 가져오고 싶은 T의 프로퍼티들을 유니온 타입으로 적어주면 된다.

 

Omit<T, U> : 특정 프로퍼티만 제거(생략)하는 타입

interface Person {
    name: string;
    age: number;
    job: string;
    address: string;
    hobby: string;
}

const omitA: Omit<Person, 'name' | 'age'> = {
    job: 'developer',
    address: 'seoul',
    hobby: 'swimming'
    // 'name'과 'age'만 제거했기 때문에 오류가 발생하지 않음
}

// Omit 타입 구현해보기
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

Omit 타입은 Pick 타입과 반대로 특정 프로퍼티를 제거해 주는 타입이다.

예를 들어, 위 코드처럼 Person 타입의 프로퍼티 중에 name과 age만 생략하고 나머지 프로퍼티만 가져오고 싶은 경우가 있을 수 있다. 이럴 때, Omit<T, U> 타입을 사용해서 T에 객체 타입, U에 T에서 생략하고 싶은 프로퍼티들을 유니온 타입으로 적어주면 된다.

 

Record<T, U> : 객체 타입을 만들어주는 타입

type Legacy = {
    large: {
        url: string
    };
    medium: {
        url: string
    };
    small: {
        url: string
    };
    ...
    // 이렇게 하나씩 만들어 주는 것은 너무 비효율적!
}

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

// Record 타입 구현해보기
type Record<K extends keyof any, V> = {
    [key in K]: V;
}

각 프로퍼티마다 공통된 값이 들어가는 객체 타입이 있을 때, 타입을 프로퍼티마다 하나씩 모두 선언해 주는 것은 매우 비효율적인 방법이다. 이때, Record 타입을 사용하면 프로퍼티마다 공통된 값을 가지는 객체 타입을 쉽게 만들 수 있다.

Record<K, V>로 선언하고, K에는 프로퍼티의 이름, V에는 K가 가지는 공통된 값을 넣어주면 된다.

 

일반적으로 사용되는 유틸리티 타입

Exclude<T, U> : T에서 U를 제거하는 타입

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

// Exclude 구현해보기
type Exclude<T, U> = T extends U ? never : T;

Exclude<T, U>T에서 U를 제거한다. 위의 예시를 보면 string | boolean 유니온 타입에서 boolean을 제거했으므로 변수 A의 타입은 string 타입이 된다.

 

이전 글에서 제네릭 타입에 유니온 타입이 들어오는 경우에는 분산적인 조건부 타입으로 변한다고 했었다.

따라서, T는 string | boolean 유니온 타입으로 들어가는 게 아니라 string으로 한 번, boolean으로 한 번, 분산적으로 T에 들어가게 된다. T가 U의 서브 타입일 경우 T를 반환하며, 그렇지 않을 경우 never를 반환한다.

 

Extract<T, U> : T에서 U를 추출하는 타입

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

// Extract 구현해보기
type Extract<T, U> = T extends U ? T : never;

Exclude와 반대로 Extract특정 타입을 추출하는 타입이다. 위의 예시를 보면 string | boolean 유니온 타입에서 boolean 타입을 추출했으므로 변수 B의 타입은 boolean이 된다.

 

Exclude와 구현 방식은 똑같으며, 대신 참과 거짓일 때 반환되는 타입만 서로 바뀌었다.

 

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

function funcA() {
    return 'hello';
}

function funcB() {
    return 1;
}

type ReturnA = ReturnType<typeof funcA>;
type ReturnB = ReturnType<typeof funcB>;

// ReturnType 구현해보기
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : never;

ReturnType함수의 반환값 타입을 추출하는 타입으로, typeof와 함께 사용한다. (결국 반환값의 타입이기 때문에)

funcA()의 반환값은 string 타입이므로 ReturnA의 타입은 string 타입이 되고, funcB()의 반환값은 number 타입이므로 ReturnB의 타입은 number 타입이 된다.

 

1. Partial<T> : 객체 타입 T의 모든 프로퍼티를 선택적 프로퍼티로 바꿈
2. Required<T> : 객체 타입 T의 모든 프로퍼티를 필수 프로퍼티로 바꿈
3. Readonly<T> : 객체 타입 T의 모든 프로퍼티에 readonly 속성을 추가
4. Pick<T, U> : 객체 타입 T의 프로퍼티 중 U 프로퍼티만 가져옴
5. Omit<T, U> : 객체 타입 T의 프로퍼티 중 U 프로퍼티만 생략함
6. Record<T, U> : 모든 프로퍼티가 공통된 값을 가지는 객체 타입을 만들 때 사용. T는 프로퍼티의 이름, U는 T의 값. 
7. Exclude<T, U> : T 타입에서 U 타입을 제거. 보통 T는 유니온 타입.
8. Extract<T, U> : T 타입에서 U 타입만 추출. 보통 T는 유니온 타입.
9. ReturnType<typeof T> : 함수 반환값의 타입을 추출. T는 함수.