-
[Type-challenges] 난이도 Easy - (1)Front-End(Web)/Typescript 2023. 1. 24. 18:21반응형
Github 챌린지 문제들을 풀고 관련된 내용을 정리하면서, 부족했던 타입스크립트 기본지식을 다지고자 한다. (주 1-2회)
https://github.com/type-challenges/type-challenges
📘 목차 - Easy
- Pick
- Readonly
- Tuple to Object
- First of Array
- Length of Tuple
- Exclude
- Awaited
📘 문제 및 풀이
1. Pick
Implement the built-in Pick<T, K> generic without using it.
Constructs a type by picking the set of properties K from T
For example:interface Todo { title: string description: string completed: boolean } type TodoPreview = MyPick<Todo, 'title' | 'completed'> const todo: TodoPreview = { title: 'Clean room', completed: false, } /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/utils' type cases = [ Expect<Equal<Expected1, MyPick<Todo, 'title'>>>, Expect<Equal<Expected2, MyPick<Todo, 'title' | 'completed'>>>, // @ts-expect-error MyPick<Todo, 'title' | 'completed' | 'invalid'>, ] interface Todo { title: string description: string completed: boolean } interface Expected1 { title: string } interface Expected2 { title: string completed: boolean }
🖌 풀이
type MyPick<T, K extends keyof T> = { [P in K]: T[P] }
Pick 유틸리티 타입을 직접 작성하는 코드다. 제네릭의 인자를 각각 T(원본 타입), K(반환할 타입의 key들) 로 활용할 것이다.
반환할 타입을 Object로 먼저 설정해준다. 이 타입의 key를 P(타입)로 명시하고, key는 K에서, value는 T[P]로 가져온다.
마지막으로, K가 T의 키값임을 명시하기 위해 keyof T 타입을 extends로 "제약"시켜준다. (에러 케이스)
* keyof
Object 타입의 키값들을 유니온 타입으로 반환하는 문법
interface Person { age: number; name: string; } type PersonKeys = keyof Person; // "age" | "name"
* extends
통상적으로 interface의 확장이나 상속을 위한 문법으로 알고 있었다.
하지만, 제네릭 인자에 대해서는 타입을 제약하기 위한 문법으로도 사용된다. (참고링크)
추가로, extends keyof는 일부 키값만 활용해도 에러가 발생하지 않으나, in keyof 는 모든 키를 사용하지 않으면 에러가 발생하는 차이가 있었다.
2. Readonly
Implement the built-in Readonly<T> generic without using it.
Constructs a type with all properties of T set to readonly, meaning the properties of the constructed type cannot be reassigned.
For example:interface Todo { title: string description: string } const todo: MyReadonly<Todo> = { title: "Hey", description: "foobar" } todo.title = "Hello" // Error: cannot reassign a readonly property todo.description = "barFoo" // Error: cannot reassign a readonly property /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/utils' type cases = [ Expect<Equal<MyReadonly<Todo1>, Readonly<Todo1>>>, ] interface Todo1 { title: string description: string completed: boolean meta: { author: string } }
🖌 풀이
type MyReadonly<T extends object> = { readonly [P in keyof T]: T[P] }
Readonly 유틸리티 타입을 직접 작성하는 코드다.
반환타입도 Object이며, key는 T의 in keyof로 가져오고 여기서 readonly 문법을 통해 읽기 전용 프로퍼티임을 명시해준다.
* readonly
읽기 전용 프로퍼티를 설정하기 위한 문법으로, 선언 후 변경이 불가능한 프로퍼티들을 설정하기 위해 사용된다.
Class 초기화, interface 키 값 등에 적용할 수 있다. (참고링크)
3. Tuple to Object
Given an array, transform it into an object type and the key/value must be in the provided array.
For example:const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const type result = TupleToObject<typeof tuple> // expected { 'tesla': 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'} /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/utils' const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const const tupleNumber = [1, 2, 3, 4] as const const tupleMix = [1, '2', 3, '4'] as const type cases = [ Expect<Equal<TupleToObject<typeof tuple>, { tesla: 'tesla'; 'model 3': 'model 3'; 'model X': 'model X'; 'model Y': 'model Y' }>>, Expect<Equal<TupleToObject<typeof tupleNumber>, { 1: 1; 2: 2; 3: 3; 4: 4 }>>, Expect<Equal<TupleToObject<typeof tupleMix>, { 1: 1; '2': '2'; 3: 3; '4': '4' }>>, ] // @ts-expect-error type error = TupleToObject<[[1, 2], {}]>
🖌 풀이
type TupleToObject<T extends readonly (string | number | symbol)[]> = { [P in T[number]]: P }
튜플 타입을 받아서 각 요소를 key, value로 가지는 객체 타입을 반환해야한다.
T[number] 로 튜플의 각 요소를 인덱싱해서 P라는 타입으로 지정한 뒤, 이를 key와 value로 넣어준다.
제네릭에서 튜플타입을 받기 위해 readonly any[] 로 제약하지만, 에러 케이스처럼 튜플 요소들 외 타입을 제외하기 위해 any를 더 구체화한다. (string | number | symbol)[]
* Tuple(튜플)
튜플은 서로 다른 타입을 함께 가질 수 있는 배열이다. 튜플 타입을 선언할 때, 엘리먼트의 타입과 개수를 설정할 수 있다.
let tuple: [string, number] tuple = ["hi", 200]; // ok! tuple = ["hi", "400"]; // error
4. First of Array
Implement a generic First<T> that takes an Array T and returns its first element's type.
For example:type arr1 = ['a', 'b', 'c'] type arr2 = [3, 2, 1] type head1 = First<arr1> // expected to be 'a' type head2 = First<arr2> // expected to be 3 /* _____________ Test Cases _____________ */ import type { Equal, Expect } from '@type-challenges/utils' type cases = [ Expect<Equal<First<[3, 2, 1]>, 3>>, Expect<Equal<First<[() => 123, { a: string }]>, () => 123>>, Expect<Equal<First<[]>, never>>, Expect<Equal<First<[undefined]>, undefined>>, ] type errors = [ // @ts-expect-error First<'notArray'>, // @ts-expect-error First<{ 0: 'arrayLike' }>, ]
🖌 풀이
type First<T extends any[]> = T extends [] ? never : T[0]
제네릭으로 받은 배열(T)의 첫 번째 인자의 타입을 반환하면 된다.
단, 3번째 케이스처럼 빈 배열인 경우엔 never를 반환해야 하므로, 조건부 타입(Conditional Types) 으로 지정해준다.
* 조건부 타입(Conditional Types)
입력된 제네릭 타입에 따라 타입을 결정하는 문법이다. 삼항연산자와 비슷한 문법이며, extends 키워드로 타입을 체크한다. (참고문서)
5. Length of Tuple
For given a tuple, you need create a generic Length, pick the length of the tuple
For example: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 /* _____________ Test Cases _____________ */ 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']
튜플은 배열처럼 보이지만 아래처럼 기술될 수 있다. 즉, 여기서 length 값이 튜플의 길이에 해당한다. (참고링크)
{ 0: 'sun', 1: 'mon', 2: 'tue', 3: 'wed', 4: 'thu', 5: 'fri', 6: 'sat', readonly length: 7, }
6. Exclude
Implement the built-in Exclude<T, U>
* Exclude from T those types that are assignable to U
For example:type Result = MyExclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c' /* _____________ Test Cases _____________ */ 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
조건부 타입의 특징인 분산 조건부 타입(Distributive Conditional Types) 를 활용한 풀이이다.
유니온 타입(A | B | C)에 대해 조건부(T extends U ? X : Y)를 인스턴스화 하면 각 타입 인수에 대한 조건부로 걸린다. (참고링크)
(A extends U ? X : Y | B extends U ? X : Y | C extends U ? X : Y)
7. Awaited
If we have a type which is wrapped type like Promise. How we can get a type which is inside the wrapped type?
For example: if we have Promise<ExampleType> how to get ExampleType?type ExampleType = Promise<string> type Result = MyAwaited<ExampleType> // string /* _____________ Test Cases _____________ */ 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>
🖌 풀이
type MyAwaited<T> = T extends PromiseLike<infer R> ? R extends PromiseLike<any> ? MyAwaited<R> : R : never
우선, T의 Promise 여부를 확인해서 아닐 경우 never를 반환한다. Promise라면 infer 키워드를 통해 내부의 타입을 추론한다.
type MyAwaited<T> = T extends Promise<infer InnerType> ? InnerType : never;
위 타입도 케이스들은 해결이 가능하나, 내부 타입이 Promise인 경우를 위해 안쪽 조건부처럼 MyAwaited를 재귀적으로 실행해준다.
* infer
infer는 조건부 타입의 extends 절에서 특정 지점의 타입을 추론하기 위해 사용되는 키워드다. (어렵고.. 나도 이해하는데 꽤 걸렸다..)
T extends infer U ? X : Y
기본적인 문법은 위와 같고, 기존 조건부 타입과 큰 차이가 안느껴지지만 제네릭이나 함수 등의 예제로 가면 유용해진다.
type Unpacked<T> = T extends (infer U)[] ? U : T extends (...args: any[]) => infer U ? U : T extends Promise<infer U> ? U : T; type T0 = Unpacked<string>; // string type T1 = Unpacked<string[]>; // string type T2 = Unpacked<() => string>; // string type T3 = Unpacked<Promise<string>>; // string type T4 = Unpacked<Promise<string>[]>; // Promise<string> type T5 = Unpacked<Unpacked<Promise<string>[]>>; // string
Unpacked 유틸리티 타입은 T를 제네릭 인자로 받는다.
이 T가 각각 배열인 경우엔 엘리먼트의, 함수인 경우 반환값의, 비동기(Promise)인 경우 반환값을 추론하는 타입이다.
타입을 직접 명시하는 게 아닌, 런타임 과정에서 타입을 추론하기 위해 이 키워드가 사용된다. (참고링크)
* Promise vs PromiseLike
Promise 타입을 다루기 위해 Promise 제네릭도 있지만, PromiseLike도 있다.
이 두 타입은 Promise<T> 는 finally만, PromiseLike<T> 는 then만 있다는 차이가 있다.
// lib.2018.promise.d.ts interface Promise<T> { /** * Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected). The * resolved value cannot be modified from the callback. * @param onfinally The callback to execute when the Promise is settled (fulfilled or rejected). * @returns A Promise for the completion of the callback. */ finally(onfinally?: (() => void) | undefined | null): Promise<T> } // lib.es5.d.ts interface Promise<T> { /** * Attaches callbacks for the resolution and/or rejection of the Promise. * @param onfulfilled The callback to execute when the Promise is resolved. * @param onrejected The callback to execute when the Promise is rejected. * @returns A Promise for the completion of which ever callback is executed. */ then<TResult1 = T, TResult2 = never>( onfulfilled?: | ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: | ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null, ): Promise<TResult1 | TResult2> /** * Attaches a callback for only the rejection of the Promise. * @param onrejected The callback to execute when the Promise is rejected. * @returns A Promise for the completion of the callback. */ catch<TResult = never>( onrejected?: | ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null, ): Promise<T | TResult> }
interface PromiseLike<T> { /** * Attaches callbacks for the resolution and/or rejection of the Promise. * @param onfulfilled The callback to execute when the Promise is resolved. * @param onrejected The callback to execute when the Promise is rejected. * @returns A Promise for the completion of which ever callback is executed. */ then<TResult1 = T, TResult2 = never>( onfulfilled?: | ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: | ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null, ): PromiseLike<TResult1 | TResult2> }
그렇기에 이 둘의 가장 큰 차이는, 1) PromiseLike는 catch를 관여할 수 없다, 2) Promise는 비동기 반환 결과를 받을 수 없다 이다.
Promise가 정식 스펙이 되기 전 이를 구현한 라이브러리들이 있었고,
이들 중에 catch를 다루지 않는 타입을 지원하기 위해 PromiseLike가 별도로 출시된 것이다.
반응형'Front-End(Web) > Typescript' 카테고리의 다른 글
[Type-challenges] 난이도 Medium - (1) (0) 2023.02.06 [Type-challenges] 난이도 Easy - (2) (0) 2023.01.25 <작성중>[Typescript] Utility Type(유틸리티 타입) (0) 2022.07.25 [Typescript] 타입 단언 / 타입 가드 / 타입 호환 (0) 2022.05.13 [Typescript] Type Alias vs Interface (0) 2021.08.25