Today I Learned_230422

4 분 소요

TypeScript

인프런 타입스크립트 강의 수강

flat

  • 배열의 한 차원을 낮춰 주는 메소드
  • bind와 유사하게 파라미터의 한계가 있다.
const a = [1, 2, 3, [1, 2], [[1], [2]]].flat(); // [1, 2, 3, 1, 2, [1], [2]]
const a1 = [1, 2, 3, [1, 2], [[1], [2]]].flat(2); // [1, 2, 3, 1, 2, 1, 2]
const b = [1, 2, 3, [1, 2]].flat(); //[1, 2, 3, 1, 2];
  • 차원을 낮추려면 일단 차원을 알아내는 기능도 필요함

flat 메소드의 구현부(es2019부터 있음)

flat<A, D extends number = 1>(
        this: A,
        depth?: D
    ): FlatArray<A, D>[]
  • 그렇다면 FlatArray가 어떻게 구현되어있는지??

FlatArray 구현부

type FlatArray<Arr, Depth extends number> = {
    "done": Arr,
    "recur": Arr extends ReadonlyArray<infer InnerArr>
        ? FlatArray<InnerArr, [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20][Depth]>
        : Arr
}[Depth extends -1 ? "done" : "recur"];
  • 원래는 객체를 타이핑 한 것인데 타이핑하면서 뒤에 바로 속성 접근자 넣어둠
  • depth가 -1이면 “done”값(Arr)을 가져온다.
  • depth가 -1이 아니면 “recur” 값을 가져온다.
  • type에서는 빼기가 안된다. (type c = 3-1 이거 안됨)
  • flat은 차원을 하나 낮춘 값을 넘겨줘야 하는데, type에서 빼기가 안되니까 어떻게 넘겨줘야 할지… → 꼼수를 써버렸다
  • FlatArray<InnerArr, [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20][Depth]>
  • depth가 1이면 0을 리턴하고, 2면 1을 리턴하는 구조
  • 그래서 depth가 22 이상이면 표현이 불가능하다.
  • 근데 20차원 이상 배열을 쓸 사람이 있을까.. 없을듯
  • 타입스크립트의 언어적 한계 - 모든 케이스는 고려하지 않았지만 99%의 케이스를 고려한 설계

아래와 같은 방식으로 변환이 된다고 보면 된다.

const c = [1, 2, 3, [1, 2], [[1], [2]]].flat(2); // [1, 2, 3, 1, 2, [1], [2]]
// FlatArray<(number[] | number[][] | number[][][]), 2>[]
// FlatArray<(number | number[] | number[][]), 1>[]
// FlatArray<(number | number | number[]), 1>[]
// FlatArray<(number), -1>[]
// number[]

타입 가져오는 방법

type A = {
    name: string,
    age: number,
}

type B = A["age"];  //number

Never

type IsNever<T> = [T] extends [never] ? true : false;
type A1 = IsNever<never>;   //true
type A2 = IsNever<boolean>; //false

type IsNever2<T> = T extends never ? true : false;
type B1 = IsNever<never>;   //never 여기 IDE에서는 true인데??
type B2 = IsNever<boolean>; //falseㅇ
  • never는 공집합에 해당
  • 타입 매개변수랑 union이 만나면 분배법칙(distributed)이 실행됨
  • 그런데 never가 union? - never는 never 자체로 Union이다.
type IsNever2<T> = never extends never ? true : false;

이렇게 들어가면 never extends never가 true가 나와야 하는데 never가 나옴

type IsNever<T> = [T] extends [never] ? true : false;
type IsNever3<T> = { type: T } extends { type:never} ? true : false;

이러면 분배법칙을 막을 수 있음

never extends never는 never이다.

typescript에서의 분배법칙 관련 : https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types

interface VO {
    value: any;
}

// T != VO
// T는 VO의 부분집합이다
const returnVO = <T extends VO>(): T => {
    return { value: 'test'};    // 안됨. T의 부분집합은 다 여기에 들어갈 수 있다.
}
// 에러 메시지 : 'T' could be instantiated with a different subtype of constraint 'VO'.
  • 부분집합이 들어올 수 있기에 안된다.
interface VO {
    value: any;
}

const obj: VO = {value: 'hi', what: 123} // 객체에 리터럴을 바로 넣으면 잉여속성검사가 바로 발동 -> 에러

// 이러면 넣을 수 있다.
const obj = {value: 'hi', what:123}
const a: VO = obj; //리터럴이 아닌 참조를 넣으면 잉여 속성 검사가 발생하지 않는다.
// never도 boolean의 부분집합이 될 수 있다.
// 따라서 arg: T= false가 안 된다.
function onlyBoolean<T extends boolean> (arg: T=false): T {
    return arg;
}

타입스크립트에서는 never의 속성 때문에 에러가 나는 경우가 많다. 이 부분 주의할 것!

Intersection, Union

type Union<T> = T extends {a: infer U, b:infer U} ? U : never;
type Union2<T> = T extends {a: () => infer U, b: () => infer U} ? U : never;
type Result1 = Union<{a: 1 | 2, b: 2 | 3}>;

// intersection을 만드는 특이한 방법
// 공변성과 반공변성이 들어가는 개념 - 같은 타입을 Infer하면 교집합이 된다.
type Intersection<T> = T extends {
    a: (pa: infer U) => void,
    b: (pb: infer U) => void
} ? U : never;
type Result2 = Intersection<{ a(pa: 1 | 2): void, b(pb: 2 | 3): void }>;

반환값과 속성에다가 infer 같은값을 하면 합집합이 되고, 매개변수에 넣으면 교집합이 된다.

⇒ 함수의 공변성과 반공변성을 이해해야 한다.

  • 함수의 매개변수 부분은 반공변적
    • 반공변적인 부분은 교집합이 된다
  • 매개변수가 아닌것은 대부분 공변적
    • 공변적인 부분은 합집합이 된다
  • extends는 같은 것이 아니다.

지연 평가

// 여기 안에 never가 숨어있다고 생각해야 함
// T는 string이어도 되고, number여도 되고, never여도 된다.
function double<T extends string | number> ( x : T) : T extends string? string : number {
    return x; //T extends string ? string : number
}

논리적으로는 문제가 없는 코드이다. string 또는 number가 들어올 것이기 때문에.

  • T extends string? string : number → 사람의 눈으로 봤을 때는 한번에 결정된다.
  • 타입스크립트는 근데 이걸 사람처럼 타입을 결정하지 않는다.
  • 이걸 바로 평가할 수 없어서 가장 마지막에 평가한다. → 타입스크립트 컴파일러의 한계
  • 그래서 타입스크립트는 타입을 T extends string ? string : number 로 계속 남겨 둔다.
  • conditional type을 보면 함부로 판단하지 말고 그냥 놔둬야 한다.

해결 방법?

지연 평가(가장 늦게 평가하는것)를 막는다.

function double
<T extends ([T] extends string | number ? string : number)> 
( x : T) : [T] extends string? string : number {
    return x;
}

원리 : 배열로 감싸면 타입스크립트 원래 동작과 다르게 동작하는 몇몇 부분이 있다. (동작을 너무 깊게 알려고 하지 말자 - 컴파일러나 엔진의 동작은 버전별로 동작이 달라지기 때문에…)

일단은 마지막의 conditinal 부분이 지연평가 되는 부분을 막는 것이다.

function double
<T extends string | number> ( x : T) : T extends string? string : number {
    return x as any;
}

as를 붙여도 되긴 함

꼭 return이 아니어도 conditional type은 지연평가가 발생한다.

function double
<T extends string | number> ( x : T){
    const a: T extends string ? string : number = x;
    return x;
}

카테고리:

업데이트: