✅ 데이터를 이용해 간단한 계산을 하는 함수들을 Utility Function이라고 부르는 것처럼 타입을 통해 간단한 계산을 수행해 주는 타입을 유틸리티 타입!이라고 합니다.
-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과 정반대의 동작을 해요. <원본타입, 우리가 빼고 싶은 타입>
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;
유틸리티 타입은 계산에 초점을 둔 타입이라고 했죠?
여기 기초가 되는 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는 무조건 포함되어야 합니다라는 새로운 타입을 만들 수 있어요.
이런식으로 하나의 기초타입을 만들어주고,
다른 타입들이 이 기초 타입을 통해 계산되도록 만들어주면
우리가 함수를 쓰는 것과 동일하게 좀 더 확장성있고 안전한 개발을 할 수 있게 됩니다.
앞에서 Awaited를 할 때, ReturnType이랑 typeof를 조합해서, 이 함수의 반환 값을 추론하고 있었잖아요
이런게 왜 필요하냐?
우리가 타입을 직접 선언해서 사용해 주면 되는게 아닌가??
라이브러리나 다른 모듈을 사용하게 되면
그 함수의 반환 값의 타입을 추론해서 빠르게 사용해야 될 때가 올거에요
그래서 우리한테 타입이 명확하게 공개되어 있지 않을 때,
우리가 함수의 반환값에 의존해서 타입을 추론해야할때,
이런 함수 계산 타입을 이용해서 추론된 값을 받아오는 것을 많이 사용합니다.