728x90
유틸리티 타입
- 제네릭, 맵드 타입, 조건부 타입 등 타입 조작 기능을 이용해 실무에서 자주 사용되는 타입을 미리 만들어 놓은 것
- 여러 가지 유틸리티 타입이 있다. (참고 링크)
맵드 타입 기반의 유틸리티 타입 1 : Partial, Required, Readonly
Partial
- 특정 객체 타입의 모든 프로퍼티를 '선택적 프로퍼티'로 바꿔주는 타입
- 맵드 타입과 인덱스드 엑세스(참고 링크)를 활용해 구현할 수 있다.
interface Post {
title: string;
tags: string[];
content: string;
thumbnailURL?: string;
}
// const draft: Post = {
// title: '제목1',
// content: '초안',
// };
const draft: Partial<Post> = {
title: '제목1',
content: '초안',
};
- Post라는 인터페이스가 있을 때, Post 타입을 갖는 객체 draft를 선언하면 에러가 발생한다.
- thumbnailURL은 선택적 프로퍼티라 없어도 되지만, tags는 필수 프로퍼티라 에러가 발생하는 것이다.
- Post 타입의 부분에만 해당하는 변수를 선언하기 위해서 마지막 코드처럼 수정해준다.
type Partial<T> = {
[key in keyof T]?: T[key];
};
- 직접 구현하기 위해서 맵드 타입과 인덱스드 엑세스를 활용한다.
- 맵드 타입은 기존 타입의 속성을 변환하는데 사용한다. 여기서는 key in keyof T 구문을 통해 타입 변수 T의 모든 속성을 반복한다.
- 그리고 해당 속성이 '선택적 프로퍼티'가 되기 위해 ?를 추가하고, 값으로는 T[key]를 추가한다.
- 따라서 Partial<T>는 타입 변수 T의 모든 속성을 가지지만 각 속성은 선택적 프로퍼티로 취급된다.
Required
- 특정 객체 타입의 모든 프로퍼티를 '필수 프로퍼티'로 바꿔주는 타입
- 맵드 타입과 인덱스드 엑세스(참고 링크)를 활용해 구현할 수 있다.
const withThumbnailPost: Required<Post> = {
title: '후기',
tags: ['ts'],
content: '',
thumbnailURL: 'https...',
};
- 이번에 만드는 변수는 썸네일을 포함한 포스트라서 반드시 thunbnailURL이 필요하다고 가정해보자.
- Post 타입에서는 해당 속성이 '선택적 프로퍼티'지만 여기서는 필수 프로퍼티가 되어야 한다.
- 따라서 모든 프로퍼티가 필수 프로퍼티가 되기 위해 Required를 작성해준다.
type Required<T> = {
[key in keyof T]-?: T[key]; // ?를 빼겠다는 뜻
};
- 직접 구현할 때는 ?(선택적 프로퍼티)를 빼겠다는 뜻으로 -?를 붙여준다.
- 이렇게 하면 Required<T>는 타입 변수 T의 모든 속성을 가지지만 각 속성은 필수 프로퍼티로 취급된다.
Readonly
- 특정 객체 타입에서 모든 프로퍼티를 읽기 전용 프로퍼티로 만들어주는 타입
- 맵드 타입과 인덱스드 엑세스(참고 링크)를 활용해 구현할 수 있다.
const readonlyPost: Readonly<Post> = {
title: '보호된 게시글',
tags: [],
content: '',
};
// readonlyPost.title = '수정' // 에러
- Readonly 유틸리티로 title을 수정할 수 없게 된다.
type Readonly<T> = {
readonly [key in keyof T]: T[key];
};
- 직접 구현할 때는 앞에 readonly 키워드만 붙여주면 된다.
- 이렇게 Readonly<T>는 타입 변수 T의 모든 속성을 가지지만 각 속성은 읽기 전용으로 취급되어 수정할 수 없게 된다.
맵드 타입 기반의 유틸리티 타입 2 : Pick, Omit, Record
Pick
- 객체 타입으로부터 특정 프로퍼티만 뽑아내는 타입
interface Post {
title: string;
tags: string[];
content: string;
thumbnailURL?: string;
}
const legacyPost: Pick<Post, 'title' | 'content'> = {
title: '옛날 글',
content: '옛날 콘텐츠',
};
- 위에서 썼던 Post 타입을 그대로 쓸 때, 만약에 옛날 버전이라 tag랑 thumbnailURL은 없는 변수가 필요하다고 가정해보자.
- Partical로 모두 선택적 프로퍼티를 만든 후 title, content만 써도 문제는 없겠지만 적합한 타입은 아니라고 생각이 된다.
- 이 때 필요한 title, content 프로퍼티만 뽑아내는 유틸리티 Pick을 사용하면 된다.
- Post 타입에서 'title' | 'content'만 뽑아내겠다는 뜻으로 Pick<T, K>를 사용한다.
type Pick<T, K extends keyof T> = {
// K extends 'title' | 'tags' | 'content' | 'thumbnailURL'
// 'title' | 'content' extends 'title' | 'tags' | 'content' | 'thumbnailURL' (O)
// number extends 'title' | 'tags' | 'content' | 'thumbnailURL' (X)
[key in K]: T[key];
};
- 실제로 구현할 때는 제네릭 타입 K가 타입 T의 키 중 하나로 제한된다는 뜻으로 extends를 써준다!
- keyof T는 타입 T의 모든 속성 키를 나타내는데, 이 때 K는 T 안에 있는 것만 가질 수 있도록 제한해주어야 하기 때문이다.
- 참고로 number 같이 속성이 아니라 타입 자체가 들어와 버리면 제약을 위반하게 된다. (number가 T의 속성 키가 될 수 없기에)
Omit
- 객체 타입으로부터 특정 프로퍼티를 제거하는 타입
- 만약에 title이 없는 변수 noTitlePost를 만든다면 기존 Post 타입에서 title을 제거해야 한다.
const noTitlePost: Omit<Post, 'title'> = {
content: '',
tags: [],
thumbnailURL: '',
};
- 이 때 Pick으로 tags, content, thumbnailURL만 뽑아서 쓸 수도 있지만, 프로퍼티가 만약 3개가 아니라 훨씬 많다면 일일이 뽑아내기 어려워진다.
- 따라서 특정 프로퍼티, 즉 title만 빼내는 유틸리티 Omit을 사용하면 된다.
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
// T = Post, K = 'title'
// Pick<Post, Exclude<keyof Post, 'title'>>
// Pick<Post, Exclude<'title' | 'tags' | 'content' | 'thumbnailURL', 'title'>>
// Pick<Post, 'content' | 'tags' | 'thumbnailURL'>
- 구현 할 때는 T, K 2가지를 쓰는데 여기서도 K가 T에 속해 있는 속성 값임을 제한해줘야 한다.
- 또 Exclude를 쓰면 제거를 해주는데, [type Exclude<T, U> = T extends U ? never : T] 이렇게 공집합으로 만들어준다.
- Exclude는 TS의 타입 유틸리티로, 첫 번째 매개변수에서 두 번째 매개변수에 해당하는 것을 제외한 타입을 생성한다.
- 즉 keyof T에서 K에 해당하는 것을 제외한다. 이 때 T는 Post로 Post 프로퍼티 중 K(title)에 해당하는 것이 제외된다.
Record
- 썸네일 기능을 업데이트 할 때, 3가지 버전의 썸네일을 지원하려고 한다.
type Thumbnail = {
large: {
url: string;
};
medium: {
url: string;
};
small: {
url: string;
};
};
- 이 때 watch라든지 phone이라든지 새로운 프로퍼티가 추가될 때마다 값으로 객체를 추가해줘야 하는 번거로움이 있다.
type Thumbnail = Record<
'large' | 'medium' | 'small' | 'watch',
{ url: string; size: number }
>;
- Record를 사용하면 첫 번째 타입 변수로는 객체의 프로퍼티 키를 유니언으로, 두 번째 타입 변수는 해당 키들의 밸류값을 받는다.
- 따라서 large, medium, small, watch 처럼 객체의 프로퍼티 키가 유니언으로 들어오고, 밸류 값으로 url: string; size: number가 들어온다.
- 이렇게 하면 불필요한 하드코딩을 줄일 수 있게 된다.
type Recode<K extends keyof any, V> = {
[key in K]: V;
};
- 구현 시 T, K처럼 타입 변수가 아니라 Key, Value를 넣어준다고 보면 된다.
- 이 때 K에 들어오는 타입이 뭔지는 모르겠지만, 어떤 객체의 프로퍼티 키 타입이라고 제약을 걸어준다.
- 그래서 어떤 객체 타입(any)의 프로퍼티 키라는 뜻으로 K extends keyof any가 들어온다.
맵드 타입 기반의 유틸리티 타입 3: Exclude, Extrack, ReturnType
Exclude
- Exclude<T, U>
- T에서 U를 제거하는 타입
type Exclude<T, U> = T extends U ? never : T;
type A = Exclude<string | boolean, boolean>;
// string | never
// string
Extract
- Extract<T, U>
- T에서 U를 추출하는 타입
type Extract<T, U> = T extends U ? T : never;
type B = Extract<string | boolean, boolean>;
// never | boolean
// boolean
ReturnType
- ReturnType<T>
- 함수의 반환값 타입을 추출하는 타입
type ReturnType<T extends (...args: any) => any> = T extends (
...args: any
) => infer R
? R
: never;
function funcA() {
return 'hello';
}
function funcB() {
return 10;
}
type ReturnA = ReturnType<typeof funcA>;
type ReturnB = ReturnType<typeof funcB>;
- ReturnA, ReturnB는 각각 string, number라고 추론된다.
- 직접 ReturnType을 구현해보면 이전에 했던 infer가 더욱 잘 이해된다.
- 함수 타입 T를 확인해, T가 함수 타입인 경우 해당 함수의 반환 타입을 R로 추론한다.
- 만약 아니라면 never를 반환한다.
- 함수 안에는 어떤 타입의 인자든, 몇 개든 들어올 수 있기 때문에 나머지 매개변수, any로 받아준다.
- 즉 함수 타입(ReturnA의 경우 string이 T가 됨)이 정상적으로 추론되려면 서브 타입이어야 하는데, 이를 같은 타입인 string이라고 추론해 조건부 추론이 가능해진다.
**출처: 한 입 크기로 잘라먹는 타입스크립트 (인프런, 이정환 강사님)
728x90
'❔ TypeScript > 💭 한 입 크기 TypeScript' 카테고리의 다른 글
[TypeScript] 타입 스크립트에서 외부 라이브러리 사용하기 (0) | 2024.01.02 |
---|---|
[TypeScript] 타입스크립트 리액트 시작하기 (0) | 2024.01.02 |
[TypeScript] 조건부 타입 (1) | 2023.12.30 |
[TypeScript] 타입 조작 (0) | 2023.12.22 |
[TypeScript] 프로미스와 제네릭 (0) | 2023.12.22 |