TS. Part1 Ch03-Utility Types

hun2__2·2023년 8월 8일
0

03.1 - Partial 타입 분석

Partial의 기능은 원래 필수였던 값들을 옵션값으로 다 변경해준다.

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

인덱스 시그니처를 사용해서 맵드 타입으로 T객체의 키값들 키로 갖고 ([P in keyof T]) 옵셔널값으로 객체의 키값에 따른 value도 가져온다(T[P])

이렇게 되면 타입이 느슨해져서 Partial보다는 Pick, Omit 사용을 지향하자

03.2 - Pick 타입 분석

Pick을 사용하면 기존객체의 필요한 값만 가져올 수 있다.

제네릭을 쓸때는 서로의 제한 조건을 먼저 작성해주는게 편하다 (K extends keyof T, P in K)

interface Profile {
  name: string;
  age: number;
  married: boolean;
}

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

const hun: Profile = {
  name: "hun",
  age: 3,
  married: false,
};

const hun2: Pick<Profile, "name" | "age"> = {
  name: "hun2",
  age: 3,
};

03.3 - Omit, Exclude, Extract 타입 분석

Omit을 사용하면 기존객체의 필요없는 값만 빼고 가져올 수 있다.

Omit은 전체에서 Pick을 제외하면 Omit이 되므로 제외하는방법인 Exclude를 알아야 한다.

type Exclude<T, U> = T extends U ? never : T;

Exclude는 T에서 U에 해당하는 값을 뺀 타입들만 남는다.

interface Profile {
  name: string;
  age: number;
  married: boolean;
}

type Test = Exclude<keyof Profile, "name">; // "age" | "married"

Exclude와 반대되는 개념으로 Extract도 있다.

type Exclude<T, U> = T extends U ? never : T; // T가 U의 부분집합이면 never, 아니면 T
type Extract<T, U> = T extends U ? T : never; // T가 U의 부분집합이면 T, 아니면 never

interface Profile {
  name: string;
  age: number;
  married: boolean;
}

type Test = Exclude<keyof Profile, "name">; // 두번째 인자 제거
type Test2 = Extract<keyof Profile, "name">; // 두번째 인자만 남김

Omit은 그러면 T에서 K를 뺀 값을 Pick 한 것과 동일하기 때문에 아래와 같이 작성할 수 있다

type E<T, U> = T extends U ? never : T;

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

type O<T, K extends keyof T> = P<T, E<keyof T, K>>;

사용예시

interface Profile {
  name: string;
  age: number;
  married: boolean;
}

type E<T, U> = T extends U ? never : T;

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

type O<T, K extends keyof T> = P<T, E<keyof T, K>>;

const hun: Profile = {
  name: "hun",
  age: 3,
  married: false,
};

const hun2: O<Profile, "married"> = {
  name: "hun2",
  age: 3,
};

03.4 - Required, Record, NonNullable 타입 분석

Required는 Partial의 반대 개념으로 옵셔널로되어있는값을 모두 필수로 바꿔준다.

type Required<T> = {
    [P in keyof T]-?: T[P];
};

여기서 -?는 다른 타입을 가져올 때 옵션값(?)을 제거하라는 뜻이다.

cf) 수정 못하게 읽기전용으로 만들고싶으면 readonly를 앞에써주면됨

type Ro<T> = {
  readonly [R in keyof T]: T[R];
};

이것도 ?와 마찬가지로 가져올 코드에 readonly가 붙어있다면 -를 붙여서 빼주고 가져올 수 있다.

interface Profile {
  readonly name?: string;
  readonly age?: number;
  readonly married?: boolean;
}

type Name = Profile["name"];

type R<T> = {
  -readonly [R in keyof T]-?: T[R];
};

const hun: Profile = {
  name: "hun",
  age: 3,
  married: false,
};

const hun2: R<Profile> = {
  name: "hun2",
  age: 3,
  married: false,
};
hun2.name = "change";

Record 타입

obj를 편하게 만들어주는타입

type Rc<K extends keyof any, T> = {
  [P in K]: T;
};

const a: Rc<string, number> = { a: 2, b: 3, c: 5 };

NonNullable 타입

null과 undefined는 빼고 가지고 오고 싶을때 사용하는 타입

type NonNullable<T> = T extends null | undefined ? never : T;

// 더 간단하게 intersection을 사용해서 단축평가로 나타낼수도있다.  
type NonNullable<T> = T & {};

03.5 - infer 타입 분석

infer는 조건부 타입 extends를 사용할 때만 사용할 수 있다.

조건부 타입의 조건식이 참으로 평가될때 infer [제네릭변수] 는 해당 타입을 추론하고 추론이 가능하면 제네릭 변수를 타입으로 사용할 수 있다.

infer는 ts가 알아서 추론하라는 뜻이다. TS의 추론능력을 믿고 앞에 infer를 쓰고 ?: 추론된 타입이 있을 경우 그 타입을 반환한다

function zip(
  x: number,
  y: string,
  z: boolean
): { x: number; y: string; z: boolean } {
  return { x, y, z };
}

// T extends (...args: any)=> any  이건 T는 함수여야 한다라는 뜻
type P<T extends (...args: any) => any> = T extends (...args: infer A) => any
  ? A
  : never;

// 매개변수의 타입을 가져오고 싶을 때 (변수는 타입으로 바로 사용할 수 없으므로 typeof를 붙여줘야한다.)
type Params = Parameters<typeof zip>; // [number, string, boolean]
type Params_1 = Params[0]; // number

// 리턴 타입을 가져오고 싶을 때
type Returns = ReturnType<typeof zip>; // {x: number, y: string, z: boolean}

비슷한 방법으로 infer를 사용한 타입이 생성자 타입과 인스턴스 타입도 가져올 수 있다.

class A {
  a: string;
  b: number;
  c: boolean;
  constructor(a: string, b: number, c: boolean) {
    this.a = a;
    this.b = b;
    this.c = c;
  }
}

const c = new A("123", 123, true);

// 생성자의 타입 가져오고 싶을 때
type C = ConstructorParameters<typeof A>;

// 인스턴스의 타입 가져오고 싶을 때
type I = InstanceType<typeof A>;

이외에도 여러가지 utility 타입이 있는데 TS로 구현이 안되는 것들은 intrinsic라고 적혀있다.

03.6 - 완전 복잡한 타입 분석하기(Promise와 Awaited 편)

TS는 Promise.all의 타입을 어떻게 추론할까?

const p1 = Promise.resolve(1) // Promise<number>
  .then((a) => a + 1) // Promise<number>
  .then((a) => a.toString()); // Promise<string>
const p2 = Promise.resolve(2);
const p3 = new Promise((res, rej) => {
  setTimeout(res, 1000);
});

Promise.all([p1, p2, p3]).then((result) => {
  console.log(result);
}); // [string, number, unknown]
  1. all의 타입이 뭐지?

    먼저 all 타입을 찾아보면 lib.es2015.promise.d.ts에 있다.

    interface PromiseConstructor {
    	
    	...
    
      all<T extends readonly unknown[] | []>(
        values: T
      ): Promise<{ -readonly [P in keyof T]: Awaited<T[P]> }>;
    
    	...
    
    }

    위의 예제에 따르면 values가 [p1, p2, p3]이므로 T = [p1, p2, p3]가 된다.

      all<[p1, p2, p3] extends readonly unknown[] | []>(
        values: [p1, p2, p3]
      ): Promise<{ -readonly [P in keyof [p1, p2, p3]]: Awaited<T[P]> }>;

    여기서 key of [p1, p2, p3] 는 ‘0’ | ‘1’ | ‘2’ | ‘length’ 가 된다

    그리고 in을 사용해서 Mapped type으로 나타냈으므로 P에는 ‘0’ | ‘1’ | ‘2’가 들어가므로 T

    는 T[0],T[1],T[2]가 들어간다. 또한 -readonly이므로 readonly 였던 p1,p2,p3가 풀려서 나온다

    { [P in keyof [p1, p2, p3]]: Awaited<T[P]> }는 아래와 같이 풀어줄 수 있다.

    {0: Awaited<p1> ,1: Awaited<p2> ,2: Awaited<p3>
    }
  2. Awaited 타입이 뭐지?

    그럼 이제 Awaited를 살펴보면

    type Awaited<T> = T extends null | undefined
      ? T // special case for `null | undefined` when not in `--strictNullChecks` mode
      : T extends object & { then(onfulfilled: infer F, ...args: infer _): any } // `await` only unwraps object types with a callable `then`. Non-object types are not unwrapped
      ? F extends (value: infer V, ...args: infer _) => any // if the argument to `then` is callable, extracts the first argument
        ? Awaited<V> // recursively unwrap the value
        : never // the argument to `then` was not callable
      : T; // non-object or non-thenable
    1. 첫번째 extends ? :

      조건문을 보면 T extends null | undefined ? T : … T가 null | undefined이면 T가 타입이된다. 하지만 T는 Promise1,2,3 이므로 T extends null | undefined는 false이다.

      그러므로 : 뒤인 … 으로 넘어간다.

    2. 두번째 extends ? :

      조건문을 보면 T extends object & { then(*onfulfilled*: infer F, ...*args*: infer _): any } T는 Promise 객체 이므로 extends object는 참이다.

      그리고 T extends { then(onfulfilled: infer F, ...args: infer _): any }는 T가 then()이라는 메서드가 있는지를 물어보는 것인데 then은 Promise객체이므로 만족한다. 그리고 then의 매개변수인 onfulfilled에 해당하는 타입이 F가 추론된다. 따라서 ? 뒤로 넘어간다.

      cf) 제약조건이 아닌 조건부타입에 사용된 extends이므로 infer를 통해 타입을 유추할 수 있다.

    3. 세번째 extends ? :

      조건문을 보면 F extends (value: infer V, ...args: infer _) => any 이고

      이때 F는 2번째 조건부타입에서 infer로 추론한 타입이다.

      Promise의 then 타입을 살펴보면

      // Promise의 then 메서드 타입
      interface Promise<T> {
        then<TResult1 = T, TResult2 = never>(
          onfulfilled?:
            | ((value: T) => TResult1 | PromiseLike<TResult1>)
            | undefined
            | null,
          onrejected?:
            | ((reason: any) => TResult2 | PromiseLike<TResult2>)
            | undefined
            | null
        ): Promise<TResult1 | TResult2>;
      }

      임을 알 수 있고, 매개변수 onfulfilled의 타입은 (*value*: T) => TResult1 | PromiseLike<TResult1>) | undefined | null 를 바탕으로 infer F를 추론한다.

      이때 F가 함수라면 (*value*: infer V, ...*args*: infer _) => any 를 만족하므로 Awaited<V> 타입을 반환한다.

      cf) PromiseLike타입이란?

      PromiseLike는 Promise가 정식 스펙이 되기 이전에 Promise를 구현하기 위한 다양한 라이브러리에서 존재했는데 이들 중 catch 구문 없이 Promise를 처리한 것들 이 있었고 TS는 이를 지원하기 위해 PromiseLike를 만들었다.

    Promise.all 메서드에서의 p1의 타입 추론 방법 따라가 보기

    Step1

    Promise.resolve(1) 에서는 T = number이므로 Promise 타입이 된다.

    .then((a) ⇒ a+1)에서는 T = number가 되고 리턴값인 TResults1 = number이므로 Promise타입이 된다.

    .then((*a*) => *a*.toString())에서는 T = number가 되고 T 리턴값인 TResults1 = string이므로

    Promise타입이 된다.

    p1은 최종적으로 Promise 타입이다.

    **Step2**

    Promise.all 타입인

    all<T extends readonly unknown[] | []>(
        values: T
      ): Promise<{ -readonly [P in keyof T]: Awaited<T[P]> }>;

    에서 T = [Promise, …]가 되고 mapped type에 의해서 T[’0’]은 Promise이되므로

    Awaited<Promise>을 분석하면 된다.

    Step3

    Awaited 타입인

    type Awaited<T> = T extends null | undefined
      ? T // special case for `null | undefined` when not in `--strictNullChecks` mode
      : T extends object & { then(onfulfilled: infer F, ...args: infer _): any } // `await` only unwraps object types with a callable `then`. Non-object types are not unwrapped
      ? F extends (value: infer V, ...args: infer _) => any // if the argument to `then` is callable, extracts the first argument
        ? Awaited<V> // recursively unwrap the value
        : never // the argument to `then` was not callable
      : T; // non-object or non-thenable

    에서 T = Promise이 되고 T extends null | undefined 가 false이므로 : 뒤로 넘어간다.

    넘어간 조건부 타입을 생략해주면 아래와 같다.

    type Awaited<T> = T extends object & { then(onfulfilled: infer F, ...args: infer _): any } // `await` only unwraps object types with a callable `then`. Non-object types are not unwrapped
      ? F extends (value: infer V, ...args: infer _) => any // if the argument to `then` is callable, extracts the first argument
        ? Awaited<V> // recursively unwrap the value
        : never // the argument to `then` was not callable
      : T; // non-object or non-thenable

    여기서 T = Promise는 object이면서 Promise는 then 메서드를 가지고 있으므로 T extends object & { then(onfulfilled: infer F, ...args: infer _): any } 가 true가 되서 ? 뒤로 넘어간다.

    이때 infer F로 F 타입 추론을 하게 되는데 then 메서드 타입을 살펴보면

    interface Promise<T> {
      then<TResult1 = T, TResult2 = never>(
        onfulfilled?:
          | ((value: T) => TResult1 | PromiseLike<TResult1>)
          | undefined
          | null,
        onrejected?:
          | ((reason: any) => TResult2 | PromiseLike<TResult2>)
          | undefined
          | null
      ): Promise<TResult1 | TResult2>;
    }

    인데 이때 T extends object & { then(onfulfilled: infer F, ...args: infer _): any } 에서 T는 Promise이였으므로 위의 코드에서 T = string이 된다. 따라서

    F = (value : stirng) ⇒ TResult1 | PromiseLike | undefined | null 이 된다.

    넘어간 조건부 타입을 한 번 더 생략해 주면 아래와 같이 남는다.

    type Awaited<T> = F extends (value: infer V, ...args: infer _) => any // if the argument to `then` is callable, extracts the first argument
        ? Awaited<V> // recursively unwrap the value
        : never // the argument to `then` was not callable

    이때 이전에 F가 (value : stirng) ⇒ TResult1 | PromiseLike | undefined | null 타입은 것을 구했으므로 F extends (value: infer V, ...args: infer _) => any 는 true이다.

    그리고 value: infer V 에서 V = string인 것을 알 수 있다.

    최종 적으로 마지막에는 Awaited<string> 타입이 반환된다.

    다시 Awaited으로 돌아가는데 이번에는 T = string이다.

    type Awaited<T> = T extends null | undefined
      ? T // special case for `null | undefined` when not in `--strictNullChecks` mode
      : T extends object & { then(onfulfilled: infer F, ...args: infer _): any } // `await` only unwraps object types with a callable `then`. Non-object types are not unwrapped
      ? F extends (value: infer V, ...args: infer _) => any // if the argument to `then` is callable, extracts the first argument
        ? Awaited<V> // recursively unwrap the value
        : never // the argument to `then` was not callable
      : T; // non-object or non-thenable

    T가 string 이므로 T extends null | undefined 는 false이고 : 뒤로 넘어간다.

    type Awaited<T> = T extends object & { then(onfulfilled: infer F, ...args: infer _): any } // `await` only unwraps object types with a callable `then`. Non-object types are not unwrapped
      ? F extends (value: infer V, ...args: infer _) => any // if the argument to `then` is callable, extracts the first argument
        ? Awaited<V> // recursively unwrap the value
        : never // the argument to `then` was not callable
      : T; // non-object or non-thenable

    T 가 원시타입인 string이므로 object 타입에 포함되어있지않고 T extends object & { then(*onfulfilled*: infer F, ...*args*: infer _): any } 는 false이고 : 뒤로 넘어간다.

    type Awaited<T> = T; // non-object or non-thenable

    최종적으로 Awaited의 타입은 string 타입을 반환한다.

    (후,,,,,,,,,,,,,,,,)

    💡 **Awaited 타입 활용하기**

    위의 예제를 보면 Awaited의 특징이 있다. Awaited<Promise> 같은경우 Promise가 아닌 T 타입으로 반환해준다는 것이다.
    이를 응용해서 중첩된 Promise타입의 경우 원시타입으로 풀어준다.
    ex) Awaited<Promise<Promise<Promise>>> = number

03.7 - 완전 복잡한 타입 분석하기(bind 편)

js에서 this에 내가 원하는 값을 바인딩해서 함수를 교체하는 bind 함수의 타입을 분석해보자

TS v5.1.6 기준으로 bind의 타입은 아래와 같다

bind<T>(this: T, thisArg: ThisParameterType<T>): OmitThisParameter<T>;
 
bind<T, A extends any[], B extends any[], R>(
    this: (this: T, ...args: [...A, ...B]) => R,
    thisArg: T,
    ...args: A
  ): (...args: B) => R;

하나씩 차근차근 살펴보면 먼저

bind의 첫번째 인자로 this가 넘어가고 이를 T타입이라고 제네릭으로 사용한다

두번째 인자로 thisArg가 ThisParameterType로 나온다.

type ThisParameterType<T> = T extends (this: infer U, ...args: never) => any
  ? U
  : unknown;

ThisParameterType를 살펴보면 조건부 타입으로 나타나져있다.

T가 함수 형태이며 this를 추론할 수 있다면 추론한 타입으로 반환하고, T가 함수 형태가 아니거나 추론할 수 없다면 unknown 타입을 반환한다

이렇게 T의 타입을 추출할 수 있다.

bind 타입의 반환값인 OmitThisParameter를 살펴보면

type OmitThisParameter<T> = unknown extends ThisParameterType<T>
  ? T
  : T extends (...args: infer A) => infer R
  ? (...args: A) => R
  : T;

여기는 조건부타입이 이중으로 사용되었다. 하나씩 살펴보면

첫번째 조건인 unknown extends ThisParameterType<T> 에서 ThisParameterType 가 unKnown이라면 T를 반환하고 아니면 : 뒤로 넘어간다. (: 로 간다는 것은 this의 타입 추론이 성공했다는 뜻)

두번째 조건인 T extends (...args: infer A) => infer R 에는 타입추론이 성공한 this의 타입이 T가 되므로 (...args: infer A) => infer R 를 통해 함수 T의 this를 제외한 나머지 타입들

즉, 매개변수 타입(A)과 리턴값 타입(R)을 추론한다.

추론이 성공하면 매개변수와 리턴값 타입이 그대로인 함수를 만들어서 반환하고 실패하면 T 타입을 반환한다.

즉, OmitThisParameter에는 기존의 this가 사라진다.

bind 메서드는 기존의 함수에 this를 내가 넣어준 인수로 변경한 새로운 함수를 반환하는 메서드인 것을 생각하면 this의 타입 추론 후 기존 this를 지워주는게 맞다

즉 기존의 함수에서 this만 없앤 타입을 추출할 수 있다

그리고 bind 의 두번째 이후로 오는 인자들은 새로만든 함수에 인자로 넣어준다.

function add(a: number, b: number, c: number, d: number): number {
  return a + b + c + d;
}

const add1 = add.bind(null);
add1(1, 2, 3, 4);

const add2 = add.bind(null, 1);
add2(2, 3, 4);

const add3 = add.bind(null, 1, 2);
add3(3, 4);

const add4 = add.bind(null, 1, 2, 3);
add4(4);

const add5 = add.bind(null, 1, 2, 3, 4);
add5();

// bind의 오버로딩된 타입
bind<T, A extends any[], B extends any[], R>(
    this: (this: T, ...args: [...A, ...B]) => R,
    thisArg: T,
    ...args: A
  ): (...args: B) => R;

03.8 - 완전 복잡한 타입 분석하기(flat 편)

배열을 펴주는 flat 메서드의 타입에 대해서 알아보자

interface Array<T> {
    flat<A, D extends number = 1>(this: A, depth?: D): FlatArray<A, D>[];
}

flat 타입을 알기 위해서는 FlatArray타입을 알아야 한다.

type FlatArray<Arr, Depth extends number> = {
  done: Arr;
  recur: Arr extends ReadonlyArray<infer InnerArr>
    ? FlatArray<
        InnerArr,
        [-1, 0, 1, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20][Depth]
      >
    : Arr;
}[Depth extends -1 ? "done" : "recur"];

하나씩 차근하근…

먼저 객체의 대괄호표기법 안에 들어가는 값을 살펴보자 [Depth extends -1 ? "done" : "recur"]

이것은 Depth가 -1 이면 { }[“done”] 타입을 아니면 { }[”recur”] 타입을 말한다

(대괄호 안에도 조건부타입이,,,오호,,)

  1. Depth가 -1 일때

    FlatArray[’done’] 타입을 반환해서 Arr를 반환한다. (Arr를 flat해줬다는 뜻이다.)

  2. Depth가 -1이 아닐때

    FlatArray[’recur’] 타입을 반환해서 재귀호출에 들어간다. (Arr에 아직 flat 할 게 남았다는 것으로 flat해주고 depth를 1 빼주고 다시 FlatArray로 돌아가 재귀호출을 해야한다.)

    Arr가 ReadonlyArray<infer InnerArr>에 속한다면 재귀에 들어가고 아니면 Arr를 반환한다.

    이를 위해 ReadonlyArray<T> 타입을 알아보면

    interface ReadonlyArray<T> {
      readonly [n: number]: T;
    }

    로 읽기버전인 number[]의 요소 타입이 T이다.

    ReadonlyArray<infer InnerArr> 에서 <infer InnerArr> 는 배열의 요소의 타입을 추론한다

    예를들어 [1, 2, 3, [1, 2], [[1], [2]]]; 같은 배열에서 요소의 타입은 number | number[] | number[][] 이다.

    추론에 성공하면 추론한 타입인 InnerArr를 첫번째 인수에 넣어 FlatArray 재귀를 호출한다.

    이때 한차원 벗겨지는데 Arr ⇒ InnerArr로 갈때 가장 겉에 있는 차원이 하나 벗겨졌기 때문이다.

    그 다음 두번째 인수인 Depth를 1 빼줘야한다

    하지만 TS에서는 빼기 연산이 안된다.

    type A = 3 - 1 // error

    이를 해결하기 위해 꼼수를 썼는데 그게 바로

    [-1, 0, 1, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20][Depth]

    예를 들어 Depth = 1 이면 배열의 1번째 index값인 0이 반환될 것이다.

    즉, Depth = x 이면 배열[x] = x-1 가 되서 1을 빼주는 효과가 있다.

    물론 위에 적혀있는 x 가 21까지만 가능하다. 22차원 이상의 배열에서는 타입을 추론할 수 없지만 그 이상 차원을 사용해야되면 생각을 다시해봐야하지 않을까..?

와… 기본강좌인데 진짜 많은 걸 배우고 어려웠다,,,,,댑악,,, 주말에 날 잡고 TS의 꼼수들 찾아보는 시간을 가져야겠다ㅋㅋㅋ 블로그에 글쓸 소스 추가~

profile
과정을 적는 곳

1개의 댓글

comment-user-thumbnail
2023년 8월 8일

유익한 자료 감사합니다.

답글 달기