[Type-challenges] 난이도 Medium - (7)
Github 챌린지 문제들을 풀고 관련된 내용을 정리하면서, 부족했던 타입스크립트 기본지식을 다지고자 한다. (주 1-2회)
https://github.com/type-challenges/type-challenges
📘 목차 - Medium
- Reverse
- Flip Arguments
- FlattenDepth
- BEM style string
- InorderTraversal
- Flip
📘 문제 및 풀이
1. Reverse
Implement the type version of Array.reverse
For example:
type a = Reverse<['a', 'b']> // ['b', 'a']
type b = Reverse<['a', 'b', 'c']> // ['c', 'b', 'a']
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<Reverse<[]>, []>>,
Expect<Equal<Reverse<['a', 'b']>, ['b', 'a']>>,
Expect<Equal<Reverse<['a', 'b', 'c']>, ['c', 'b', 'a']>>,
]
type errors = [
// @ts-expect-error
Reverse<'string'>,
// @ts-expect-error
Reverse<{ key: 'value' }>,
]
🖌 풀이
type Reverse<T extends any[]> = T extends [infer F, ...infer R] ? [...Reverse<R>, F] : []
배열 전개를 재귀적으로 사용하면 간단히 reverse가 가능하다.
2. Flip Arguments
Implement the type version of lodash's _.flip.
Type FlipArguments<T> requires function type T and returns a new function type which has the same return type of T but reversed parameters.
For example:
type Flipped = FlipArguments<(arg0: string, arg1: number, arg2: boolean) => void>
// (arg0: boolean, arg1: number, arg2: string) => void
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<FlipArguments<() => boolean>, () => boolean>>,
Expect<Equal<FlipArguments<(foo: string) => number>, (foo: string) => number>>,
Expect<Equal<FlipArguments<(arg0: string, arg1: number, arg2: boolean) => void>, (arg0: boolean, arg1: number, arg2: string) => void>>,
]
type errors = [
// @ts-expect-error
FlipArguments<'string'>,
// @ts-expect-error
FlipArguments<{ key: 'value' }>,
// @ts-expect-error
FlipArguments<['apple', 'banana', 100, { a: 1 }]>,
// @ts-expect-error
FlipArguments<null | undefined>,
]
🖌 풀이
type Reverse<T extends any[]> = T extends [infer F, ...infer R] ? [...Reverse<R>, F] : []
type FlipArguments<T extends (...args: any) => any> = (...args: Reverse<Parameters<T>>) => ReturnType<T>;
위 1번의 Reverse를 활용하면 쉽게 풀 수 있다. 함수 매개변수 타입(Parameters)에 Reverse만 적용해주면 된다.
3. FlattenDepth
Recursively flatten array up to depth times.
For example:
type a = FlattenDepth<[1, 2, [3, 4], [[[5]]]], 2> // [1, 2, 3, 4, [5]]. flattern 2 times
type b = FlattenDepth<[1, 2, [3, 4], [[[5]]]]> // [1, 2, 3, 4, [[5]]]. Depth defaults to be 1
If the depth is provided, it's guaranteed to be positive integer.
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<FlattenDepth<[]>, []>>,
Expect<Equal<FlattenDepth<[1, 2, 3, 4]>, [1, 2, 3, 4]>>,
Expect<Equal<FlattenDepth<[1, [2]]>, [1, 2]>>,
Expect<Equal<FlattenDepth<[1, 2, [3, 4], [[[5]]]], 2>, [1, 2, 3, 4, [5]]>>,
Expect<Equal<FlattenDepth<[1, 2, [3, 4], [[[5]]]]>, [1, 2, 3, 4, [[5]]]>>,
Expect<Equal<FlattenDepth<[1, [2, [3, [4, [5]]]]], 3>, [1, 2, 3, 4, [5]]>>,
Expect<Equal<FlattenDepth<[1, [2, [3, [4, [5]]]]], 19260817>, [1, 2, 3, 4, 5]>>,
]
🖌 풀이
type FlattenOnce<T extends any[]> = T extends [infer F, ...infer R]? [...F extends [...infer K]? K : [F], ...FlattenOnce<R> ] : T
type FlattenDepth<T extends any[], C extends number = 1, P extends any[] = []> = P['length'] extends C ? T : T extends FlattenOnce<T>? T: FlattenDepth<FlattenOnce<T>, C, [...P, any] >
먼저, FlattenOnce는 flatten을 한 번 실행하는 유틸리티이다.
그리고, FlattenDepth를 설정한다. C는 flatten 횟수, P는 flatten 한 횟수를 any의 개수로 저장하는 배열이다.
P의 길이(length)와 C를 비교하거나, T가 flatten한 T와 같으면 더 이상 진행이 의미없으므로 이 때 T를 반환한다.
아니라면, FlattenDepth를 재귀적으로 실행해주며, T는 한 번 flatten 해주고 배열엔 임의 요소를 하나 추가해준다. (참고링크)
4. BEM style string
The Block, Element, Modifier methodology (BEM) is a popular naming convention for classes in CSS.
For example, the block component would be represented as btn, element that depends upon the block would be represented as btn__price, modifier that changes the style of the block would be represented as btn--big or btn__price--warning.
Implement BEM<B, E, M> which generate string union from these three parameters. Where B is a string
literal, E and M are string arrays (can be empty).
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<BEM<'btn', ['price'], []>, 'btn__price'>>,
Expect<Equal<BEM<'btn', ['price'], ['warning', 'success']>, 'btn__price--warning' | 'btn__price--success' >>,
Expect<Equal<BEM<'btn', [], ['small', 'medium', 'large']>, 'btn--small' | 'btn--medium' | 'btn--large' >>,
]
🖌 풀이
type BEM<B extends string, E extends string[], M extends string[]> = `${B}${E extends [] ? '' : `__${E[number]}`}${M extends [] ? '' : `--${M[number]}`}`
배열[number] 로 각 엘리먼트에 유니온처럼 접근할 수 있는 방법을 응용하였다.
E, M 각 배열이 빈 경우는 빈 문자열('')을, 문자열 값들이 존재하는 경우는 [number]로 각 엘리먼트에 접근하여 문법처럼 적용해준다.
5. InorderTraversal
Implement the type version of binary tree inorder traversal.
For example:
const tree1 = {
val: 1,
left: null,
right: {
val: 2,
left: {
val: 3,
left: null,
right: null,
},
right: null,
},
} as const
type A = InorderTraversal<typeof tree1> // [1, 3, 2]
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
const tree1 = {
val: 1,
left: null,
right: {
val: 2,
left: {
val: 3,
left: null,
right: null,
},
right: null,
},
} as const
const tree2 = {
val: 1,
left: null,
right: null,
} as const
const tree3 = {
val: 1,
left: {
val: 2,
left: null,
right: null,
},
right: null,
} as const
const tree4 = {
val: 1,
left: null,
right: {
val: 2,
left: null,
right: null,
},
} as const
type cases = [
Expect<Equal<InorderTraversal<null>, []>>,
Expect<Equal<InorderTraversal<typeof tree1>, [1, 3, 2]>>,
Expect<Equal<InorderTraversal<typeof tree2>, [1]>>,
Expect<Equal<InorderTraversal<typeof tree3>, [2, 1]>>,
Expect<Equal<InorderTraversal<typeof tree4>, [1, 2]>>,
]
🖌 풀이
interface TreeNode {
val: number
left: TreeNode | null
right: TreeNode | null
}
type InorderTraversal<T extends TreeNode | null> =
[T] extends [TreeNode]
? (
[
...InorderTraversal<T['left']>,
T['val'],
...InorderTraversal<T['right']>
]
)
: []
InorderTraversal은 트리 노드 값들을 배열로 펼치는 유틸리티로, 현재 인자 기준으로 left값은 바로 왼쪽, right값은 바로 오른쪽에 위치해야한다.
T가 TreeNode에 해당하면 좌, 우에는 InorderTraversal을 재귀적으로 적용해주고 가운데에는 현재값을 넣어준다.
단, 이 때 제네릭 인자 T가 유니온이므로, Distribute Conditional Types(분배 조건부 타입)을 회피하기 위해 []로 감싸준다. (참고링크)
6. Flip
Implement the type of just-flip-object. Examples:
Flip<{ a: "x", b: "y", c: "z" }>; // {x: 'a', y: 'b', z: 'c'}
Flip<{ a: 1, b: 2, c: 3 }>; // {1: 'a', 2: 'b', 3: 'c'}
Flip<{ a: false, b: true }>; // {false: 'a', true: 'b'}
No need to support nested objects and values which cannot be object keys such as arrays
/* _____________ Test Cases _____________ */
import type { Equal, Expect, NotEqual } from '@type-challenges/utils'
type cases = [
Expect<Equal<{ a: 'pi' }, Flip<{ pi: 'a' }>>>,
Expect<NotEqual<{ b: 'pi' }, Flip<{ pi: 'a' }>>>,
Expect<Equal<{ 3.14: 'pi'; true: 'bool' }, Flip<{ pi: 3.14; bool: true }>>>,
Expect<Equal<{ val2: 'prop2'; val: 'prop' }, Flip<{ prop: 'val'; prop2: 'val2' }>>>,
]
🖌 풀이
type Flip<T extends Record<PropertyKey, any>> = {
[P in keyof T as T[P] extends PropertyKey ? T[P] : `${T[P]}`]: P
}
이전에 풀었던, Mapped Type에서 키값에 value(T[P])를 사용하는 방식을 활용하면 쉽게 풀 수 있다.
단, T[P]가 키값으로 사용 가능한 타입(string, number, symbol) 인지 여부를 확인해서, 아니라면 리터럴화 해준다.