본문 바로가기
개발자 도전기/[STUDY] JS || TS

TypeScript | 강의 메모 | 제네릭(Generic), 맵드 타입(Mapped Type), 조건부 타입(Conditional Type)

by 답수 2022. 4. 1.
728x90
반응형

 

 

※ 인프런 - 타입스크립트 시작하기(by 이재승) 강의를 기반으로 정리한 내용입니다.

 

 

제네릭(Generic)

제네릭은 타입 정보가 동적으로 결정되는 타입을 말한다. 제네릭을 통해 같은 규칙을 여러 타입에 적용할 수 있기 때문에 타입 코드를 작성할 때 발생할 수 있는 중복 코드를 제거할 수 있다.

export {};

function makeNumberArray(defaultValue: number, size: number): number[] {
    const arr: number[] = [];
    for (let i = 0; i < size; i++) arr.push(defaultValue);
    return arr;
}
function makeStringArray(defaultValue: string, size: number): string[] {
    const arr: string[] = [];
    for (let i = 0; i < size; i++) arr.push(defaultValue);
    return arr;
}
const arr1 = makeNumberArray(1, 10);
const arr2 = makeStringArray('empty', 10);
console.log(arr1);      // [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
console.log(arr2);      // ['empty', 'empty', 'empty', 'empty', 'empty', 'empty', 'empty', 'empty', 'empty', 'empty']


// 함수 오버로드. 위의 두 함수를 이처럼 하나로 만들 수 있음
// 필요한 타입 정의
function makeArray(defaultValue: number, size: number): number[];
function makeArray(defaultValue: string, size: number): string[];
// 로직 정의
function makeArray(defaultValue: number | string, size: number): Array<number | string> {
    const arr: Array<number | string> = [];
    for (let i = 0; i < size; i++) arr.push(defaultValue);
    return arr;
}
const arr3 = makeArray(1, 10);
const arr4 = makeArray('empty', 10);
/**
 * 하지만 이 방법은 number와 string만 입력 가능함
 * 타입을 추가하고 싶으면 타입 정의, 매개변수들의 타입 정의 등 많은 코드 수정해야 하기 때문에 번거로워질 수 있음
 * 이를 해결할 수 있는 것이 제네릭!
 */


// 제네릭
function makeArr<T>(defaultValue: T, size: number): T[] {
    const arr: T[] = [];
    for (let i = 0; i < arr.length; i++) arr.push(defaultValue);
    return arr;
}
const arr5 = makeArr<number>(1, 10);
const arr6 = makeArr<string>('empty', 10);
const arr7 = makeArr(1, 10);
const arr8 = makeArr('empty', 10);
/**
 * 제네릭은 함수 이름 오른쪽에 <>를 이용해서 입력할 수 있음
 * T는 원하는 이름으로 정할 수 있고, 현재 T라는 것의 타입은 정해지지 않음
 * T의 타입은 나중에 동적으로 결정될 것
 * T는 매개변수 쪽과 구현하는 쪽 모두 사용할 수 있음
 * arr5, arr6처럼 함수를 호출할 때 <>를 이용하여 타입 정의
 * 하지만 타입스크립트는 유연하기 때문에 arr7, arr8처럼 타입을 따로 정의하지 않아서 자동으로 숫자와 문자열로 정의됨
 */


// 제네릭은 데이터의 타입에 다양성을 부여해주기 때문에 자료구조에서 많이 사용됨
class Stack<D> {
    private items: D[] = [];
    push(item: D) {
        this.items.push(item);
    }
    pop() {
        return this.items.pop();
    }
}

const numStack = new Stack<number>();
numStack.push(10);
const v1 = numStack.pop();
const strStack = new Stack<string>();
strStack.push('a');
const v2 = strStack.pop();

let myStack: Stack<number>;
myStack = numStack;
myStack = strStack;     // string 형식은 number 형식에 할당 불가능하기 때문에 에러!


// 리액트와 같은 라이브러리의 API는 입력 가능한 값의 범위를 제한함
// 이를 위해 제네릭은 타입의 종류를 제한할 수 있는 기능을 제공
function identity<T extends number | string>(p1: T): T {    // A extends B : A가 B에 할당 가능해야 한다
    return p1;
}
identity(1);
identity('a');
identity(true);       // never[]형식의 인수는 number | string 형식의 매개변수에 할당 불가능


// extends 키워드 자세히 알아보기
interface Person {
    name: string;
    age: number;
}

interface Korean extends Person {
    liveInSeoul: boolean;
}

function swapProperty<T extends Person, K extends keyof Person> (p1: T, p2: T, key: K): void {  
    const tmp = p1[key];
    p1[key] = p2[key];
    p2[key] = tmp;
}
/**
 * keyof? Person의 모든 속성의 이름을 나열한 것
 * type T1 = keyof Person;  // type T1 = "name" | "age"
 * 즉 K는 name과 age에 할당 가능한 값이어야 함 
 */

const p1: Korean = {
    name: '기리',
    age: 30,
    liveInSeoul: true
};

const p2: Korean = {
    name: '우니',
    age: 29,
    liveInSeoul: false
};
swapProperty(p1, p2, 'age');    // 세 번째 매개변수에는 age, name만 나옴. 다른 키 입력 불가능
console.log(p1);    // { name: '기리', age: 29, liveInSeoul: true }
console.log(p2);    // { name: '우니', age: 30, liveInSeoul: false }

 

 

맵드 타입(Mapped Type)

A라는 인터페이스가 있을 때 A의 모든 속성을 readonly로 바꾸거나 optional로 바꾸는 등 이러한 일을 맵드 타입을 통해 할 수 있다.

export {};

// mapped type의 문법
type T1 = { [K in 'prop1' | 'prop2']: boolean};
/**
 * 맵드타입으로 만드는 것은 객체타입이기 때문에 중괄호 있고, 그안에 대괄호가 있음
 * 대괄호는 key 부분을 나타냄
 * 대괄호 안에 in 이라는 키워드를 사용함
 * 변수명 K는 아무거나 해도 상관 없음
 * in 오른쪽에 있는 키워드들이 전체 객체의 속성으로 만들어짐
 * T1에 마우스를 올리면 다음과 같이 나옴
    type T1 = {
        prop1: boolean;
        prop2: boolean;
    }   
 */


// 인터페이스의 모든 속성을 boolean 타입으로 만들어주는 맵드 타입
interface Person {
    name: string;
    age: number;
}

type MakeBoolean<T> = { [P in keyof T]?: boolean};  // keyof로 제너릭T의 키들이 유니온 타입으로 만들어짐. ?를 사용했기 때문에 선택 속성
const pMap: MakeBoolean<Person> = {};               // MakeBoolean안에 Person 입력
pMap.name = true;       // 원래 string타입인데 boolean으로 바뀜. 선택 속성이기 때문에 undefined도 가능
pMap.age = false;       // 원래 number타입인데 boolean으로 바뀜. 선택 속성이기 때문에 undefined도 가능


// readonly 맵드 타입 정의
type T2 = Person['name'];
type Readonly<T> = { readonly [P in keyof T]: T[P] };       // T의 모든 키의 속성에 readonly 붙임
type Partial<T> = { [P in keyof T]?: T[P] };
type T3 = Partial<Person>;
type T4 = Readonly<Person>;
/**
 * T[P]? T2처럼 인터페이스에 속성 이름을 적어주면 그 속성의 값의 타입을 의미함. T2는 문자열임
 * 즉 T[P]는 각 속성의 원래 값의 타입을 적어준 것. name은 string, age는 number  (값의 변화를 주지 않은 것)
 * 이렇게 맵드 타입을 이용하면 함수처럼 사용할 수 있는데, 일종의 유틸리티 타입이라고 보면 됨
 * 사실 Readonly와 Partial은 타입스크립트에 내장되어 있기 때문에 주석 처리해도 사용 가능
 */


// Pick 타입
type Pick<T, K extends keyof T> = { [P in K]: T[P]};    // Pick도 내장타입

interface Person {
    name: string;
    age: number;
    language: string;
}
type T5 = Pick<Person, 'name' | 'language'>;


// Record 타입. 역시 내장 타입
type Record<K extends string, T> = { [P in K]: T };
type T6 = Record<'p1' | 'p2', Person>;      // p1과 p2 속성으로 이루어지고 Person 타입을 값으로 가지는 객체를 만들겠다


// enum 타입 활용도 높이기
enum Fruit {
    Apple,
    Banana,
    Orange
}

const fruitPrice = {
    [Fruit.Apple]: 1000,
    [Fruit.Banana]: 1500,
    [Fruit.Orange]: 2000
};
// 만약 Fruit enum에 새로운 과일을 추가하게 되면 fruitPrice 객체에 새로 추가된 과일 속성을 추가해야 하는 번거로움이 있음

const fruitPrice2: { [key in Fruit]: number} = {
    [Fruit.Apple]: 1000,
    [Fruit.Banana]: 1500,
    [Fruit.Orange]: 2000,
};
// 객체를 맵드 타입으로 활용하면 enum의 모든 속성을 적지 않으면 에러가 나기 때문에 실수로 누락해도 바로 정정할 수 있음

 

 

조건부 타입(Conditionnal Type)

/** 조건부 타입 (Conditional Type)
 * 조건부 타입: 입력된 제네릭 타입에 따라 타입을 결정할 수 있는 기능
 * 조건부 타입의 문법: T extends U ? X : Y
 * 제네릭으로 입력된 어떤 타입 T가 U에 할당 가능하다면 X, 아니면 Y 타입 사용
 */

export {};

type IsStringType<T> = T extends string ? 'yes' : 'no';
type T1 = IsStringType<string>;     // T1 = "yes"
type T2 = IsStringType<number>;     // T2 = "no"
/**
 * T가 문자열로 할당이 가능하다면 yes라는 문자열 리터럴 타입 사용
 * 아니라면 no 타입 사용
 * 주의할 점: 자바스크립트의 삼항연산자와 비슷해보이지만 다름!
 * 조건부 타입은 값을 다루는 것이 아니라 타입을 다루는 것임
 */


// 조건부 타입은 유니온 타입과 자주 사용됨
type T3 = IsStringType<string | number>;    // T3 = "yes" | "no"
type T4 = IsStringType<string> | IsStringType<number>;      // T4 = "yes" | "no"
/**
 * 조건부 타입은 유니온 타입과 같이 사용되면 T3, T4처럼 독특하게 사용 가능
 * 단, 조건부 타입 + 유니온 타입일 때만 이런 결과 나옴!
 */

type Array2<T> = Array<T>;
type T5 = Array2<string | number>;
/**
 * T5는 T3, T4처럼 조건부 타입을 쓰지 않았기 때문에 
 * string[] | number[] 이런 타입이 만들어지지 않음
 * 조건부 타입이 아니기 때문에 T5는 문자열 또는 숫자의 배열이 됨. (string | number)[]
 */


// Exclude, Extract
type T6 = number | string | never;  // 유니온 타입에 있는 never는 해당되는 속성을 제외시킴
// Exclude: T가 U에 할당 가능한 타입 제거
type Exclude<T, U> = T extends U ? never : T;
type T7 = Exclude<1 | 3| 5 | 7, 1| 5| 9>;   // T7 = 3 | 7
type T8 = Exclude<string | number | (() => void), Function>;    // Function에 해당되는 것은 제거. T8 = string | number
// Extract: T가 U에 할당 가능하지 않으면 타입 제거
type Extract<T, U> = T extends U ? T : never;
type T9 = Extract<1 | 3 | 5 | 7, 1 | 5 | 9>;     // T9 = 1 | 5


// Return Type: T가 함수일 때 T 함수의 반환 타입을 뽑아줌
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
/**
 * 함수 T가 extends 뒤의 함수에 할당 가능한 타입이면 R이라는 것을 사용
 * R: 이 함수의 반환 타입 의미
 * 타입 추론을 위해 infer라는 키워드 사용
 * infer 키워드는 조건부 타입을 사용할 때 extends 키워드 뒤에서 이렇게 사용됨 
 */

function f1(s: string): number {
    return s.length;
}
type T10 = ReturnType<typeof f1>;       // T10 = number;
/**
 * 리턴타입은 제네릭에 함수만 입력할 수 있기 때문에 값으로부터 타입을 가져오기 위해서 typeof 키워드 사용함
 */


// infer 키워드 자세히 알아보기
type Unpacked<T> = T extends (infer U) []           // T 타입이 어떤 값의 배열이면(어떤 배열인지는 아직 정해지지 않았기 때문에 infer)
    ? U                                             // U 배열의 타입을 사용하겠다
    : T extends (...args: any[]) => infer U         // 배열이 아닐 시, 함수에 할당 가능한 타입이라면 
    ? U                                             // 함수의 반환 타입을 사용하겠다
    : T extends Promise<infer U>                    // 아니라면 프로미스에 할당 가능한 타입이라면(프로미스의 어떤 값인지는 아직 결정X --> infer)
    ? U                                             // 프로미스의 값인 U를 사용하겠다
    : T;                                            // 모두 만족하지 않으면 자기 자신인 T 사용

type T11 = Unpacked<string>;
type T12 = Unpacked<string[]>;
type T13 = Unpacked<() => string>;
type T14 = Unpacked<Promise<string>>;
type T15 = Unpacked<Promise<string>[]>;
type T16 = Unpacked<Unpacked<Promise<string>[]>>;


// 조건부 타입 사용하여 몇 가지 유틸리티 타입 만들어보기

// 인터페이스에서 값이 문자열인 속성 이름을 추출하는 유틸리티 타입
type StringPropertyNames<T> = {
    [K in keyof T]: T[K] extends string ? K : never;
}[keyof T];

interface Person {
    name: string;
    age: number;
    nation: string;
}
type T17 = StringPropertyNames<Person>;

type StringProperties<T> = Pick<T, StringPropertyNames<T>>;
type T18 = StringProperties<Person>;    // Pick을 통해 스트링 타입의 속성만 모아놓은 객체 생성


// Omit
type Omit<T, U extends keyof T> = Pick<T, Exclude<keyof T, U>>;
type T19 = Omit<Person, 'nation' | 'age'>;  // Person인터페이스에서 nation과 age를 제거한다 는 의미


// Overwrite
/**
 * 타입스크립트에 내장된 타입은 아님
 * 두 인터페이스를 받아서 T라는 인터페이스를 베이스로 해서 U를 T로 덮어쓰겠다!
 */
type Overwrite<T, U> = { [P in Exclude<keyof T, keyof U>]: T[P] } & U;  // T와 U 중에서 겹치는 것은 Exclude로 제거, 그후 U와 교집합

interface Person {
    name: string;
    age: number;
}
type T20 = Overwrite<Person, {age: string; nation: string}>;
const p: T20 = {
    name: 'mike',
    age: '23',
    nation: 'Korea'
};

 

 

 

 

 

 

 

 

 

 

728x90
반응형

댓글