Utility type 모아보기
antutility type 들을 함수처럼 사용하기 전에 어떻게 제네릭으로 풀어낼 수 있는 지를 고민해보고 해당 타입들에 대한 예제들을 함께 다루면서 공부합니다.
예제들은 대부분 Ant Design이나 실무에서 쓰이는 type들로 구성되어 있으며, 개발하기 전에 사용되는type들을 눈에 익히기 위함이니 참고해주시면 좋을 거 같습니다.
연습 문제 )
배열의 길이를 반환하는 함수의 type을 지정하세요.
const studentNameList = ["bob", "json", "stefan"]; # 정답을 맞춰주세요! const GenericReturnFunc = <T>(arg) => { return arg.length; } const a = GenericReturnFunc(studentNameList); console.log(a)
정답 >><T extends {}>(arg: T[]): number => { return arg.length; }
Partial
Partial
export type SiteData = { articles: Articles; authors: Authors; recommendations: Recommendations; extras: Extras; icons: Icons; }; const [data, setData] = useState<Partial<SiteData>>({}); useEffect(() => { if (Object.keys(data ?? {}).length === 0 && typeof fetch !== 'undefined') { setLoading(true); fetch(`https://~~~~`) .then((result) => { setData(result); // SiteData type들이 선택적으로 data 값에 들어갈 수 있다. }); } }, []);
Partial 제네릭으로 직접 구현해보기
type Partial<T> = { # 답을 맞춰주세요 ! };
정답 >>[P in keyof T]?: T[P];
- Required type은 Partial과 반대라고 생각하면 되고, 간단하기 때문에 설명은 생략하겠습니다.
Pick
Pick<Type, Keys>: 오브젝트 타입에서 Type의 Key가 Keys에 해당하는 프로퍼티들만 모은 새로운 타입을 반환한다.
type User = { firstName: string, lastName: string, age: number } type UserName = Pick<User, "firstName" | "lastName">
Pick 제네릭으로 직접 구현해보기
# 답을 맞춰주세요 ! type MyPick<T, K> = { }
정답 >>type MyPick<T, K extends keyof T> = { P in K]: T[P] }
- K가 오브젝트 타입T의 프로퍼티 키여야 한다는 것에만 유의
Extract
Extract<Type, Union>: 여러개의 타입이 함께 존재하는 유니언 타입에서 특정 유니언 타입을 포함하는 타입들만 반환하는 타입입니다.
type T0 = Extract<"a" | "b" | "c", "a">; // "a" type T1 = Extract<"a" | "b" | "c", "a" | "b">; // "a" | "b" type T2 = Extract<string | number | (() => void), Function>; // () => void
interface UserBase { email: string image: string | null username: string } interface UserProfile { id: string email: string image: string | null isAdmin: boolean username: string reviews: string[] } type SharedUserKeys = Extract<keyof UserBase, keyof UserProfile> // 'email' | 'image' | 'username' key값들만 유니언으로 반환된다. // key와 타입을 같이 반환 type IntersectingTypes<T, U> = { [K in Extract<keyof T, keyof U>]: T[K] } type IntersectionTypes = { email: string image: string | null username: string }
Extract 제네릭으로 직접 구현해보기
type Extract<T, U> = # 정답을 맞춰주세요.
정답 >>T extends U ? T : never
Exclude
Exclude<UnionType, ExcludedMembers>: 여러개의 타입이 함께 존재하는 유니언 타입에서 특정 타입을 제거하는 유틸리티 타입이고,exclude로 제거할 수 있는 것은 하나의 타입 부터 유니언 까지 가능합니다. ( Extract과 반대 )
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 을 제네릭으로 풀어보면, 아래와 같이 표현이 가능합니다.
→ U안에 T가 존재할 경우 아무것도 반환하지 않고 없을 경우에만 T를 다시 반환한다.
type Exclude<T, U> = T extends U ? never : T
export const IconMap = { success: CheckCircleFilled, error: CloseCircleFilled, info: ExclamationCircleFilled, warning: WarningFilled, }; export type ExceptionStatusType = 403 | 404 | 500 | '403' | '404' | '500'; export type ResultStatusType = ExceptionStatusType | keyof typeof IconMap; const iconNode = React.createElement( IconMap[status as Exclude<ResultStatusType, ExceptionStatusType>], // IconMap type을 선택 );
Omit
Omit<Type, Keys>: 특정 속성만 제거한 타입을 정의합니다. ( pick의 반대 )
type mappedType = { abc: string; bcd: string; cde: string; def: string; } type mappedTypeWithOmit = Omit<mappedType,'def'> type mappedTypeWithOmit = { abc: string; bcd: string; cde: string; }
// 선언된 Route type 안에 children에 다시 Route type을 선언할 수 있고, // Omit을 활용하여 children을 제외한 type만을 받겠다고 명시. export interface Route { path: string; breadcrumbName: string; children?: Omit<Route, 'children'>[]; }
:face_with_open_eyes_and_hand_over_mouth: Extract과 Pick / Exclude과 Omit이 비슷해보이는데 차이점은 무엇인가요?
Omit 의 첫번째 예제를 Exclude로 풀어보면, 아래와 같습니다.
type something = 'abc' | 'bcd' | 'cde' | 'def'; type somethingWithExclude = Exlude<something, "def"> // 'abc' | 'bcd' | 'cde' type mappedTypeWithExclude = { [k in somethingWithExclude]: string; } // equal type mappedTypeWithExclude = { abc: string; bcd: string; cde: string; }
Exclude의 경우, union type들을 받아 특정 타입을 제외한 union type( key ) 을 다시 반환하게 됩니다.
해당 부분은 Omit 함수를 실행 했을 때 결과를 도출하기 위한 하나의 과정으로 생각하면 좋을 거 같습니다.
그래서 Omit 을 제네릭으로 풀어냈을 때, 아래와 같이 표현할 수 있습니다.
type Omit<T, K extends keyof any> = { [P in Exclude<keyof T, K>]: T[P]; } // Pick도 함께 사용 type Omit <T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>
Record
Record<Keys, Type>: 타입의 프로퍼티들을 다른 타입에 매핑시키는 데 사용될 수 있습니다.
interface CatInfo { age: number; breed: string; } type CatName = "miffy" | "boris" | "mordred"; const cats: Record<CatName, CatInfo> = { miffy: { age: 10, breed: "Persian" }, boris: { age: 5, breed: "Maine Coon" }, mordred: { age: 16, breed: "British Shorthair" }, };
export declare type DefaultRecordType = Record<string, any>; // equal type DefaultRecordType = { [x: string]: any; }
해당 함수를 제네릭으로 풀어보면 아래와 같습니다.
type Record<T, K> = { [P in T] : K } // 오브젝트 키값에 사용될 수 있는 type까지 넣어주면 안정성이 더욱 올라간다. type Record<K extends string | number | symbol, T> = { [P in K]: T; }
ReturnType
ReturnType: 주어진 제네릭 타입 T의 return type을 할당합니다.
ReturnType을 제네릭으로 풀어보면 아래와 같습니다.
type ReturnType<T extends (...args : any) => any > = T extends (...args : any) => infer R ? R : any;
함수 타입에서 리턴 타입이 추론되는 경우 리턴 타입을 반환하고 그렇지 않은 경우 그냥 제너릭 타입 그대로 반환한다.
여기서,

infer라는 개념이 쓰이는데 infer는 말그대로type을 추론한다는 뜻입니다!
위에 ReturnType의 반환되는 부분을 해석하면, 아래와 같습니다.
T extends (...args : any) => infer R ? R : any;
T는 함수여야 하는데 그의 반환타입을 R이라고 이름 붙이고 이를 뒤에서 확인 하였을 때 반환 타입이 True이면 R을 반환하고 아니면 any를 반환한다.
# 예제 type T0 = ReturnType<() => string>; // string type T1 = ReturnType<(s: string) => void>; // void interface Payload { foo: string; bar: string; } const fooBarCreator = (): Payload => ({ foo: "foo", bar: "bar" }); type IFooBarCreator = ReturnType<typeof fooBarCreator>; // typeof를 써주는건 제네릭에 타입을 넣어줘야하기 때문 // type IFooBarCreator = Payload
#redux예제
useSelector를 사용해서 redux에서 특정값을 조회할 수 있습니다.
const data = useSelector( (state :RootState) => state );
이때, state가 어떻게 구성되어 있는 지 파악해서 직접 리턴되는 type을 지정해줄 수 있지만 다 확인할 수 없으니
RootState 타입을 불러와서 state의 타입을 지정하게해줍니다.
export type RootState = ReturnType<typeof combinedReducer>; export const useTypedSelector: RootState = useSelector;
이때 RootState은 reducer(현재 상태와 액션객체를 파라미터로 받아와서 새로운 상태를 반환해주는 함수)의 return 타입을 받게 되는 것이다.