Notice
Recent Posts
Recent Comments
Link
관리 메뉴

윤일무이

[TypeScript] 유틸리티 타입 본문

❔ TypeScript/💭 한 입 크기 TypeScript

[TypeScript] 유틸리티 타입

썸머몽 2023. 12. 30. 20:47
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