-
예제로 알아보는 TypeScript 유틸리티 타입Javascript 2024. 6. 16. 18:15
- 예제로 알아보는 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