132 타입스크립트 리서치 2

source: categories/study/vue-experiance/vue-experiance_9-99_33.md

132 타입스크립트 리서치 2

고급 타입

유틸리티 타입 소개

  • 유틸리티 타입은 타입스크립트 공식 문서에서 acvanced type이라고 해서 고급 타입이라고 안내를 하고 있다.
  • 유틸리티 타입은 이미 정의해놓은 타입을 변환할 때 사용하기 좋은 타입 문법이다.
  • 유틸리티 타입을 꼭 쓰지 않더라도 기존의 interface, generic 등의 기본 문법으로 충분히 타입 변환을 할 수 있지만
  • 유틸리티 타입을 쓰면 훨씬 더 간결한 문법으로 타입을 정의할 수 있다.

  • 유틸리티 타입은 다른 블로그글을 보면 제네릭 타입이라고도 불리고 있다.
  • 왜 그런지는 앞으로의 내용들을 보며 말씀드리도록 하겠다.

  • 여튼 유틸리티 타입은 타입스크립트 코드량을 조금이라도 더 줄이기위해 나온 API성의 타입이라고 생각하면 된다.

자주 사용되는 유틸리티 타입 몇개 알아보기

Partial

  • 파셜(partial) 타입은 특정 타입의 부분 집합을 만족하는 타입을 정의할 수 있다.


interface Address {
    email: string;
    address: string;
}

type MayHaveEmail = Partial<Address>
const me: MayHaveEmail = {}; // OK
const you: MayHaveEmail = { email: 'test@abc.com' }; // OK
const all: MayHaveEmail = { email: 'capt@hero.com', address: 'Pangyo' }; // OK
const err: MayHaveEmail = {
    email: 'test@abc.com',
    address: 'aaaa',
    home: 'bbb', // Type '{ email: string; address: string; home: string; } is not assignable to type 'Partial<Address>'.
                 // Object literal may only specify known properties, and 'home' does not exist in type 'Partial<Address>'.
}


  • 자바스크립트의 map 메소드를 생각하시면 좋다.
  • Address라는 인터페이스를 map으로 돌려서 readonly라던가 optional이라던가 이렇게 바꾼다는 관점으로 이해하면 된다.

Pick

  • 픽(pick) 타입은 특정 타입에서 몇 개의 속성을 선택(pick)하여 타입을 정의할 수 있다.


interface Hero {
    name: string;
    skill: string;
}

const human: Pick<Hero, 'name'> = {
    name: '스킬이 없는 사람',
}




interface Hero {
    name: string;
    skill: string;
}

const human: Pick<Hero, 'name'> = {
    name: 'dd',
    skill: 'ddd', // error
}
// TS2322: Type '{ name: string; skill: string; }' is not assignable to type 'Pick<Hero, "name">'. 
// Object literal may only specify known properties, and 'skill' does not exist in type 'Pick<Hero, "name">'.




interface Product {
    id: number;
    name: string;
    price: number;
    brand: string;
    stock: number;
}

// 상품 목록을 받아오기 위한 API 함수
function fetchProducts(): Promise<Product[]> {

}

// 상품의 디테일한 정보를 보여주는 함수
// 상품 목록을 받아올 때의 인터페이스와 디테일을 보여줄 때 필요한 인터페이스는 다를 수 있다. - 실무에서 비일비재하다.
// 아래와 같이 Product와 겹치는데도 Product를 재활용할 수가 없다.
// Product의 일부만 받아와 디테일 정보를 표시해야되기 때문에.
function displayProductDetail(shoppingItem: {
    id: number;
    name: string;
    price: number;
}) {

}




interface Product {
    id: number;
    name: string;
    price: number;
    brand: string;
    stock: number;
}

// 상품 목록을 받아오기 위한 API 함수
function fetchProducts(): Promise<Product[]> {

}

// 상품의 디테일한 정보를 보여주는 함수
// 상품 목록을 받아올 때의 인터페이스와 디테일을 보여줄 때 필요한 인터페이스는 다를 수 있다. - 실무에서 비일비재하다.
// 아래와 같이 Product와 겹치는데도 Product를 재활용할 수가 없다.
// Product의 일부만 받아와 디테일 정보를 표시해야되기 때문에.

// 그래서 또 하나의 interface를 정의하곤 한다.
// 사실 이렇게 interface를 늘려가는 것이 잘못된 것은 아니다.
// 하지만 이렇게 작성한다면 중복된 코드들이 많이 생성되게 될 것이다.
interface ProductDetail {
    id: number;
    name: string;
    price: number;
}
function displayProductDetail(shopping: ProductDetail) {

}




interface Product {
    id: number;
    name: string;
    price: number;
    brand: string;
    stock: number;
}

// 상품 목록을 받아오기 위한 API 함수
function fetchProducts(): Promise<Product[]> {

}

// 아래 type ShoppingItem에 마우스 커서를 올려놓으면, Initial type: {id: number, name: string, price:number} 타입이 뜬다.
type ShoppingItem = Pick<Product, 'id' | 'name' | 'price'>;
function displayProductDetail(shoppingItem: ShoppingItem) {

}


  • 이런 것을 advanced type, utility type, generic type이라고 부른다.

Omit 타입과 기타 유틸리티 타입 목록 소개

  • 오밋(Omit) 타입은 특정 타입에서 지정된 속성만 제거한 타입을 정의해준다.
  • 타입스크립트 공식문서 - 유틸리티 타입
  • 사실 이런 고급 타입을 사용 안해도 interfacegeneric으로 할 수 있는 것들이 많다.
  • 하지만 코드를 좀 더 깔끔하게 사용하고 싶다면 이런 유틸리티 타입을 사용하는 것이 좋다.


interface AddressBook {
    name: string;
    phone: number;
    address: string;
    company: string;
}

const phoneBook: Omit<AddressBook, 'address'> = {
    name: '재택근무',
    phone: 123412341234,
    company: '내방',
}

const chigtao: Omit<AddressBook, 'address' | 'company'> = {
    name: '중국집',
    phone: 12341234,
}

const chigtao: Omit<AddressBook, 'address' | 'company'> = {
    name: 'aa',
    phone: 1234,
    company: 'sdf',
}
// Type '{ name: string; phone: number; company: string; }' is not assignable to type 'Omit<AddressBook, "address" | "company">'.
// Object literal may only specify known properties, and 'company' does not exist in type 'Omit<AddressBook, "address" | "company">'.

const chigtao: Omit<AddressBook, 'address' | 'company'> = {
    name: 'aa',
    phone: 1234,
    address: 'sdf',
}
// Type '{ name: string; phone: number; address: string; }' is not assignable to type 'Omit<AddressBook, "address" | "company">'.
// Object literal may only specify known properties, and 'address' does not exist in type 'Omit<AddressBook, "address" | "company">'.(2322)


유틸리티 타입 사례 - Partial



interface Product {
    id: number;
    name: string;
    price: number;
    brand: string;
    stock: number;
}

// 상품 목록을 받아오기 위한 API 함수
function fetchProducts(): Promise<Product[]> {

}

// 아래 Pick에 마우스 커서를 올려놓으면 뜨는 설명은 다음과 같다.
// type Pick<T, K>
// From T, pick a set of properties whose keys are in the union K
// Alias for: {[P in K]: T[P]} // 별칭: 유니온 K의 각각 요소인 P의 타입은 T안에 존재하는 프로퍼티이다. 
// Expanded: {[p: string]: T[string]} // [] 배열 안에 있는 요소들의 타입은 string인데, 이 요소들의 타입은 T의 프로퍼티 중에서 string 타입인 것들이다.
type ShoppingItem = Pick<Product, 'id' | 'name' | 'price'>;
function displayProductDetail(shoppingItem: ShoppingItem) {

}




interface Product {
    id: number;
    name: string;
    price: number;
    brand: string;
    stock: number;
}
// 아래 Pick에 마우스 커서를 올려놓으면 뜨는 설명은 다음과 같다.
// type Pick<T, K>
// From T, pick a set of properties whose keys are in the union K
// Alias for: {[P in K]: T[P]} 
// 별칭: 이 객체 안의 프로퍼티 키의 타입은 K 유니온에 있는 것들이어야 한다.
// 각 프로퍼티 키에 할당되는 프로퍼티 값의 타입은 T[P]의 타입들과 동일하다.
// Expanded: {[p: string]: T[string]} 
// 이 객체 안의 특정 프로퍼티 키의 타입은 string 이다.
// 해당 프로퍼티 키의 값의 타입은 T[string] 타입이다.
// - 이러한 추상적인 타입 정의에서 확장(expanded)을 해서 alias for(별칭)을 지어준 것 같다.
type ShoppingItem = Pick<Product, 'id' | 'name' | 'price'>;


  • 설명이 어렵다. P in K
  • 이런거 말고 좀 더 간단한 것을 살펴보도록 하겠다.


interface Product {
    id: number;
    name: string;
    price: number;
    brand: string;
    stock: number;
}

// 1. 상품 목록을 받아오기 위한 API 함수
function fetchProducts(): Promise<Product[]> {

}

// 2. 특정 상품의 상세 정보를 나타내기 위한 함수
type ShoppingItem = Pick<Product, 'id' | 'name' | 'price'>;
function displayProductDetail(shoppingItem: ShoppingItem) {

}

interface UpdateProduct {
    id?: number;
    name?: string;
    price?: number;
    brand?: string;
    stock?: number;
}

// 3. 특정 상품 정보를 업데이트(갱신)하는 함수
// 매번 Product의 모든 정보를 업데이트하는 것이 아니다. 필요한 정보만 업데이트한다.
// 그럼 이렇게 UpdateProduct라는 인터페이스를 만든 다음에 아래와 같이 타입 부여를 해야된다.
// 이렇게 하게되면 같은 타입을 동일하게 한번 더 선언하고 옵셔널로 바꾼게된다.
// 이럴 때 쓸 수 있는 것이 유틸리티 타입의 Partial이다.
function updateProductItem(productItem: UpdateProduct) {

}




interface Product {
    id: number;
    name: string;
    price: number;
    brand: string;
    stock: number;
}

// 1. 상품 목록을 받아오기 위한 API 함수
function fetchProducts(): Promise<Product[]> {

}

// 2. 특정 상품의 상세 정보를 나타내기 위한 함수
type ShoppingItem = Pick<Product, 'id' | 'name' | 'price'>;
function displayProductDetail(shoppingItem: ShoppingItem) {

}

// 3. 특정 상품 정보를 업데이트(갱신)하는 함수
// 아래와 같이 정의하면 따로 인터페이스를 또 정의 안해도 된다.
function updateProductItem(productItem: Partial<Product>) {

}




interface Product {
    id: number;
    name: string;
    price: number;
    brand: string;
    stock: number;
}

// 1. 상품 목록을 받아오기 위한 API 함수
function fetchProducts(): Promise<Product[]> {

}

// 2. 특정 상품의 상세 정보를 나타내기 위한 함수
type ShoppingItem = Pick<Product, 'id' | 'name' | 'price'>;
function displayProductDetail(shoppingItem: ShoppingItem) {

}

// 아래 type UpdateProduct에 마우스 커서를 올려놓으면
// Alias for: Partial<Product>
// Initial type: {id?: number, name?: string, price?: number, brand?: string, stock?:number}
// 이렇게 기존 Product 타입에 ? 가 붙은 것을 볼 수 있다.
// 그래서 Partial라는 것은 모든 부분집합을 뜻한다고 생각하면 된다.
type UpdateProduct = Partial<Product>;

// 3. 특정 상품 정보를 업데이트(갱신)하는 함수
// 아래와 같이 정의하면 따로 인터페이스를 또 정의 안해도 된다.
function updateProductItem(productItem: UpdateProduct) {

}


Partial 정의부분

  • Partial 키워드에서 (command + b)를 누르면 아래와 같이 타입스크립트 내부적으로 어떤식으로 Partial을 정의해놨는지 볼 수 있다.


type Partial<T> = {
    [P in keyof T]?: T[P];
    // P의 타입은 T 객체의 프로퍼티 키
    // P의 값의 타입은 T[P] 타입
}


  • 보면 Partial에서 <T> 제네릭을 받아서 [P in keyof T]?: T[P]; 이렇게 처리한 것을 확인할 수 있다.

유틸리티 타입 구현



interface UserProfile {
    username: string;
    email: string;
    profilePhotoUrl: string;
}

interface UserProfileUpdate {
    username?: string;
    email?: string;
    profilePhotoUrl?: string;
}




interface UserProfile {
    username: string;
    email: string;
    profilePhotoUrl: string;
}

type UserProfileUpdate = {
    username?: UserProfile['username'];
    email?: UserProfile['email'];
    profilePhotoUrl?: UserProfile['profilePhotoUrl'];
}




interface UserProfile {
    username: string;
    email: string;
    profilePhotoUrl: string;
}

// 맵드 타입(Mapped Type)
type UserProfileUpdate = {
    [p in 'username' | 'email' | 'profilePhotoUrl']?: UserProfile[p]
}
// type UserProfileUpdate에 마우스 커서를 올리면
// Initial Type: {username?: string, email?: string, profilePhotoUrl?: string}


  • 위의 예시 코드는 실제 Partial 타입을 구현한 것은 아니다.


interface UserProfile {
    username: string;
    email: string;
    profilePhotoUrl: string;
}

type UserProfileKeys = keyof UserProfile

const a: UserProfileKeys = 'email'; // Initial Type: "username" | "email" | "profilePhotoUrl"
const b: UserProfileKeys = 'skill'; // Type '"skill"' is not assignable to type 'keyof UserProfile'.




interface UserProfile {
    username: string;
    email: string;
    profilePhotoUrl: string;
}

type UserProfileUpdate = {
    [p in keyof UserProfile]?: UserProfile[p]
}


  • Partial에 가까운 타입이 되었다.


type Subset<T> = {
    [p in keyof T]?: T[p]
}


  • 위 모습이 바로 Partial의 구현 모습이다.

유틸리티 타입 구현 내용 정리



interface UserProfile {
    username: string;
    email: string;
    profilePhotoUrl: string;
}

// 이런식으로 인터페이스를 늘려나가는 방식은 똑같은 코드를 계속 붙여넣기하는 식으로 늘려나간다.
// 즉, 중복되는 코드가 많아진다.
interface UserProfileUpdate {
    username?: string;
    email?: string;
    profilePhotoUrl?: string;
}




interface UserProfile {
    username: string;
    email: string;
    profilePhotoUrl: string;
}

// #1: 중복코드 줄이기 첫번째 시도
// 중복되는 코드를 줄이고자 아래와 같이 정의했다.
// 이미 정의되어있는 인터페이스를 활용하여 타입별칭을 활용해 타입을 정의한다.
// 하지만 아까와 별반 차이가 없다.
// 여전히 중복코드가 많다.
type UserProfileUpdate = {
    username?: UserProfile['username'];
    email?: UserProfile['email'];
    profilePhotoUrl?: UserProfile['profilePhotoUrl'];
}




interface UserProfile {
    username: string;
    email: string;
    profilePhotoUrl: string;
}

// #2
// 맵드 타입(Mapped Type)
// 이전에 3줄로 정의되어있던 것을 1줄로 줄였다.
type UserProfileUpdate = {
    [p in 'username' | 'email' | 'profilePhotoUrl']?: UserProfile[p]
}




interface UserProfile {
    username: string;
    email: string;
    profilePhotoUrl: string;
}

// #3
// UserProfile에 국한된 Partial 타입
type UserProfileUpdate = {
    [p in keyof UserProfile]?: UserProfile[p]
}




// 제네릭을 활용한 진정한 Partial 타입
// 아래 타입이 사실 Partial임
// 타입스크립트엔 이미 Partial이라는 아래와 같은 타입이 만들어져있기 때문에 Partial를 가져다 쓰면된다.
type Subset<T> = {
    [p in keyof T]?: T[p]
}


맵드 타입(Mapped Type)

맵드 타입 소개

  • 맵드 타입이란 기존에 정의되어있는 타입을 새로운 타입으로 변환해주는 문법을 의미한다.
  • 마치 자바스크립트 map() API 함수를 타입에 적용한 것과 같은 효과를 가진다.


// 변환 전 타입
interface {
    name: string;
    email: string;
}

// 변환 후 타입
interface {
    name: number;
    email: number;
}


자바스크립트의 map 함수란?

  • 자바스크립트의 map API는 배열을 다룰 때 유용한 자바스크립트 API이다.
  • 간단하게 예시 코드를 보도록 하겠다.


var arr = [
    { id: 1, title: '함수' },
    { id: 2, title: '변수' },
    { id: 3, title: '인자' },
]

var result = arr.map(function (item) {
    return item.title;
})

console.log(result); // ['함수', '변수', '인자']


  • 위 코드는 3개의 객체 요소를 가진 배열 arr.map() API를 적용한 코드이다.
  • 배열의 각 요소를 순회하여 객체에서 title을 반환하였다.

맵드 타입의 기본 문법

  • Mapped Type Proposal 깃헙 PR
  • 맵드 타입은 위에서 살펴본 자바스크립트의 map 함수를 타입에 적용했다고 보면된다.
  • 이를 위해서는 아래와 같은 형태의 문법을 사용해야한다.


{[P in K]: T}
{[P in K]?: T}
{ readonly [P in K]: T }
{ readonly [P in K]?: T }


맵드 타입 예제



// 맵드 타입은 for in 문을 떠올리면서 보면 된다.
const arr = ['a', 'b', 'c'];
for (let key in arr) {
    console.log(arr[key]);
}

type Heroes = 'Hulk' | 'Capt' | 'Thor';
// HeroAges에 마우스 커서를 올려놓으면
// Initial type: {Hulk: number, Capt: number, Thor: number} 라고 뜬다.
type HeroAges = {[K in Heroes]: number};

const ages: HeroAges = {
    Hulk: 30,
    Capt: 100,
    Thor: 1000,
}


최종 프로젝트 안내