[Type-challenges] 난이도 Medium - (2)
Github 챌린지 문제들을 풀고 관련된 내용을 정리하면서, 부족했던 타입스크립트 기본지식을 다지고자 한다. (주 1-2회)
https://github.com/type-challenges/type-challenges
📘 목차 - Medium
- Promise.all
- Type Lookup
- Trim Left
- Trim
- Capitalize
- Replace
- ReplaceAll
- Append Argument
📘 문제 및 풀이
1. Promise.all
Type the function PromiseAll that accepts an array of PromiseLike objects, the returning value should be Promise<T> where T is the resolved result array.
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise<string>((resolve, reject) => {
setTimeout(resolve, 100, 'foo');
});
// expected to be `Promise<[number, 42, string]>`
const p = PromiseAll([promise1, promise2, promise3] as const)
const fn = (v: boolean) => v ? 1 : 2
const fn1 = (v: boolean, w: any) => v ? 1 : 2
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
const promiseAllTest1 = PromiseAll([1, 2, 3] as const)
const promiseAllTest2 = PromiseAll([1, 2, Promise.resolve(3)] as const)
const promiseAllTest3 = PromiseAll([1, 2, Promise.resolve(3)])
const promiseAllTest4 = PromiseAll<Array<number | Promise<number>>>([1, 2, 3])
type cases = [
Expect<Equal<typeof promiseAllTest1, Promise<[1, 2, 3]>>>,
Expect<Equal<typeof promiseAllTest2, Promise<[1, 2, number]>>>,
Expect<Equal<typeof promiseAllTest3, Promise<[number, number, number]>>>,
Expect<Equal<typeof promiseAllTest4, Promise<number[]>>>,
]
🖌 풀이
type MyAwaited<T> = T extends Promise<infer R> ? MyAwaited<R> : T
declare function PromiseAll<T extends any[]>(
values: readonly [...T]
): Promise<{
[K in keyof T]: MyAwaited<T[K]>
}>
우선 MyAwaited 라는 커스텀 유틸리티를 만든다. 이는, Promise 값을 받으면 재귀적으로 반환타입을 반환한다.
PromiseAll 유틸리티는, 먼저 배열을 제네릭 인자로 받고, 매개변수(values)는 그 배열을, 반환값은 매개변수들의 반환값을 Promise로 감싼 배열을 반환해야 한다.
특히, 매개변수(values)를 사용할 때, 케이스 1,2와 같이 readonly인 경우가 있으므로 이를 추가해줘야 한다.
2. Type Lookup
Sometimes, you may want to lookup for a type in a union to by their attributes.
In this challenge, we would like to get the corresponding type by searching for the common type field in the union Cat | Dog. In other words, we will expect to get Dog for LookUp<Dog | Cat, 'dog'> and Cat for LookUp<Dog | Cat, 'cat'> in the following example.
interface Cat {
type: 'cat'
breeds: 'Abyssinian' | 'Shorthair' | 'Curl' | 'Bengal'
}
/* _____________ Your Code Here _____________ */
type LookUp<U, T> = U extends { type: T } ? U : never
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
interface Cat {
type: 'cat'
breeds: 'Abyssinian' | 'Shorthair' | 'Curl' | 'Bengal'
}
interface Dog {
type: 'dog'
breeds: 'Hound' | 'Brittany' | 'Bulldog' | 'Boxer'
color: 'brown' | 'white' | 'black'
}
type Animal = Cat | Dog
type cases = [
Expect<Equal<LookUp<Animal, 'dog'>, Dog>>,
Expect<Equal<LookUp<Animal, 'cat'>, Cat>>,
]
🖌 풀이
type LookUp<U, T> = U extends { type: T } ? U : never
유니온 타입에 extends로 체이닝할 수 있다. { type: T } 에 해당되면 U를, 아니면 never를 반환해서 필터링한다.
3. Trim Left
Implement TrimLeft<T> which takes an exact string type and returns a new string with the whitespace beginning removed.
For example
type trimed = TrimLeft<' Hello World '> // expected to be 'Hello World '
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<TrimLeft<'str'>, 'str'>>,
Expect<Equal<TrimLeft<' str'>, 'str'>>,
Expect<Equal<TrimLeft<' str'>, 'str'>>,
Expect<Equal<TrimLeft<' str '>, 'str '>>,
Expect<Equal<TrimLeft<' \n\t foo bar '>, 'foo bar '>>,
Expect<Equal<TrimLeft<''>, ''>>,
Expect<Equal<TrimLeft<' \n\t'>, ''>>,
]
🖌 풀이
type W = ' ' | '\n' | '\t'
type TrimLeft<S extends string> = S extends `${W}${infer Rest}`
? TrimLeft<Rest>
: S
`${W}${infer Rest}` 처럼 문자열 타입을 template literal 형태로 접근 가능하다. (문서링크)
왼쪽만 자르면 되므로, 이 부분이 W(공백)에 해당할 경우 TrimLeft를 재귀적으로 아니면 S 그대로를 반환한다.
4. Trim
Implement Trim<T> which takes an exact string type and returns a new string with the whitespace from both ends removed.
For example
type trimmed = Trim<' Hello World '> // expected to be 'Hello World'
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<Trim<'str'>, 'str'>>,
Expect<Equal<Trim<' str'>, 'str'>>,
Expect<Equal<Trim<' str'>, 'str'>>,
Expect<Equal<Trim<'str '>, 'str'>>,
Expect<Equal<Trim<' str '>, 'str'>>,
Expect<Equal<Trim<' \n\t foo bar \t'>, 'foo bar'>>,
Expect<Equal<Trim<''>, ''>>,
Expect<Equal<Trim<' \n\t '>, ''>>,
]
🖌 풀이
type W = ' ' | '\n' | '\t'
type Trim<S extends string> =
S extends `${W}${infer T}` | `${infer T}${W}` ? Trim<T> : S
위의 TrimLeft를 응용하면 된다. 좌우측 중 한 곳이 W(공백)이 있다면 Trim을 재귀적으로, 아니면 S 문자열 타입 그대로 반환한다.
5. Capitalize
Implement Capitalize<T> which converts the first letter of a string to uppercase and leave the rest as-is.
For example
type capitalized = Capitalize<'hello world'> // expected to be 'Hello world'
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<MyCapitalize<'foobar'>, 'Foobar'>>,
Expect<Equal<MyCapitalize<'FOOBAR'>, 'FOOBAR'>>,
Expect<Equal<MyCapitalize<'foo bar'>, 'Foo bar'>>,
Expect<Equal<MyCapitalize<''>, ''>>,
Expect<Equal<MyCapitalize<'a'>, 'A'>>,
Expect<Equal<MyCapitalize<'b'>, 'B'>>,
Expect<Equal<MyCapitalize<'c'>, 'C'>>,
Expect<Equal<MyCapitalize<'d'>, 'D'>>,
Expect<Equal<MyCapitalize<'e'>, 'E'>>,
Expect<Equal<MyCapitalize<'f'>, 'F'>>,
Expect<Equal<MyCapitalize<'g'>, 'G'>>,
Expect<Equal<MyCapitalize<'h'>, 'H'>>,
Expect<Equal<MyCapitalize<'i'>, 'I'>>,
Expect<Equal<MyCapitalize<'j'>, 'J'>>,
Expect<Equal<MyCapitalize<'k'>, 'K'>>,
Expect<Equal<MyCapitalize<'l'>, 'L'>>,
Expect<Equal<MyCapitalize<'m'>, 'M'>>,
Expect<Equal<MyCapitalize<'n'>, 'N'>>,
Expect<Equal<MyCapitalize<'o'>, 'O'>>,
Expect<Equal<MyCapitalize<'p'>, 'P'>>,
Expect<Equal<MyCapitalize<'q'>, 'Q'>>,
Expect<Equal<MyCapitalize<'r'>, 'R'>>,
Expect<Equal<MyCapitalize<'s'>, 'S'>>,
Expect<Equal<MyCapitalize<'t'>, 'T'>>,
Expect<Equal<MyCapitalize<'u'>, 'U'>>,
Expect<Equal<MyCapitalize<'v'>, 'V'>>,
Expect<Equal<MyCapitalize<'w'>, 'W'>>,
Expect<Equal<MyCapitalize<'x'>, 'X'>>,
Expect<Equal<MyCapitalize<'y'>, 'Y'>>,
Expect<Equal<MyCapitalize<'z'>, 'Z'>>,
]
🖌 풀이
type MyCapitalize<S extends string> = S extends `${infer F}${infer R}` ? `${Uppercase<F>}${R}` : ''
Template Literal을 좀 더 응용해서 풀어보았다. 첫 번째 인자를 F라는 타입으로 추론하고, 여기에 Uppercase 유틸리티를 적용한다.
6. Replace
Implement Replace<S, From, To> which replace the string From with To once in the given string S
For example
type replaced = Replace<'types are fun!', 'fun', 'awesome'> // expected to be 'types are awesome!'
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<Replace<'foobar', 'bar', 'foo'>, 'foofoo'>>,
Expect<Equal<Replace<'foobarbar', 'bar', 'foo'>, 'foofoobar'>>,
Expect<Equal<Replace<'foobarbar', '', 'foo'>, 'foobarbar'>>,
Expect<Equal<Replace<'foobarbar', 'bar', ''>, 'foobar'>>,
Expect<Equal<Replace<'foobarbar', 'bra', 'foo'>, 'foobarbar'>>,
Expect<Equal<Replace<'', '', ''>, ''>>,
]
🖌 풀이
type Replace<
S extends string,
From extends string,
To extends string,
> = S extends `${infer Left}${From extends '' ? never : From}${infer Right}`
? `${Left}${To}${Right}`
: S
이와 같이, 가운데가 From인 경우엔 To로 변경된 타입을 반환하면 된다.
케이스3처럼 From이 빈 배열인 경우가 있으므로, 예외처리를 한다.
7. ReplaceAll
Implement ReplaceAll<S, From, To> which replace the all the substring From with To in the given string S
For example
type replaced = ReplaceAll<'t y p e s', ' ', ''> // expected to be 'types'
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<ReplaceAll<'foobar', 'bar', 'foo'>, 'foofoo'>>,
Expect<Equal<ReplaceAll<'foobar', 'bag', 'foo'>, 'foobar'>>,
Expect<Equal<ReplaceAll<'foobarbar', 'bar', 'foo'>, 'foofoofoo'>>,
Expect<Equal<ReplaceAll<'t y p e s', ' ', ''>, 'types'>>,
Expect<Equal<ReplaceAll<'foobarbar', '', 'foo'>, 'foobarbar'>>,
Expect<Equal<ReplaceAll<'barfoo', 'bar', 'foo'>, 'foofoo'>>,
Expect<Equal<ReplaceAll<'foobarfoobar', 'ob', 'b'>, 'fobarfobar'>>,
Expect<Equal<ReplaceAll<'foboorfoboar', 'bo', 'b'>, 'foborfobar'>>,
Expect<Equal<ReplaceAll<'', '', ''>, ''>>,
]
🖌 풀이
type ReplaceAll<
S extends string,
From extends string,
To extends string,
> = S extends `${infer Left}${From extends '' ? never : From}${infer Right}`
? `${Left}${To}${ReplaceAll<Right, From, To>}`
: S
위의 Replace를 응용하면 된다.
마지막 케이스들처럼, 한 번 replace가 되면 다시 적용되지 않아야 하므로 Right에 대해서만 재귀적으로 실행시켜준다.
8. Append Argument
For given function type Fn, and any type A (any in this context means we don't restrict the type, and I don't have in mind any type 😉) create a generic type which will take Fn as the first argument, A as the second, and will produce function type G which will be the same as Fn but with appended argument A as a last one.
For example,
type Fn = (a: number, b: string) => number
type Result = AppendArgument<Fn, boolean>
// expected be (a: number, b: string, x: boolean) => number
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type Case1 = AppendArgument<(a: number, b: string) => number, boolean>
type Result1 = (a: number, b: string, x: boolean) => number
type Case2 = AppendArgument<() => void, undefined>
type Result2 = (x: undefined) => void
type cases = [
Expect<Equal<Case1, Result1>>,
Expect<Equal<Case2, Result2>>,
// @ts-expect-error
AppendArgument<unknown, undefined>,
]
🖌 풀이
type AppendArgument<Fn extends Function, A> = Fn extends (...args: infer Args) => infer Returns
? (...args: [...Args, A]) => Returns
: never
매개변수(Args), 반환값(Returns) 의 타입을 각각 단언해준다.
AppendArgument 유틸리티가 반환하는 함수의 매개변수 타입에만, Args와 A로 연장시켜준다.