Today I Learned_230422
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;
}