
- 예제로 알아보는 TypeScript 유틸리티 타입
유틸리티 타입이 뭐지?
유틸리티 타입은 TypeScript 내장 타입으로, 기존 타입에서 새로운 타입을 생성하는 데 사용됩니다.
라이브러리를 만들 때 자주 사용하지만, 유틸리티 타입이 뭔지 모르는 분들도 Partial<Foo>나 Record<string, string> 같은 건 본 적이 있을 거예요.
어떤 종류가 있는지는 문서를 참고하면 되고, 오늘은 어떤 경우에 유용한지, 몇 가지 주요 유틸리티 타입을 분류해서 소개해 드릴게요.
TypeScript가 제공하는 타입: 만드는 것
Record<Keys, Type>
Record는 Keys를 키로 하는 프로퍼티를 가지고, 그 프로퍼티 값의 타입은 Type인 타입을 생성합니다.
주로 데이터를 저장하는 객체의 타입을 정의할 때 사용합니다.
type FooRecord = Record<"prop1" | "prop2", string>;
// → { prop1: string, prop2: string }
위와 같은 경우에는 직접 정의하는 게 더 편할 수 있지만, 키 집합이 이미 정의되어 있는 경우 유용합니다.
interface BarData {
prop1: string;
prop2: number;
prop3: Date;
}
type BarDataErrors = Record<keyof BarData, string>
// → { prop1: string, prop2: string, prop3: string}
string처럼 넓은 타입을 지정할 수도 있습니다.
type StringRecord = Record<string, string>;
원래 Record는
type Record<K extends keyof any, T> = {
[P in K]: T;
};
처럼 간단하기 때문에 직접 작성해도 되지만, 의도를 명확히 표현할 수 있고 간결하게 작성할 수 있습니다.
TypeScript가 제공하는 타입: 주로 프로퍼티를 가공하는 데 사용하는 것
Pick<Type, Keys> / Omit<Type, Keys>
Pick은 Type의 프로퍼티 중 Keys에 해당하는 것만 추출하여 새로운 타입을 만듭니다.
Omit은 Type의 프로퍼티 중 Keys에 해당하는 것을 제외한 나머지를 추출하여 새로운 타입을 만듭니다.
즉, 화이트리스트와 블랙리스트라고 생각하면 됩니다.
이러한 유틸리티 타입은 다양하게 사용되지만, 기존 함수의 래퍼를 만들 때 유용합니다.
예를 들어, Button이라는 React 컴포넌트가 size라는 prop을 받는다고 할 때, size를 "large"로 고정한 LargeButton이라는 컴포넌트를 만들고 싶은 경우를 생각해 보세요.
이때 LargeButton의 props 타입을 Button에서 size 프로퍼티를 제외한 것으로 만드는 것으로, Button과 동일하지만 size는 전달할 수 없다는 것을 명확히 할 수 있습니다.
type LargeButtonProps = Omit<ButtonProps, "size">;
function LargeButton(props: LargeButtonProps) {
return <Button {...props} size="large" />
}
Extract<Type, Union> / Exclude<UnionType, ExcludedMembers>
Extract는 Type에서 Union에 할당 가능한 것을 추출하여 새로운 타입을 만들고, Exclude는 UnionType에서 ExcludedMembers에 할당 가능한 것을 제외한 나머지를 추출하여 새로운 타입을 만듭니다.
이것은 discriminated union과 함께 사용합니다. 예를 들어, type: "success"인 경우는 정상, type: "error"인 경우는 에러인 Result 객체를 생각해 보세요.
interface ResultSuccess {
type: "success";
data: Record<string, string>;
}
interface ResultError {
type: "error";
errorMessage: string;
}
// 기존 라이브러리가 합쳐진 타입만 export되어 있거나
export type Result = ResultSuccess | ResultError;
이러한 경우, Extract나 Exclude를 사용하면 제한된 타입을 얻을 수 있습니다.
type SuccessType = Extract<Result, {type: "success"}>
// ResultSuccess
type NonSuccessType = Exclude<Result, {type: "success"}>
// ResultError
Partial<Type> / Required<Type>
Partial은 프로퍼티를 선택적으로 지정 가능하게 만들고, Required는 프로퍼티를 필수로 지정하게 만듭니다.
Partial은 예를 들어 프로퍼티를 부분적으로 받아서 객체를 업데이트하는 함수를 만들 때 사용할 수 있습니다.
function updateFoo(foo: Foo, fields: Partial<Foo>) {
return {...foo, ...fields };
}
원래 모든 프로퍼티가 required인 것은 아니기 때문에, Partial과 비교했을 때 Required를 단독으로 사용하는 경우는 적지만, 다른 유틸리티 타입과 함께 사용할 수 있습니다.
예를 들어, Pick과 함께 사용하여 명확하게 사용할 수 있습니다.
interface Foo {
name?: string;
age?: number;
description?: string;
}
type Bar = Required<Pick<Foo, "name" | "age">>;
NonNullable
NonNullable은 Type에서 null과 undefined를 제거합니다.
type NullableType = string | null;
type NonNullType = NonNullable<NullableType>;
// string
이것은 매우 간단합니다.
TypeScript가 제공하는 타입: 함수 관련
함수 타입에 대한 유틸리티 타입은 함수를 받아서 어떤 식으로든 변경하는 함수에 필수적이지만, 특정 함수가 이미 정의되어 있는 경우에도 다음과 같은 방법으로 사용할 수 있습니다.
ReturnType
ReturnType은 함수의 타입에서 반환 값의 타입을 얻습니다.
ReturnType은 함수는 알고 있지만 타입에 접근할 수 없거나 정의되지 않은 경우, 예를 들어 factory 같은 함수에서 명시적인 타입이 없는 경우, 해당 값을 중계하는 등의 작업을 위해 타입이 필요할 때 유용합니다.
function createFoo() {
return {
a: "abc",
b: 123
};
}
type CreateFooReturn = ReturnType<typeof createFoo>;
// { a: string, b: number }
Parameters
Parameters는 함수의 인수의 타입을 얻습니다.
래퍼 함수를 만들 때, 기존 함수의 타입을 그대로 유지하면서 인수를 추가하고 싶을 때 유용합니다.
function foo(a: string|number) {
return a;
}
type FooParameters = Parameters<typeof foo>;
function wrappedFoo(a: FooParameters[0], b: string) {
other(b);
return foo(a);
}
Awaited
비동기 함수의 경우 return type이 Promise가 되지만, 그대로 사용하기는 까다롭습니다.
이럴 때 Awaited를 사용하면 Promise를 풀 수 있습니다.
interface Foo {
a: string;
b?: number;
}
export async function createFoo(): Promise<Foo> {
return {
a: "bar",
b: 123
};
}
///
type RequiredFoo = Required<Awaited<ReturnType<typeof createFoo>>>;
// {a: string, b: number}
직접 만들기
유틸리티 타입도 TypeScript로 정의된 타입일 뿐이므로, 이러한 타입을 직접 만들 수도 있습니다.
예를 들어, 부분적으로 선택적으로 지정 가능하게 만드는 PartialPartially라는 유틸리티 타입을 만들면 다음과 같습니다.
// 이미 정의된 Foo
interface Foo {
name: string;
description: string;
startAt: Date;
finishedAt: Date;
}
type PartialPartially<T, K extends keyof T> = Partial<Pick<T, K>> & Pick<T, keyof Omit<T, K>>;
// 완료되지 않았을 수도 있는(finish되지 않았을 수도 있는) 타입을 만듭니다.
type FooMayUnfinished = PartialPartial<Foo, "finishedAt">;
이처럼 타입 조작 방법과 타입 조작 자체를 분리해 놓으면 나중에 봤을 때 훨씬 이해하기 쉽습니다.
마무리
유틸리티 타입은 사용하지 않는 것이 가장 좋지만, 어쩔 수 없는 경우에는 알고 있는지 여부에 따라 타입 처리의 난이도가 달라질 수 있습니다.
유틸리티 타입을 사용하여 타입 조작을 쉽게 해 보세요.
'Javascript' 카테고리의 다른 글
| 웹 개발의 핵심 개념들 : DOM과 가상 DOM, 모듈 번들러, 트랜스파일러, 바벨, 모듈, ESM, 비동기 처리, 그리고 프로미스 (0) | 2024.06.18 |
|---|---|
| TypeScript 타입 추론의 새로운 지평: infer의 매력 (0) | 2024.06.16 |
| Deno v2를 향하여 - Deno v2, deno_std v1, Fresh v2에 대하여 (0) | 2024.06.10 |
| PPR은 island 아키텍처인가? (0) | 2024.06.10 |
| Next.js와 PPR: 프리렌더링의 신시대와 SSR/SSG 논쟁의 종결 (1) | 2024.06.10 |