TypeScript : 유틸리티 타입(1)

2024. 8. 12. 20:18· TypeScript
목차
  1. 유틸리티 타입이란
  2. Partial<T>
  3. Required<T>
  4. Readonly
  5. Pick<T, K>
  6. Omit<T, K>
  7. Record<K, V>
반응형

유틸리티 타입이란

유틸리티 타입이란 타입스크립트가 자체적으로 제공하는 특수한 타입들입니다. 우리가 지금까지 배웠던 제네릭, 맵드 타입, 조건부 타입 등의 타입 조작 기능을 이용해 실무에서 자주 사용되는 유용한 타입들을 모아 놓은 것을 의미합니다.

 

예를 들어 다음과 같이 Readonly<T>와 같은 유틸리티 타입을 이용해 특정 객체 타입의 모든 프로퍼티를 읽기 전용 프로퍼티로 변환할 수 있습니다.
interface Person {
  name : string;
  age : number;
}

const person : Readonly<Person> ={
  name : "홍길동",
  age : 29
}

person.name = ''
// ❌ name은 Readonly 프로퍼티입니다.

 

또는 다음과 같이 Partial<T> 유틸리티 타입을 이용해 특정 객체 타입의 모든 프로퍼티를 선택적 프로퍼티로 변환하는 것도 가능합니다.

interface Person {
  name: string;
  age: number;
}

const person: Partial<Person> = {
  name: "홍길동",
};

 

타입스크립트는 굉장히 다양한 유틸리티 타입을 제공합니다. 아래의 타입스크립트 공식문서 에서 다양한 유틸리티 타입들을 확인할 수 있습니다.

 

Documentation - Utility Types

Types which are globally included in TypeScript

www.typescriptlang.org

 

 

이 중 맵드 타입을 기반과 조건부 타입 기반의 유틸리티 타입을 살펴보고자 합니다.

 

Partial<T>

가장 처음으로 살펴볼 유틸리티 타입은 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> 타입으로 문제 해결하기

이런 상황에 다음과 같이 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> 구현하기

그럼 이번에는 Partial<T> 유틸리티 타입을 직접 구현해 보겠습니다.
천천히 하나씩 만들어보겠습니다. 일단 하나의 타입 변수 T를 사용하는 제네릭 타입인 것 만은 확실합니다.
type Partial<T> = any;

 

다음으로는 T에 할당된 객체 타입의 모든 프로퍼티를 선택적 프로퍼티로 바꿔줘야 합니다. 기존 객체 타입을 다른 타입으로 변환하는 타입은 맵드 타입이었습니다. 따라서 맵드 타입을 이용해 다음과 같이 수정합니다.

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

 

Required<T>

다음으로 살펴볼 유틸리티 타입은 Required<T> 입니다. Required는 우리말로 필수의, 필수적인 이라는 뜻으로 특정 객체 타입의 모든 프로퍼티를 필수(선택적이지 않은) 프로퍼티로 변환합니다.

 

앞서 살펴본 예제에 이어서 이번에는 썸네일이 반드시 있어야 하는 게시글이 하나 필요하다고 가정하겠습니다.

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

(...)

// 반드시 썸네일 프로퍼티가 존재해야 하는 게시글
const withThumbnailPost: Post = {
  title: "한입 타스 후기",
  tags: ["ts"],
  content: "",
  thumbnailURL: "https://...",
};

 

withThumbnailPost는 모종의 이유(마케팅 등)로 반드시 썸네일이 포함된 게시글이어야 합니다. 그런데 Post 타입의 thumbnailURL 프로퍼티가 현재 선택적 프로퍼티로 설정되어 있기 때문에 다음과 같이 실수로 주석 처리하거나 삭제한다고 해도 타입 오류가 발생하지는 않습니다.

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

(...)

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

 

우리가 원하는 것은 이 변수에 한정해 thumbnailURL을 필수 프로퍼티로 만들어 주고 싶습니다. 어떻게 할 수 있을까요?

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

바로 이런 상황에 다음과 같이 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> 타입 구현하기

Required<T> 타입도 직접 구현 해 보겠습니다. 생각해보면 앞서 만들어 본 Partial<T> 타입과 거의 유사합니다. 따라서 일단 기존의 모든 프로퍼티를 포함하는 제네릭 맵드 타입으로 만들어 주겠습니다.
type Required<T> = {
  [key in keyof T]: T[key];
};

 

그리고 나서 이제 모든 프로퍼티가 필수 프로퍼티가 되도록 만들어야 합니다. 모든 프로퍼티를 필수 프로퍼티로 만든다는 말은 반대로 바꿔보면 모든 프로퍼티에서 ‘선택적’ 이라는 기능을 제거하는 것 과 같습니다. 따라서 다음과 같이 -?를 프로퍼티 이름 뒤에 붙여주면 됩니다.

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

 

Readonly

마지막으로 살펴볼 유틸리티 타입은 Readonly<T> 입니다. Readonly는 우리말로 읽기 전용 이라는 뜻으로 특정 객체 타입의 모든 프로퍼티를 읽기 전용 프로퍼티로 변환합니다.

 

이번에는 앞서 만들던 예제에 이어서 절대 내부를 수정할 수 없는 보호된 게시글이 하나 필요하다고 가정하겠습니다.

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

(...)

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

readonlyPost.content = '해킹당함';

 

변수 readonlyPost는 보호받아야 하는 게시글로 절대 객체 내부의 값을 수정하지 못하게 막아야 한다고 가정했습니다. 그러나 Post 타입의 모든 프로퍼티가 다 readonly 설정이 안되어 있기 때문에 지금은 수정을 방지하지 못합니다

 

Readonly<T>로 문제 해결하기

이럴 때 바로 다음과 같이 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> 구현하기

Readonly<T>도 직접 구현해 보겠습니다. 앞서 Partial<T>과 Required<T> 타입을 구현해 보았다면 어렵지 않을 겁니다.
type Readonly<T> = {
  readonly [key in keyof T]: T[key];
};

 

Pick<T, K>

이번 챕터에서 가장 먼저 살펴볼 유틸리티 타입은 Pick<T, K> 타입입니다. Pick은 우리말로 뽑다, 고르다 라는 뜻입니다. 따라서 특정 객체 타입으로부터 특정 프로퍼티 만을 골라내는 그런 타입입니다. 예를 들어 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 프로퍼티가 존재하기 때문에 오류가 발생하게 됩니다.
어떻게 해야 할까요? 옛날에 작성된 게시글이 몇개나 될 지도 모르기 때문에 일일이 tags를 추가해 줄 수도 없고 그렇다고 옛 게시글들 만을 위한 타입을 별도로 만들어 줄 수도 없는 노릇입니다.

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

이럴 때 바로 다음과 같이 Pick을 이용하면 됩니다.
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> 타입 구현하기

Pick 타입을 직접 구현해 보겠습니다. 이번에도 역시 객체 타입을 변형하는 타입이므로 맵드 타입을 이용해 만들 수 있습니다.
일단 2개의 타입 변수 T와 K를 사용하는 타입이므로 다음과 같이 정의합니다.
type Pick<T, K> = any;
다음으로 T로 부터 K 프로퍼티만 뽑아낸 객체 타입을 만들어야 하므로 다음과 같이 맵드 타입으로 정의합니다.
type Pick<T, K> = {
  [key in K]: T[key];
};

 

마지막으로는 K가 T의 key로만 이루어진 String Literal Union 타입임을 보장해 주어야 합니다. 따라서 다음과 같이 제약을 추가합니다.

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

 

Omit<T, K>

다음으로 살펴볼 유틸리티 타입은 Omit<T, K> 타입입니다. Omit은 우리말로 생략하다, 빼다 라는 뜻입니다. 따라서 특정 객체 타입으로부터 특정 프로퍼티 만을 제거하는 타입입니다. 예를 들어 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> 구현하기

먼저 2개의 타입 변수를 사용하는 제네릭 타입이므로 일단 다음과 같이 정의합니다.
type Omit<T, K> = any;
그 다음 앞서 Pick 타입에서 했던 것 과 같이 K에 제약을 추가합니다.
type Omit<T, K extends keyof T> = any;​
 
그리고 이때 앞서 만든 Pick 타입을 이용해 다음과 같이 완성합니다.
 
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
 
일단 T는 Post, K는 ‘title’ 이라고 가정하겠습니다.
그럼 이때 keyof T는 ‘title’ | ‘content’ | ‘tags’ | ‘thumbnailURL’이므로 Pick<T, Exclude<keyof T, K>>은 Pick<Post, Exclude<'title' | 'content' | 'tags' | 'thumbnailURL' , 'title>> 이 됩니다.
다음으로 Exclude는 앞서 조건부 타입을 살펴볼 때 직접 만들어 본 적 있는 타입입니다. 이 타입은 2개의 타입 변수를 할당받는데 T로부터 K를 제거합니다. 따라서 한번 더 변환하면 다음과 같습니다.
Pick<Post, 'content' | 'tags' | 'thumbnailURL'>
그럼 결과는 Post에서 content, tags, thubmnailURL 프로퍼티만 존재하는 객체 타입이 됩니다. 따라서 K에 전달한 ‘title’이 제거된 타입을 얻을 수 있습니다.

 

Record<K, V>

마지막으로 살펴볼 타입은 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> 타입으로 문제 해결하기

이럴 때 바로 Record를 이용하면 됩니다. 다음과 같이 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> 구현하기

Record 타입은 다음과 같이 구현할 수 있습니다.
type Record<K extends keyof any, V> = {
  [key in K]: V;
};

 

 

출처 : 한 입 크기로 잘라먹는 타입스크립트(TypeScript)

반응형

'TypeScript' 카테고리의 다른 글

TypeScript : 타입스크립트와 리액트  (0) 2024.08.13
TypeScript : 유틸리티 타입(2)  (0) 2024.08.12
TypeScript : infer  (0) 2024.08.12
TypeScript : 분산적인 조건부 타입  (0) 2024.08.12
TypeScript : 조건부 타입  (0) 2024.08.12
  1. 유틸리티 타입이란
  2. Partial<T>
  3. Required<T>
  4. Readonly
  5. Pick<T, K>
  6. Omit<T, K>
  7. Record<K, V>
'TypeScript' 카테고리의 다른 글
  • TypeScript : 타입스크립트와 리액트
  • TypeScript : 유틸리티 타입(2)
  • TypeScript : infer
  • TypeScript : 분산적인 조건부 타입
프론이
프론이
프론이
개발 Log
프론이
전체
오늘
어제
  • 분류 전체보기 (145)
    • Javascript (20)
    • TypeScript (42)
    • React (16)
    • 면접준비 (18)
    • Node.js (1)
    • MongoDB (3)
    • 프로젝트 (3)
    • AWS (1)
    • Next.js (17)
    • Docker (6)
    • CICD (2)
    • 디자인패턴 (6)
    • 알고리즘 (3)
    • 코딩테스트 (3)
    • 기타 (2)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • hoisting
  • 웹 브라우저 URL 입력하면 생기는 일
  • react
  • 나머지 매개변수
  • useParams()
  • 구조분해할당
  • TypeScript
  • mobx
  • async/await
  • 호이스팅
  • Automatic Batching
  • Promise
  • Recoil
  • zustand
  • nested routes
  • 브라우저 저장소
  • 프론트엔드 면접 질문
  • JavaScript
  • Promise.allSattled
  • reference data type
  • promise.all
  • Promise Chaining
  • 디자인패턴
  • 태스크큐
  • 브라우저 렌더링 과정
  • Promise.any
  • Broswer
  • get/set
  • Object.create()
  • 심플 팩토리

최근 댓글

최근 글

hELLO · Designed By 정상우.v4.2.2
프론이
TypeScript : 유틸리티 타입(1)
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.