Frontend/TypeScript

[TypeScript] 9. 유틸리티 타입

ayeongjin 2025. 1. 31. 00:51

유틸리티 타입

타입스크립트가 자체적으로 제공하는 특수한 타입 (제네릭, 맵드 타입, 조건부 타입 등의 타입 조작 기능을 이용해 실무에서 자주 사용되는 유용한 타입들을 모아 놓은 것을 의미)

 

타입스크립트가 제공하는 다양한 유틸리티 타입들

Documentation - Utility TypesTypes which are globally included in TypeScripthttps://www.typescriptlang.org/docs/handbook/utility-types.html

 

Documentation - Utility Types

Types which are globally included in TypeScript

www.typescriptlang.org

 

 

  • 가장 많이 사용되는 유틸리티 타입들

 


 

1. Partial<T>

Partial은 부분적인 또는 일부분의 라는 뜻으로 특정 객체 타입의 모든 프로퍼티를 선택적 프로퍼티로 변환한다. 따라서 기존 객체 타입에 정의된 프로퍼티들 중 일부분만 사용할 수 있도록 도와주는 타입입니다.

 

❗️문제상황

  • 다음과 같이 게시글 하나를 표현하는 타입을 먼저 선언
interface Post {
  title: string;
  tags: string[];
  content: string;
  thumbnailURL?: string;
}
  • 다음으로 임시 저장 기능이 필요하다고 가정하면 다음과 같이 임시 저장된 게시글을 변수로 저장할 수 있어야 함.
interface Post {
  title: string;
  tags: string[];
  content: string;
  thumbnailURL?: string;
}

const draft: Post = { // ❌ tags 프로퍼티가 없음
  title: "제목은 나중에 짓자...",
  content: "초안...",
};
  • 위와 같이 게시글의 일부 정보가 아직 설정되어 있지 않은 임시 저장 게시글의 경우에도 변수에 저장할 수 있어야 하는데 해당 변수를 Post 타입으로 정의하면 오류 발생
    • 그렇다고 임시 저장 게시글 기능을 위해 Post 타입의 모든 프로퍼티를 선택적 프로퍼티로 설정하는 것도 불가능
    • 진짜 작성이 완료되어 화면에 렌더링 될 게시글들은 이 모든 프로퍼티를 진짜 다 가지고 있어야 하기 때문

 

✅Partial<T> 타입으로 문제 해결하기

interface Post {
  title: string;
  tags: string[];
  content: string;
  thumbnailURL?: string;
}

const draft: Partial<Post> = {
  title: "제목 나중에 짓자",
  content: "초안...",
};
  • Partial<T> 타입은 타입 변수 T로 전달한 객체 타입의 모든 프로퍼티를 다 선택적 프로퍼티로 변환
  • 따라서 Partial<Post> 타입은 모든 프로퍼티가 선택적 프로퍼티가 된 Post 타입과 같다.

 

💡Partial<T> 직접 구현하기

1. 하나의 타입 변수 T를 사용하는 제네릭 타입 선언

type Partial<T> = any;

2. T에 할당된 객체 타입의 모든 프로퍼티를 선택적 프로퍼티로 바꿔준다.

  • 기존 객체 타입을 다른 타입으로 변환하는 타입은 맵드 타입이므로 맵드 타입을 이용
type Partial<T> = {
  [key in keyof T]?: T[key];
};

 

2. Required<T>

Required는 우리말로 필수의, 필수적인 이라는 뜻으로 특정 객체 타입의 모든 프로퍼티를 필수(선택적이지 않은) 프로퍼티로 변환한다.

 

❗️문제상황

  • 썸네일이 반드시 있어야 하는 게시글이 하나 필요하다고 가정
interface Post {
  title: string;
  tags: string[];
  content: string;
  thumbnailURL?: string;
}

(...)

// 반드시 썸네일 프로퍼티가 존재해야 하는 게시글
const withThumbnailPost: Post = {
  title: "한입 타스 후기",
  tags: ["ts"],
  content: "",
  thumbnailURL: "https://...",
};
  • 그런데 Post 타입의 thumbnailURL 프로퍼티가 현재 선택적 프로퍼티로 설정되어 있기 때문에 다음과 같이 삭제한다고 해도 타입 오류가 발생하지 않음
interface Post {
  title: string;
  tags: string[];
  content: string;
  thumbnailURL?: string;
}

(...)

const withThumbnailPost: Post = {
  title: "한입 타스 후기",
  tags: ["ts"],
  content: "",
  // thumbnailURL: "https://...",
};

 

✅Required<T> 타입으로 문제 해결하기

interface Post {
  title: string;
  tags: string[];
  content: string;
  thumbnailURL?: string;
}

(...)

const withThumbnailPost: Required<Post> = { // ❌
  title: "한입 타스 후기",
  tags: ["ts"],
  content: "",
  // thumbnailURL: "https://...",
};
  • Required<Post>는 Post 타입의 모든 프로퍼티가 필수 프로퍼티로 변환된 객체 타입이므로 thumbnailURL 프로퍼티를 생략하면 이제 오류가 발생하게 된다.

 

💡Required<T> 타입 구현하기

1. 일단 기존의 모든 프로퍼티를 포함하는 제네릭 맵드 타입으로 만든다.

type Required<T> = {
  [key in keyof T]: T[key];
};

2. 모든 프로퍼티를 필수 프로퍼티로 만든다는 말은 반대로 바꿔보면 모든 프로퍼티에서 ‘선택적’ 이라는 기능을 제거하는 것 과 같다. 따라서 다음과 같이 -?를 프로퍼티 이름 뒤에 붙여준다.

  • -?는 ?가 붙어있는 선택적 프로퍼티가 있으면 ?를 제거하라는 의미
type Required<T> = {
  [key in keyof T]-?: T[key];
};

 

3. Readonly

Readonly는 우리말로 읽기 전용 이라는 뜻으로 특정 객체 타입의 모든 프로퍼티를 읽기 전용 프로퍼티로 변환

 

❗️문제상황

  • 절대 내부를 수정할 수 없는 보호된 게시글이 하나 필요하다고 가정
interface Post {
  title: string;
  tags: string[];
  content: string;
  thumbnailURL?: string;
}

(...)

const readonlyPost: Post = {
  title: "보호된 게시글입니다.",
  tags: [],
  content: "",
};

readonlyPost.content = '해킹당함';
  • 그러나 Post 타입의 모든 프로퍼티가 다 readonly 설정이 안되어 있기 때문에 지금은 수정을 방지하지 못한다.

 

✅Readonly<T>로 문제 해결하기

interface Post {
  title: string;
  tags: string[];
  content: string;
  thumbnailURL?: string;
}

(...)

const readonlyPost: Readonly<Post> = {
  title: "보호된 게시글입니다.",
  tags: [],
  content: "",
};

readonlyPost.content = '해킹당함'; // ❌
  • Readonly<Post>는 Post 타입의 모든 프로퍼티를 readonly(읽기 전용) 프로퍼티로 변환
    • 따라서 점표기법을 이용해 특정 프로퍼티의 값을 수정하려고 하면 오류를 발생시킨다.

 

💡Readonly<T> 구현하기

type Readonly<T> = {
  readonly [key in keyof T]: T[key];
};

 


 

4. Pick<T, K>

Pick은 우리말로 뽑다, 고르다 라는 뜻으로 특정 객체 타입으로부터 특정 프로퍼티 만을 골라내는 타입

 

ex)

예를 들어 Pick 타입에 T가 name, age가 있는 객체 타입이고 K가 name 이라면 결과는 name만 존재하는 객체 타입이 된다.

 

❗️문제상황

  • 옛날에 작성된 포스트가 하나 존재한다고 가정
interface Post {
  title: string;
  tags: string[];
  content: string;
  thumbnailURL?: string;
}

(...)

const legacyPost: Post = { // ❌
  title: "",
  content: "",
};
  • 이때 legacyPost에 저장되어 있는 게시글은 태그나 썸네일 기능이 추가되기 이전에 만들어진 게시글이라고 가정한다.
  • 그런데 이 변수를 Post 타입으로 설정하면 tags 프로퍼티가 존재하기 때문에 오류 발생

 

✅Pick<T, K>로 문제 해결하기

interface Post {
  title: string;
  tags: string[];
  content: string;
  thumbnailURL?: string;
}

(...)

const legacyPost: Pick<Post, "title" | "content"> = {
  title: "",
  content: "",
};
// 추출된 타입 : { title : string; content : string }
  • 변수 legacyPost의 타입으로 Pick<Post, "title" | "content">을 정의
  • 따라서 이때 타입변수 T에는 Post가 타입변수 K에는 “title” | “content” 이 각각 할당
  • 그럼 Post 타입으로부터 “title”과 “content” 프로퍼티만 쏙 뽑아낸 객체 타입이 된다.

 

💡Pick<T, K> 타입 구현하기

1. 이번에도 객체 타입을 변형하는 타입이므로 맵드 타입을 이용해 만들 수 있다.

2. 일단 2개의 타입 변수 T와 K를 사용하는 타입이므로 다음과 같이 정의

type Pick<T, K> = any;

3. 다음으로 T로 부터 K 프로퍼티만 뽑아낸 객체 타입을 만들어야 하므로 다음과 같이 맵드 타입으로 정의

type Pick<T, K> = {
  [key in K]: T[key];
};

4. K가 T의 key로만 이루어진 String Literal Union 타입임을 보장해 줘야 하므로 다음과 같이 제약을 추가

type Pick<T, K extends keyof T> = {
  [key in K]: T[key];
};

 

5. Omit<T, K>

Omit은 우리말로 생략하다, 빼다 라는 뜻이다. 따라서 특정 객체 타입으로부터 특정 프로퍼티 만을 제거하는 타입

 

ex)

예를 들어 Omit 타입에 T가 name, age가 있는 객체 타입이고 K가 name 이라면 결과는 name을 제외하고 age 프로퍼티만 존재하는 객체 타입이 된다.

 

❗️문제상황

  • 제목이 없는(title 프로퍼티가 생략된) 게시글도 존재할 수 있다고 가정
interface Post {
  title: string;
  tags: string[];
  content: string;
  thumbnailURL?: string;
}

(...)

const noTitlePost: Post = { // ❌
  content: "",
  tags: [],
  thumbnailURL: "",
};
  • title 프로퍼티가 없으면 오류 발생

 

✅Omit<T, K>로 문제 해결하기

  • Omit을 이용해 Post 타입으로부터 title 프로퍼티를 제거한 타입으로 변수의 타입을 정의해 준다.
const noTitlePost: Omit<Post, "title"> = {
  content: "",
  tags: [],
  thumbnailURL: "",
};

 

💡Omit<T, K> 구현하기

1. 2개의 타입 변수를 사용하는 제네릭 타입이므로 일단 다음과 같이 정의

type Omit<T, K> = any;

2. 앞서 Pick 타입에서 했던 것 과 같이 K에 제약을 추가

type Omit<T, K extends keyof T> = any;

3. 이때 앞서 만든 Pick 타입을 이용해 다음과 같이 완성

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

 

구현 ex)

  1. T는 Post, K는 ‘title’ 이라고 가정
  2. 이때 keyof T는 ‘title’ | ‘content’ | ‘tags’ | ‘thumbnailURL’이므로 Pick<T, Exclude<keyof T, K>>은 Pick<Post, Exclude<'title' | 'content' | 'tags' | 'thumbnailURL' , 'title>> 이 된다.
  3. Exclude 타입은 2개의 타입 변수를 할당받는데 T로부터 K를 제거한다. 따라서 한번 더 변환하면Pick<Post, 'content' | 'tags' | 'thumbnailURL'> 이 된다.
  4. 결과는 Post에서 content, tags, thubmnailURL 프로퍼티만 존재하는 객체 타입이 된다.
  5. 따라서 K에 전달한 ‘title’이 제거된 타입을 얻을 수 있다.

 

6. Record<K, V>

 

❗️문제상황

  • 화면 크기에 따라 3가지 버전의 썸네일을 지원한다고 가정
    • Thumbnail 타입을 별도로 정의
type Thumbnail = {
  large: {
    url: string;
  };
  medium: {
    url: string;
  };
  small: {
    url: string;
  };
};
  • 그런데 여기에 watch 버전이 또 추가되어야 한다고 가정
    • 그럼 다음과 같이 똑같이 생긴 프로퍼티를 하나 더 추가해줘야 하므로 중복 코드 발생
type Thumbnail = {
 (...)
  watch: {
    url: string;
  };
};

 

✅Record<K, V> 타입으로 문제 해결하기

  • 다음과 같이 K에는 어떤 프로퍼티들이 있을지 String Literal Union 타입을 할당하고 V에는 프로퍼티의 값 타입을 할당
type Thumbnail = Record<
  "large" | "medium" | "small",
  { url: string }
>;
  • 위 Record 타입은 K에는 “large” | “medium” | “small”이 할당되었으므로 large, medium, small 프로퍼티가 있는 객체 타입을 정의
  • 그리고 각 프로퍼티 value의 타입은 V에 할당한 { url : stirng } 이 된다.

 

💡Record<K, V> 구현하기

type Record<K extends keyof any, V> = {
  [key in K]: V;
};

 


 

7. Exclude<T, K>

T로부터 U를 제거하는 타입

 

✅ Exclude<T, K> 타입

type A = Exclude<string | boolean, string>;
// boolean

 

💡Exclude<T, K> 구현하기

type Exlcude<T, U> = T extends U ? never : T;

 

8. Extract<T, K>

T로 부터 U를 추출하는 타입

 

✅ Extract<T, K> 타입

type B = Extract<string | boolean, boolean>;
// boolean

 

💡Extract<T, K> 구현하기

type Extract<T, U> = T extends U ? T : never;

 

9. ReturnType<T>

타입변수 T에 할당된 함수 타입의 반환값 타입을 추출하는 타입

 

✅ ReturnType<T> 타입

type ReturnType<T extends (...args: any) => any> = T extends (
  ...agrs: any
) => infer R
  ? R
  : never;

function funcA() {
  return "hello";
}

function funcB() {
  return 10;
}

type ReturnA = ReturnType<typeof funcA>;
// string

type ReturnB = ReturnType<typeof funcB>;
// number