Typescript

[Typescript] 이펙티브 타입스크립트 2장 아이템14

sian han 2023. 3. 5. 21:55

다음은 원기둥의 반지름과 높이, 표면적, 부피를 출력하는 코드이다.

console.log('Cylinder 1 X 1', //r: 1 h: 1
    'Surface area:', 6.283185 * 1 * 1 + 6.283185 * 1 * 1, //2πrh + 2πr^2
    'Volume: ', 3.14159 * 1 * 1 * 1); //πr^2h

console.log('Cylinder 1 X 2', //r: 1 h: 2
    'Surface area:', 6.283185 * 1 * 1 + 6.283185 * 2 * 1, //2πr^2 + 2πrh
    'Volume: ', 3.14159 * 1 * 2 * 1); //πr^2h


console.log('Cylinder 2 X 1', //r: 2 h: 1
    'Surface area:', 6.283185 * 2 * 1 + 6.283185 * 2 * 1, //2πrh + 2πr^2
    'Volume: ', 3.14159 * 2 * 2 * 1); //πr^2h

비슷한 코드가 반복되어있어 보기 불편하다.

함수, 상수, 루프의 반복을 제거해 코드를 개선해 보자 ! (DRY 원칙)

const surfaceArea = (r,h) => 2* Math.PI * r * (r * h);
const volume = (r,h) => Math.PI * r * r * h;
for(const [r,h] of [[1,1],[1,2],[2,1]]){
    console.log(
        `Cylinder ${r} x ${h}`,
        `Surface area: ${surfaceArea(r,h)}`,
        `Volume: ${volume(r,h)}`);
}

 

코드의 반복을 줄이는 가장 간단한 방법은 타입에 이름을 붙이는 것이다.

function distance(a: {x: number, y: number}, b: {x: number, y: number}){
    return Math.sqrt(Math.pow(a.x -b.x, 2) + Math.pow(a.y - b.y,2));
}
interface Point2D{
    x: number;
    y: number;
}

function distance(a: Point2D, b: Point2D){}

상수를 사용해서 반복을 줄이는 기법을 타입 시스템에 적용했다.

 

그러나 중복된 타입은 문법에 의해 가려지기도 한다.

 

예를 들어 아래와 같이 함수가 같은 타입 시그니처를 공유하고 있을 때 

시그니처를 명명된 타입으로 분리해 낼 수 있다. 

function get(url : string, opts: Options) : Promise<Response>{}
function post(url : string, opts: Options) : Promise<Response>{}
type HTTPFunction = (url: string, opts: Options) => Promise<Response>; //시그니처를 타입으로 분리
const get: HTTPFunction =(url, opts) => {};
const post: HTTPFunction =(url, opts) => {};

 

또는 인터페이스를 확장하여 반복을 제거할 수 있다.

interface Person {
    firstName: string;
    lastName: string;
}

interface PersonWithBirthDate extends Person{
    birth: Date;
}

 

 

전체 상태를 표현하는 State 타입과 부분만 표현하는 TopNavState 가 있다고 가정해보자. 

interface State{
    userId: string;
    pageTitle: string;
    recentFiles: string[];
    pageContents: string;
}

interface TopNavState{
    userId: string;
    pageTitle: string;
    recentFiles: string[];
}

 

 

TopNavState 를 확장해서 State 를 구성하는 것 보다

State 의 부분집합으로 TopNavState 를 정의하는 것이 바람직하다

type TopNavState = {
    userId: State['userId'];
    pageTitle: State['pageTitle'];
    recentFiles: State['recentFiles'];
};

만약 이때 State 내의 pageTitle 타입이 바뀌면 TopNavState 에도 반영된다.

 

 

'매핑된 타입' 을 사용해 코드를 더 줄일 수 있다.

type TopNavState = {
    [k in 'userId' | 'pageTitle' | 'recnetFiles']: State[k]
};

이 때 TopNavState 에 마우스를 올리면 앞의 예제와 완전히 동일하게 정의되어있는 것을 확인할 수 있다.

 

매핑된 타입

매핑된 타입은 기존 타입을 변환해서 새로운 타입을 만들어내는 기능이고, 주로 객체나 배열의 타입변환에 사용된다. 

 

매핑된 타입은 배열의 필드를 루프 도는 것과 같은 방식이다. 

이 패턴은 표준 라이브러리에서도 일반적으로 찾을 수 있으며, Pick 이라고 한다.

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

매핑된 타입은 `in` 키워드를 사용해

배열이나 튜플 등의 타입에 대해 루프를 도는 것과 같은 방식으로 새로운 타입을 만들어낸다.

 

type TopNavState = Pick<State, 'userId' | 'pageTitle' | 'recentFiles'>;
  • Pick 을 사용해서 State 에서 userId, pageTtiel, recentFiles 필드만 추출해서 새로운 타입을 만들 수 있다.
  • Pick 은 제네릭 타입으로 첫번째 인자로 객체타입을, 두번째 인자로 해당 객체 타입에서 추출하고자 하는 필드의 이름들을 문자열 리터럴 타입 배열로 받는다. 
  • TopNavState 타입은 userId, pageTtiel, recentFiles 필드만을 가진 새로운 타입이 되었다. 

매핑된 타입과 `Pick` 을 사용하면 코드를 간결하게 만들 수 있다.

 

 

태그된 유니온에서도 다른 형태의 중복이 발생할 수 있다.

  - 태그된 유니온 : 유니온 타입에 문자열 리터럴 타입을 추가해서 타입 안전성을 높인 방법

interface SaveAction{
    type: 'save';
}

interface LoadAction{
    type: 'load';
}

type Action = SaveAction | LoadAction;

 

Action 유니온을 인덱싱하면 타입 반복없이 ActionType 을 정의할 수 있다.

type ActionType = 'save' | 'load'; //타입의 반복 !
type ActionType = Action['type']; //타입은 "save" | "load"

Action 유니온에 타입을 더 추가하면 ActionType 은 자동적으로 그 타입을 포함한다.

 

ActionType 은 Pick 을 사용해서 얻게되는 type 속성을 갖는 인터페이스와는 다르다.  (객체)

type ActionRec = Pick<Action, 'type'>; //{type: "save" | "load"}

 

 

▷ keyof

interface Options{
    width: number;
    height: number;
    ocolor: string;
    label: string;
}

interface OptionsUpdate{
    width?: number;
    height?: number;
    color?: string;
    label?: string;
}

매핑된 타입과 keyof 를 사용하면 동일하게 OptionsUpdate 를 만들 수 있다. 

keyof 는 타입을 받아서 속성 타입의 유니온을 반환한다.

type OptionsUpdate = {[k in keyof Options]?: Options[k]};

 

편집기를 통해 코드와 동일하게 정의되어있는 것을 확인할 수 있다. 

 

 

▷ typeof

값의 형태에 해당하는 타입을 정의하고 싶을 때 typeof

const INIT_OPTIONS = {
    width: 640,
    height: 480,
    color: '#00FF00',
    label: 'VGA',
};

interface Options{
    width: number,
    height: number,
    color: string,
    label: string,   
}

 

 

 

▷ ReturnType

함수나 메서드의 반환값에 명명된 타입을 만들 때 ReturnType

  • ReturnType 은 함수의 반환타입을 추출하는 유틸리티 타입이다. 
  • ReturnType 을 사용해 함수의 반환 타입을 변수로 선언하거나 다른 타입의 매개변수로 전달할 수 있다.

예제 1 )

function calculatePrice(quantity: number, price: number): number {
  return quantity * price;
}

calculatePrice 는  quantity와 price를 받아서 곱한 값을 반환한다.

함수의 반환 타입은 number 이다.

 

ReturnType 을 사용해서 calculatePrice 함수의 반환 타입을 추출해서 변수로 선언할 수 있다.

type CalculatePriceReturnType = ReturnType<typeof calculatePrice>;

 

 ReturnType<typeof calculatePrice> 을 통해 함수의 반환 타입인 number 를 CalculatePriceReturnType 타입에 할당한다. 

 

const totalPrice : CalculatePriceReturnType = 100;

totalPrice 변수에 CalculatePriceReturnType 의 값인 100을 할당했다.

 

 

예제 2 )

function getUserInfo(userId: string){
    //...
    return {
        userId,
        name,
        age,
        height,
        weight,
        favoriteColor
    };
}
type UserInfo = ReturnType<typeof getUserInfo>;

ReturnType 은 함수의 타입인 typeof getUserInfo 에 적용되었다.

적용대상이 값인지 타입인지 정확하게 알고 구분해서 처리해야 한다.

 

 

▷ 제너릭타입

  • 제너릭 타입은 타입을 위한 함수와 같다. 
  • 제네릭 타입은 타입스크립트에서 DRY 원칙을 적용하는 핵심 방법 중 하나이다. 제너릭을 사용하면 타입의 중복을 최소화하고 재사용성을 높일 수 있다.

ex )

function addNumbers(a: number, b: number): number {
  return a + b;
}

function concatenateStrings(a: string, b: string): string {
  return a + b;
}

두 함수는 각각 두개의 인수를 받아서 그 값을 합쳐서 반환하고있다.

 

이를 제너릭으로 변경할 수 있다.

function combine<T>(a: T, b: T): T {
  return a + b;
}

combine 함수는 <T> 라는 타입의 매개변수를 사용한다.

`T` 는 함수를 호출할 때 전달된 값의 타입으로 결정된다. 

 

따라서 이 함수는 어떤 타입의 값을 합쳐서 반환할 수 있게된다. 

 

제너릭 타입에서는 매개변수를 제한할 수 있는 방법이 필요하다. (타입안전성을 보장하기 위해서)

제너릭 타입을 사용하면 여러 종류의 값들을 다룰 수 있는데, 제너릭 타입이 특정한 종류의 값만 다루도록 제한해야 하는 경우가 있다. 

extends 를 이용하면 제너릭 매개변수가 특정 타입을 확장한다고 선언할 수 있고 이를 통해 매개변수를 제한할 수 있다. 

interface Name{
    first: string;
    last: string;
}
type DancingDuo<T extends Name> = [T,T];

 

 

제네릭 타입 매개변수 T 는 Name을 확장한다.

DancingDuo 타입은 T 타입의 배열이고, 요소는 Name 타입 객체로 이뤄져있다. 

 

const couple1: DancingDuo<Name> = [
    {first: 'Fred', last: 'Astaire'},
    {first: 'Ginger', last: 'Rogers'}
];   //정상
const couple2: DancingDuo<{first: string}> = [
    {first: 'Sonny'},
    {first: 'Cher'}
]; // 오류

오류메세지 : Name 타입에 필요한 last 속성이 {first: string;} 타입에 없습니다.

{first: string} 은 Name 을 확장하는 것이 아니기 때문에 오류가 발생한다. 

 

 

Pick

K는 T타입과 무관하며 범위가 너무 넓다.

K는 keyof T 가 되어야 한다.

 

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

T : 객체 타입

K : T의 속성 이름 중 일부를 선택할 수 있는 keyof 연산자를 사용한 타입 매개변수

 

Pick 타입은 T 타입에서 K 타입에 포함된 속성만 선택해 새로운 타입을 정의한다.

이 때 in 키워드를 사용한 매핑타입을 이용해서 새로운 객체 타입을 생성한다. 

 

 

<참고>

 

[TS] 📘 타입스크립트 - 유틸리티 타입 💯 총정리 (+ 응용)

타입스크립트 - Utility Types 지금까지 타입스크립트를 다루면서, 자바스크립트를 어느정도 아니까 타입 종류만 배우면 뚝딱 마스터 할 줄 알았더니, 타입 자체를 코딩하며 에러줄을 사라지게 하

inpa.tistory.com

 

 

 

[TS] 📘 타입스크립트 - 조건부 타입 완벽 이해하기

고급 타입 - Conditional Types 조건부 타입(conditional type)이란 입력된 제네릭 타입에 따라 타입을 결정 기능을 말한다. 위와 같이 조건부 타입 문법은 extends 키워드와 물음표 ? 기호를 사용하는데, 보

inpa.tistory.com