Partial의 기능은 원래 필수였던 값들을 옵션값으로 다 변경해준다.
type Partial<T> = {
[P in keyof T]?: T[P];
};
인덱스 시그니처를 사용해서 맵드 타입으로 T객체의 키값들 키로 갖고 ([P in keyof T]
) 옵셔널값으로 객체의 키값에 따른 value도 가져온다(T[P]
)
이렇게 되면 타입이 느슨해져서 Partial보다는 Pick, Omit 사용을 지향하자
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,
};
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,
};
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 & {};
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라고 적혀있다.
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]
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>
}
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
첫번째 extends ? :
조건문을 보면 T extends null | undefined ? T : …
T가 null | undefined이면 T가 타입이된다. 하지만 T는 Promise1,2,3 이므로 T extends null | undefined는 false이다.
그러므로 : 뒤인 … 으로 넘어간다.
두번째 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를 통해 타입을 유추할 수 있다.
세번째 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
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;
배열을 펴주는 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”] 타입을 말한다
(대괄호 안에도 조건부타입이,,,오호,,)
Depth가 -1 일때
FlatArray[’done’] 타입을 반환해서 Arr를 반환한다. (Arr를 flat해줬다는 뜻이다.)
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의 꼼수들 찾아보는 시간을 가져야겠다ㅋㅋㅋ 블로그에 글쓸 소스 추가~
유익한 자료 감사합니다.