-
[Type-challenges] 18, 43, 189, 268, 533, 898, 3057, 3060, 3312, 2Front-End/TypeScript 2024. 2. 23. 14:57
🍀 목차
18 - Length of Tuple
43 - Exclude
189 - Awaited
268 - If
533 - Concat
898 - Includes
3057 - Push
3060 - Unshift
3312 - Parameters
2 - Get Return Type18 - Length of Tuple
// 배열(튜플)을 받아 길이를 반환하는 제네릭 Length<T>를 구현하세요. // 예시 type tesla = ['tesla', 'model 3', 'model X', 'model Y'] type spaceX = ['FALCON 9', 'FALCON HEAVY', 'DRAGON', 'STARSHIP', 'HUMAN SPACEFLIGHT'] type teslaLength = Length<tesla> // expected 4 type spaceXLength = Length<spaceX> // expected 5
/* _____________ 여기에 코드 입력 _____________ */ type Length<T> = any /* _____________ 테스트 케이스 _____________ */ import type { Equal, Expect } from '@type-challenges/utils' const tesla = ['tesla', 'model 3', 'model X', 'model Y'] as const const spaceX = ['FALCON 9', 'FALCON HEAVY', 'DRAGON', 'STARSHIP', 'HUMAN SPACEFLIGHT'] as const type cases = [ Expect<Equal<Length<typeof tesla>, 4>>, Expect<Equal<Length<typeof spaceX>, 5>>, // @ts-expect-error Length<5>, // @ts-expect-error Length<'hello world'>, ]
type Length<T extends readonly any[]> = T['length'];
readonly
키워드로 읽기 전용임을 나타낸 후, 안전하게length
속성을 추출할 수 있도록 한다. readonly를 사용하지 않으면 length 속성에 대한 접근을 허용하지 않고 오류를 발생시킨다.43 - Exclude
// T에서 U에 할당할 수 있는 타입을 제외하는 // 내장 제네릭 Exclude<T, U>를 이를 사용하지 않고 구현하세요. // 예시 type Result = MyExclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'
/* _____________ 여기에 코드 입력 _____________ */ type MyExclude<T, U> = any /* _____________ 테스트 케이스 _____________ */ import type { Equal, Expect } from '@type-challenges/utils' type cases = [ Expect<Equal<MyExclude<'a' | 'b' | 'c', 'a'>, 'b' | 'c'>>, Expect<Equal<MyExclude<'a' | 'b' | 'c', 'a' | 'b'>, 'c'>>, Expect<Equal<MyExclude<string | number | (() => void), Function>, string | number>>, ]
type MyExclude<T, U> = T extends U ? never : T;
T에 오는 타입 중 U 타입은 제외하겠다고 정의하면 된다.
never
타입은 값을 반환할 수 없는 타입이다. 에러를 던지거나, 무한 루프 등 정상적으로 종료되지 않을 때 사용된다. 또 다른 특징은 모든 타입의 하위 타입이란 것이다. never 자신을 제외한 어떤 타입도 never에 할당될 수 없다(any 조차도). 이 특징들은 TypeScript에서 다양한 패턴을 만들어 타입을 잡아주는 역할이 된다. 여기에서는 never을 반환시킴으로써 T에는 U에 할당 가능한 타입이 없게 한다. 결국, U를 제외시킨 T가 반환된다.189 - Awaited
// Promise와 같은 타입에 감싸인 타입이 있을 때, // 안에 감싸인 타입이 무엇인지 어떻게 알 수 있을까요? //예시: Promise<ExampleType>이 있을 때, ExampleType을 어떻게 얻을 수 있을까요? type ExampleType = Promise<string> type Result = MyAwaited<ExampleType> // string
/* _____________ 여기에 코드 입력 _____________ */ type MyAwaited<T> = any /* _____________ 테스트 케이스 _____________ */ import type { Equal, Expect } from '@type-challenges/utils' type X = Promise<string> type Y = Promise<{ field: number }> type Z = Promise<Promise<string | number>> type Z1 = Promise<Promise<Promise<string | boolean>>> type T = { then: (onfulfilled: (arg: number) => any) => any } type cases = [ Expect<Equal<MyAwaited<X>, string>>, Expect<Equal<MyAwaited<Y>, { field: number }>>, Expect<Equal<MyAwaited<Z>, string | number>>, Expect<Equal<MyAwaited<Z1>, string | boolean>>, Expect<Equal<MyAwaited<T>, number>>, ] // @ts-expect-error type error = MyAwaited<number>
테스트 케이스 타입 Z1처럼 중첩된 Promise의 타입도 알 수 있어야 한다(재귀적).
extends
를 사용할 때infer
키워드를 사용할 수 있다. extends로 조건을 서술하고 infer로 타입을 추론할 수 있다.type MyAwaited<T> = T extends Promise<infer U> ? U extends Promise<any> ? MyAwaited<U> : U : never;
처음에는 위처럼 작성했다. 그런데 테스트 케이스 타입 T를 통과하지 못했다.
type T = { then: (onfulfilled: (arg: number) => any) => any }
T 타입처럼
then
만 구현된 유사 프로미스(?)는 프로미스가 정식적으로 채택되기 전 사용되었던 다양한 라이브러리가 가지던 형태라고 한다. TypeScript는 이를 지원하기 위해 좀 더 광범위한 프로미스인PromiseLike
를 추가하게 되었다.type MyAwaited<T> = T extends PromiseLike<infer U> ? U extends PromiseLike<any> ? MyAwaited<U> : U : never;
-Like
타입에 대한 자세한 내용은 해당 포스팅을 읽었다.268- If
// 조건 C가 참일 때 반환하는 타입 T, // 거짓일 때 반환하는 타입 F를 받는 타입 If를 구현하세요. // C는 true 또는 false이고, T와 F는 아무 타입입니다. // 예시 type A = If<true, 'a', 'b'> // expected to be 'a' type B = If<false, 'a', 'b'> // expected to be 'b'
/* _____________ 여기에 코드 입력 _____________ */ type If<C, T, F> = any /* _____________ 테스트 케이스 _____________ */ import type { Equal, Expect } from '@type-challenges/utils' type cases = [ Expect<Equal<If<true, 'a', 'b'>, 'a'>>, Expect<Equal<If<false, 'a', 2>, 2>>, ] // @ts-expect-error type error = If<null, 'a', 'b'>
type If<C extends Boolean, T, F> = C extends true ? T : F;
533 - Concat
// JavaScript의 Array.concat 함수를 타입 시스템에서 구현하세요. // 타입은 두 인수를 받고, 인수를 왼쪽부터 concat한 새로운 배열을 반환해야 합니다. // 예시 type Result = Concat<[1], [2]> // expected to be [1, 2]
/* _____________ 여기에 코드 입력 _____________ */ type Concat<T, U> = any /* _____________ 테스트 케이스 _____________ */ import type { Equal, Expect } from '@type-challenges/utils' const tuple = [1] as const type cases = [ Expect<Equal<Concat<[], []>, []>>, Expect<Equal<Concat<[], [1]>, [1]>>, Expect<Equal<Concat<typeof tuple, typeof tuple>, [1, 1]>>, Expect<Equal<Concat<[1, 2], [3, 4]>, [1, 2, 3, 4]>>, Expect<Equal<Concat<['1', 2, '3'], [false, boolean, '4']>, ['1', 2, '3', false, boolean, '4']>>, ] // @ts-expect-error type error = Concat<null, undefined>
type Concat<T extends readonly any[], U extends readonly any[]> = [...T,...U];
898 - Includes
// JavaScript의 Array.includes 함수를 타입 시스템에서 구현하세요. // 타입은 두 인수를 받고, true 또는 false를 반환해야 합니다. // 예시 type isPillarMen = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'> // expected to be `false`
/* _____________ 여기에 코드 입력 _____________ */ type Includes<T extends readonly any[], U> = any /* _____________ 테스트 케이스 _____________ */ import type { Equal, Expect } from '@type-challenges/utils' type cases = [ Expect<Equal<Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Kars'>, true>>, Expect<Equal<Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'>, false>>, Expect<Equal<Includes<[1, 2, 3, 5, 6, 7], 7>, true>>, Expect<Equal<Includes<[1, 2, 3, 5, 6, 7], 4>, false>>, Expect<Equal<Includes<[1, 2, 3], 2>, true>>, Expect<Equal<Includes<[1, 2, 3], 1>, true>>, Expect<Equal<Includes<[{}], { a: 'A' }>, false>>, Expect<Equal<Includes<[boolean, 2, 3, 5, 6, 7], false>, false>>, Expect<Equal<Includes<[true, 2, 3, 5, 6, 7], boolean>, false>>, Expect<Equal<Includes<[false, 2, 3, 5, 6, 7], false>, true>>, Expect<Equal<Includes<[{ a: 'A' }], { readonly a: 'A' }>, false>>, Expect<Equal<Includes<[{ readonly a: 'A' }], { a: 'A' }>, false>>, Expect<Equal<Includes<[1], 1 | 2>, false>>, Expect<Equal<Includes<[1 | 2], 1>, false>>, Expect<Equal<Includes<[null], undefined>, false>>, Expect<Equal<Includes<[undefined], null>, false>>, ]
type Includes<T extends readonly any[], U> = U extends T[number] ? true : false;
위처럼 작성하니 튜플 타입의 테스트케이스를 통과하지 못했다.
type Includes<T extends readonly any[], U> = T extends [infer First, ...infer Rest] ? Equal<U, First> extends true ? true : Includes<Rest, U> : false;
튜플이나 배열의 첫 번째 요소 타입을 First로 추론, 나머지 요소는 Rest로 추론한다. U가 배열이나 튜플의 첫 요소들과 같은지 비교한다. 일치하지 않다면 재귀적으로 Rest로 Includes 타입을 호출하여 확인해 나간다.
3057 - Push
// Array.push의 제네릭 버전을 구현하세요. // 예시 type Result = Push<[1, 2], '3'> // [1, 2, '3']
/* _____________ 여기에 코드 입력 _____________ */ type Push<T, U> = any /* _____________ 테스트 케이스 _____________ */ import type { Equal, Expect } from '@type-challenges/utils' type cases = [ Expect<Equal<Push<[], 1>, [1]>>, Expect<Equal<Push<[1, 2], '3'>, [1, 2, '3']>>, Expect<Equal<Push<['1', 2, '3'], boolean>, ['1', 2, '3', boolean]>>, ]
type Push<T extends any [], U> = [...T, U];
3060 - Unshift
// Array.unshift의 타입 버전을 구현하세요. // 예시 type Result = Unshift<[1, 2], 0> // [0, 1, 2,]
/* _____________ 여기에 코드 입력 _____________ */ type Unshift<T, U> = any /* _____________ 테스트 케이스 _____________ */ import type { Equal, Expect } from '@type-challenges/utils' type cases = [ Expect<Equal<Unshift<[], 1>, [1]>>, Expect<Equal<Unshift<[1, 2], 0>, [0, 1, 2]>>, Expect<Equal<Unshift<['1', 2, '3'], boolean>, [boolean, '1', 2, '3']>>, ]
type Unshift<T extends any [], U> = [U, ...T];
3057(Push) 문제와 3060(Unshift) 문제의 경우
any []
가 아닌unknown []
을 쓴 분도 있어 차이를 알아보았다.unknown 타입의 경우 any 타입과 유사하게 어떤 값이든 올 수 있음을 의미하는 동시에 "개발자에게 엄격한 타입 검사를 강제하는 의도"를 담고 있다. 타입 검사를 강제하고 타입이 식별된 후에 사용할 수 있기 때문에 any보다 안전하다. 예를 들어, any로 선언된 변수의 length 속성을 참조하면 에러가 나지 않지만 unknown 타입은 에러가 난다(더 안전하고 엄격하다).
// 안전하게 타입을 확인하고 싶다면 any보다는 unknown을 쓴다. type Push<T extends unknown [], U> = [...T, U]; type Unshift<T extends unknown [], U> = [U, ...T];
3312 - Parameters
// 내장 제네릭 Parameters<T>를 이를 사용하지 않고 구현하세요. // 예시 const foo = (arg1: string, arg2: number): void => {} type FunctionParamsType = MyParameters<typeof foo> // [arg1: string, arg2: number]
/* _____________ 여기에 코드 입력 _____________ */ type MyParameters<T extends (...args: any[]) => any> = any /* _____________ 테스트 케이스 _____________ */ import type { Equal, Expect } from '@type-challenges/utils' function foo(arg1: string, arg2: number): void {} function bar(arg1: boolean, arg2: { a: 'A' }): void {} function baz(): void {} type cases = [ Expect<Equal<MyParameters<typeof foo>, [string, number]>>, Expect<Equal<MyParameters<typeof bar>, [boolean, { a: 'A' }]>>, Expect<Equal<MyParameters<typeof baz>, []>>, ]
type MyParameters<T> = T extends (...args : infer U) => unknown ? U : never;
2 - Get Return Type
// 내장 제네릭 ReturnType<T>을 이를 사용하지 않고 구현하세요. // 예시 const fn = (v: boolean) => { if (v) return 1 else return 2 } type a = MyReturnType<typeof fn> // should be "1 | 2"
/* _____________ 여기에 코드 입력 _____________ */ type MyReturnType<T> = any /* _____________ 테스트 케이스 _____________ */ import type { Equal, Expect } from '@type-challenges/utils' type cases = [ Expect<Equal<string, MyReturnType<() => string>>>, Expect<Equal<123, MyReturnType<() => 123>>>, Expect<Equal<ComplexObject, MyReturnType<() => ComplexObject>>>, Expect<Equal<Promise<boolean>, MyReturnType<() => Promise<boolean>>>>, Expect<Equal<() => 'foo', MyReturnType<() => () => 'foo'>>>, Expect<Equal<1 | 2, MyReturnType<typeof fn>>>, Expect<Equal<1 | 2, MyReturnType<typeof fn1>>>, ] type ComplexObject = { a: [12, 'foo'] bar: 'hello' prev(): number } const fn = (v: boolean) => v ? 1 : 2 const fn1 = (v: boolean, w: any) => v ? 1 : 2
테스트 케이스 fn, fn1을 보면 매개변수가 존재하는 함수도 받아야 한다.
type MyReturnType<T> = T extends (...args : any[]) => infer U ? U : never;
아래 코드처럼
any
를unknown
타입으로 바꾸면 에러가 뜨는데, unknown []으로 argument를 설정하게 되면 함수 타입 T의 매개변수는 unknown이 올 것이라 예측하게 된다(당연하다). 그러나fn
은 number 타입의 매개변수를 받고 있다. unknown이 올 줄 알았는데 number 타입이 할당되었으므로 거짓이다.MyReturnType<typeof fn>
은 never이 되는 것.type MyReturnType<T> = T extends (...args : unknown []) => infer U ? U : never; const fn = (v: number) => v ? 1 : 2; type test = MyReturnType<typeof fn>; // test는 never이다.
any에 대해서는 모든 타입을 허용하므로 컴파일러가 어떠한 유형의 값도 올바르다고 가정한다. 오류가 발생하지 않는다.
type MyReturnType<T> = T extends (...args : unknown []) => infer U ? U : never; const fn = (v: any) => v ? 1 : 2; type test = MyReturnType<typeof fn>; // test는 1 | 2 이다.
만약, any [] 였다면?
T는 매개변수가 any 타입이 할당될 때만 참이 되고 number 타입의 매개변수가 할당되었으므로 참이 된다, any 타입은 모든 타입이 될 수 있기 때문이다.
any와 unknown 모두 어떤 값이든 할당 가능하나, unknown은 값 사용 전에 적절한 유형으로 바꿔주어야 한다. 그렇기에 타입스크립트 컴파일러는 명시적으로 값을 지정하거나 확인하라고 오류를 발생시킨다.
참고자료
우아한 타입스크립트 with 리액트 - never 타입, infer를 활용해서 타입 추론하기, unknown 타입, 타입
https://yceffort.kr/2021/11/array-arraylike-promise-promiselike
https://github.com/type-challenges/type-challenges/blob/main/questions/00268-easy-if/README.ko.md
https://github.com/type-challenges/type-challenges/blob/main/questions/03057-easy-push/README.ko.md