[TypeScript] 실전 유틸리티 타입

hyeon·2024년 12월 8일

실전 유틸리티 타입

✅ 데이터를 이용해 간단한 계산을 하는 함수들을 Utility Function이라고 부르는 것처럼 타입을 통해 간단한 계산을 수행해 주는 타입을 유틸리티 타입!이라고 합니다.

Utility Type 들

-Pick<T, K>

interface Todo {
    title: string;
    description: string;
    completed: boolean;
}

type TodoPreview = Pick<Todo, 'title' | 'completed'>;

const todo: TodoPreview = {
    title: 'Clean room',
    completed: false,
};

Pick은 우리가 가지고 있는 타입 중에서 원하는 값을 골라서 Pick하는거죠. => 골라서 새로운 타입을 만들게 해줍니다.

Pick안에 제너릭으로 이렇게 쓰고, <원본타입, 우리가 넣고 싶은 타입> 을 유니온으로 key값을 넣어주게 되면, TodoPreview에 그 타입만 따로 빠져있는 것을 볼 수 있어요.


  • Omit<T, K>
interface Todo {
    title: string;
    description: string;
    completed: boolean;
}

type TodoPreview = Omit<Todo, 'description'>;

const todo: TodoPreview = {
    title: 'Clean room',
    completed: false,
};

Omit은 Pick과 정반대의 동작을 해요. <원본타입, 우리가 빼고 싶은 타입>


  • Exclude<T, U>
type T0 = Exclude<"a" | "b" | "c", "a">;  // "b" | "c"
type T1 = Exclude<"a" | "b" | "c", "a" | "b">;  // "c"
type T2 = Exclude<string | number | (() => void), Function>;  // string | number

Exclude는 Pick과 Omit의 사용성과 똑같은데요. 앞에 있는 원본 타입에서, 내가 뒤에 넣어져 있는 타입과 겹치는 것을 제거해줍니다.

💡 예시)
앞: "a" | "b" | "c"
뒤: "a"

=> 앞,뒤에서 겹치는 부분 제거
=> "b" | "c"


  • Partial<T>
interface Todo {
    title: string;
    description: string;
}

function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
    return { ...todo, ...fieldsToUpdate };
}

const todo1 = {
    title: 'organize desk',
    description: 'clear clutter',
};

const todo2 = updateTodo(todo1, {
    description: 'throw out trash',
});

console.log(todo1) //{ title: 'organize desk', description: 'clear clutter' }
console.log(todo2) //{ title: 'organize desk', description: 'throw out trash' }

우리가 원본타입을 Parital이라는 유틸리티 타입을 감싸주게 되면,
여기있는(Todo) 이 타입은 전부 옵셔널 타입으로 변경됩니다.

즉 밑에 방식으로 변환이 됩니다.

interface ParitalTodo {
  title?: string;
  description?: string;
}

우리가 어떤 값을 업데이트해야해서,
필드를 전체 다 불러올 수 없을 때
Parital이라는 유틸리티 타입으로 감싸주면 됩니다.


  • ReadOnly<T>
interface Todo {
    title: string;
}

const todo: Readonly<Todo> = {
    title: 'Delete inactive users',
};

todo.title = 'Hello'; // 오류: 읽기 전용 프로퍼티에 재할당할 수 없음

Readonly라는 제너릭 타입으로 이렇게 원본 타입을 묶어주게 되면,
이 객체(todo)는 타입스크립트에서 변경할 수 없는 객체로 프로퍼티 오류를 내뱉게 돼요.


  • Record<K, T>
interface PageInfo {
    title: string;
}

type Page = 'home' | 'about' | 'contact';

const x: Record<Page, PageInfo> = {
    about: { title: 'about' },
    contact: { title: 'contact' },
    home: { title: 'home' },
};

Record는 우리가 key값과 value 값을 따로따로 모두 다 타입을 지정해주고 싶을 때 사용합니다.


  • Extract<T, U>
type T0 = Extract<"a" | "b" | "c", "a" | "f">;  // "a"
type T1 = Extract<string | number | (() => void), Function>;  // () => void

Extract라는 타입은 아까 Exclude랑 조금은 비슷한 면이 있는데요.
<원본타입, 내가 추출하고 싶은 타입>, Exclude랑 다르게 "중복이 되는 타입만" 추출해줍니다.


  • ReturnType<T>
function getUser() {
    return { name: 'Alice', age: 25 };
}

type User = ReturnType<typeof getUser>;

const user: User = { name: 'Alice', age: 25 };

ReturnType도 굉장히 많이 사용합니다.
이것은 함수의 반환 값의 타입을 추론해주는 유틸리티 타입이에요

<typeof getUser>의 의미는 아래 코드와 같아요.

type FunctionType = typeof getUser;
// 위의 명령어는 아래와 같은 내용
// type FunctionType = () => {
//  name: string;
//  age: number;
}

그래서 이 함수가 반환하고 있는 타입을 따로 계산해 줄 수가 있는 거죠. 우리가 외부에서 함수가 선언되어 있거나 함수의 반환값이 조금 복잡해서 따로 선언을 해주기 어려울 때 함수를 통해서 타입을 만들어 줄 때 사용합니다.


  • Parameters<T>
function log(message: string, userId: number): void {
    console.log(`${userId}: ${message}`);
}

type LogParams = Parameters<typeof log>;

const params: LogParams = ['Hello, world!', 1];

log(...params); // 1: Hello, world!

똑같이 함수의 타입을 받는 유틸리티 타입인데요. 어떤 함수가 있을때, 여기 안에다가 파라미터를 뭘 넣어야 될지 추론하고 싶을때 사용합니다.

그래서 log라는 함수를 Parameters 제네릭으로 감싸주게 되면,
LogParams에는 이렇게 첫번째에는 message, 두번째에는 userId가 들어가야 합니다.

type LogParams = Parameters<typeof log>;
// 위의 의미는 아래코드와 같습니다.
type LogParams = [message: string, userId: number]

함수의 인자 타입 또한 Parameters를 통해서 계산할 수 있는 겁니다.


  • Awaited<T>
async function fetchData(): Promise<string> {
    return "Hello, world!";
}

// fetchData 함수의 반환 타입 추론
type FetchDataType = Awaited<ReturnType<typeof fetchData>>;

const data: FetchDataType = await fetchData();
console.log(data); // "Hello, world!"

Awaited는 우리가 async function을 썼을 때, Promise를 반환하도록 되어 있어요. 그때 이 Promise라는 것보다 우리한테 중요한 건 이 안에 있는 실제 반환 값이 더 중요하겠죠.

그래서 이 반환값을 기다린 것처럼 타입을 가져와 주는게 바로 Awaited라는 타입입니다.

type FetchDataType = Awaited<ReturnType<typeof fetchData>>;
//위의 코드는 아래코드와 의미가 같습니다.
type FetchDataType = string;



Generic, Utility Type 응용하기

1. 하나의 타입으로 여러가지 타입 만들기

유틸리티 타입은 계산에 초점을 둔 타입이라고 했죠?

여기 기초가 되는 Todo 타입을 하나 만들어 봅시다.

type Todo = {
	id: number;
	title: string;
	completed: boolean;
}

그리고나서, Pick과 Omit 등을 통해서
새로운 타입을 만들어줄 수 있어요.

type TodoId = Pick<Todo, 'id'>

type CreateTodo = Pick<Todo, 'title' | 'completed'>
type CreateTodo = Omit<Todo, 'id'>

type ToggleTodo = Pick<Todo, 'id' | 'completed'>
type ToggleTodo = Omit<Todo, 'title'>

Partial Type도 이용할 수 있어요.

type EditTodo = Partial<Todo> & TodoId;

우리가 어떤 값을 Edit할 때, Todo는 Partial로 들어오고,
id는 무조건 포함되어야 합니다라는 새로운 타입을 만들 수 있어요.

이런식으로 하나의 기초타입을 만들어주고,
다른 타입들이 이 기초 타입을 통해 계산되도록
만들어주면
우리가 함수를 쓰는 것과 동일하게 좀 더 확장성있고 안전한 개발을 할 수 있게 됩니다.



2. 함수를 기반으로 타입을 빼오기

앞에서 Awaited를 할 때, ReturnType이랑 typeof를 조합해서, 이 함수의 반환 값을 추론하고 있었잖아요

이런게 왜 필요하냐?
우리가 타입을 직접 선언해서 사용해 주면 되는게 아닌가??

라이브러리나 다른 모듈을 사용하게 되면
그 함수의 반환 값의 타입을 추론해서 빠르게 사용해야 될 때가 올거에요

그래서 우리한테 타입이 명확하게 공개되어 있지 않을 때,
우리가 함수의 반환값에 의존해서 타입을 추론해야할때,
이런 함수 계산 타입을 이용해서 추론된 값을 받아오는 것을 많이 사용합니다.

profile
당근🥕

0개의 댓글