.
본문 바로가기
TypeScript

우아한 타입스크립트 실전 코드 작성하기(1)

by 와칸다개발자 2022. 5. 26.

https://www.youtube.com/watch?v=ViS8DLd6o-E 

 

위 영상을 보고 공부한 것을 기록하는 포스트입니다.

 

제너릭에 따라 조건부 타입 설정하기

interface StringContainer {
    value: string;
}

interface NumberContainer {
    value: number;
}

type Item1<T> = {
    id: T;
    container: T extends string ? StringContainer : NumberContainer;
}

제너릭은 함수나 변수를 호출시 타입이 결정되도록 하는 기능

 

위 코드에서 T extends string은 제너릭 T가 string 타입에 할당 가능한가? 즉 서브타입이냐에 따라

 

타입이 결정되도록 삼항연산자를 사용하였다.

 

하지만 만약에 위 코드에서 제너릭 타입이 number, string이 둘다 아니라면 ???

type Item2<T> = {
    id: T extends string | number ? T : never;
    container: T extends string 
        ? StringContainer
        : T extends number
        ? NumberContainer
        : never;
}

const item3 : Item2<boolean> = {
    id:true,        // Type 'boolean' is not assignable to type 'never'.
    container: null // Type 'never' is not assignable to type 'never'.
}

never 라는 키워드를 사용한다.

 

never는 사용불가하다.

 

이중 삼항연산자를 사용하여 string과 number 타입이 아니라면 never를 사용한다.

 

ArrayFilter<T>

arrayfilter는 말그대로 배열 타입을 걸러내는 커스텀 타입

 

type ArrayFilter<T> = T extends any[] ? T : never;
type StringsOrNumbers = ArrayFilter<string | number | string[] | number[]>;

위 코드에서 StringsOrNumbers 타입은

type StringsOrNumbers = string[] | number[];

위와 같이 추론된다. any[]는 제너릭 T가 배열에 할당가능한지 판단하고 T를 반환한다.

 

제너릭 타입 함수 파라미터에 타입 제약걸기

함수 파라미터에 제너릭타입을 받을 때 특정 타입으로만 추론될 수 있게 제약을 걸고 싶다면

interface Table {
    id: string;
    chairs: string[];
}

interface Dino {
    id: number;
    legs: number;
}

interface World{
    getItem<T extends string | number>(id: T): T extends string ? Table : Dino;
}

let world:World = null as any;	// 명시적으로 any 할당

const dino = world.getItem(10);
const what = world.getItem(true); // Error! Argument of type 'boolean' is not assignable to parameter of type 'string | number'.(2345)

여기서 world 인터페이스를 자세히 살펴보자

interface World{
    getItem<T extends string | number>(id: T): T extends string ? Table : Dino;
}

getItem 함수에 제너릭을 받는데 string | number 유니온 타입만을 받고 있어서 위 코드에 

 

boolean 타입을 할당할 경우 에러 발생

 

Flatten<T>

type Flatten<T> = T extends any[]
    ? T[number]
    : T extends object
    ? T[keyof T]
    : T;

const numbers = [1,2,3];
type NumbersArrayFlattened = Flatten<typeof numbers>;   // number

const Person = {
    age:23,
    name:"king"
}

type SomeObjectFlattened = Flatten<typeof Person>;      // string | number
// 추론과정
// 1. keyof T --> "age" | "name"
// 2. T["age" | "name"] --> T["age"] | T["name"] --> number | string

const bool = true;  
type SomeBooleanFlattened = Flatten<typeof bool>;       // true

Flatten 타입은 이중 삼항연산자로 타입을 추론하는데

 

1. 배열이면 배열에 아이템 타입을 리턴 

위 코드에서 numbers 라는 number 배열을 제너릭에 넘겨줬기에 number를 리턴받는다.

string[]을 넘겨주면 string을 받는다.

 

2. object 타입이면 object 타입에 키속성을 리턴 

Person 객체의 속성은 number 와 string을 가지고 있으므로 string | number를 리턴받느다.

 

3. 배열이나 객체도 아니라면 원시객체를 리턴

 

infer

type UnPackPromise<T> = T extends Promise<infer K>[] ? K : any;
const promises = [Promise.resolve("str"), Promise.resolve(3)];  // (Promise<string> | Promise<number>)[]
type Expected = UnPackPromise<typeof promises>;		// string | number

UnPackPromise 타입에 제너릭으로 promises 의 타입을 넘겨준다.

 

UnPackPromise 제너릭으로 들어온 타입은 (Promise<string> | Promise<number>)[] 이므로 

 

Promise 내부 리턴타입 K를 infer 키워드를 사용하여서 뱉어낸다. 즉 K는 string | number 타입이 된다.

 

infer 키워드를 사용해서 나만의 함수의 리턴타입을 만들어보자.

 

function plus1(seed: number): number {
    return seed + 1;
}

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

type Id = MyReturnType<typeof plus1>;

아까 위에서 함수 파라미터에 제약 사항을 걸어놓았듯이

 

MyReturnType 에 제너릭 타입에도 함수를 걸어놓았다.

 

<T extends (...args:any) => any> 즉 T는 반드시 함수여야 한다.

 

만약에 함수라면 infer 키워드를 사용해서 함수의 리턴타입을 추론한다.

 

typescript 에 내장된 Utility Type은 다음과 같이 작성할 수 있다.

공식 doc은 여기 있으니 확인해보자.

https://www.typescriptlang.org/docs/handbook/utility-types.html

 

Documentation - Utility Types

Types which are globally included in TypeScript

www.typescriptlang.org

 

// type Exclude<T,U> = T extends U ? never : T;
type MyExclude = Exclude<string | number, string>;  // number

// type Extract<T,U> = T extends U ? T : never;
type MyExtracted = Extract<string | number, string>;    // string -> filter의 기능

// Pick<T , Exclude<keyof T, K>>;
type MyPicked = Pick<{name:string, age:number}, "name">;    // {name:string}

// type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
type MyOmited = Omit<{name:string , age:number}, "name">;

// type NonNullable<T> = T exteds null | undefined ? never : T;
type MyNonNullable = NonNullable<string | number | undefined | null>;   // string | number

 

타입이나 인터페이스안에서 함수인 속성 다루기

type FunctionPropertyNames<T> = {
    [K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];

type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;

type NonFunctionPropertyNames<T> = {
    [K in keyof T]: T[K] extends Function ? never : K;
}[keyof T];

type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;


interface Person {
    id: number;
    name: string;
    hello: (message: string) => void;
}

type T1 = FunctionPropertyNames<Person>;	// "hello"
type T2 = NonFunctionPropertyNames<Person>;		// "id" | "name"
type T3 = FunctionProperties<Person>;		// { hello: (message: string) => void;}
type T4 = NonFunctionProperties<Person>;	// { id: number; name: string;}

하나 하나 살펴보자. 느리게 살펴보면 그렇게 어렵지(?) 않다.

type FunctionPropertyNames<T> = {
    [K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];

type T1 = FunctionPropertyNames<Person>;	// "hello"

위 타입은 함수인 속성의 이름을 가져오는 타입이다. 

 

제너릭으로 받은 타입의 in keyof 를 사용하여 객체의 키값을 가져오고 함수라면 속성명을 리턴하고 아니라면 never로 사용 하지 못하도록 만든다.

type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;
type T3 = FunctionProperties<Person>;		// { hello: (message: string) => void;}

그러므로 함수의 속성명이 아닌 값타입을 얻고 싶다면 Pick 유틸리티 타입을 이용하여 함수의 실제 타입을 얻을 수 있다.

 

다음편에 계속

'TypeScript' 카테고리의 다른 글

type challange <Easy>  (0) 2022.11.03
우아한 타입스크립트 실전 코드 작성하기(2)  (0) 2022.06.08
여러가지 형태의 타입가드  (0) 2022.05.09
Type vs Interface  (0) 2021.12.05
abstract 와 interface 차이점  (0) 2021.11.08

댓글