[Type-challenges] 난이도 Hard - (2)
Github 챌린지 문제들을 풀고 관련된 내용을 정리하면서, 부족했던 타입스크립트 기본지식을 다지고자 한다. (주 1-2회)
https://github.com/type-challenges/type-challenges
📕 목차 - Hard
- Capitalize Words
- CamelCase
- C-printf Parser
- Vue Basic Props
- IsAny
- Typed Get
- String to Number
📕 문제 및 풀이
1. Capitalize Words
Implement CapitalizeWords<T> which converts the first letter of each word of a string to uppercase and leaves the rest as-is.
For example
type capitalized = CapitalizeWords<'hello world, my friends'> // expected to be 'Hello World, My Friends'
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<CapitalizeWords<'foobar'>, 'Foobar'>>,
Expect<Equal<CapitalizeWords<'FOOBAR'>, 'FOOBAR'>>,
Expect<Equal<CapitalizeWords<'foo bar'>, 'Foo Bar'>>,
Expect<Equal<CapitalizeWords<'foo bar hello world'>, 'Foo Bar Hello World'>>,
Expect<Equal<CapitalizeWords<'foo bar.hello,world'>, 'Foo Bar.Hello,World'>>,
Expect<Equal<CapitalizeWords<'aa!bb@cc#dd$ee%ff^gg&hh*ii(jj)kk_ll+mm{nn}oo|pp🤣qq'>, 'Aa!Bb@Cc#Dd$Ee%Ff^Gg&Hh*Ii(Jj)Kk_Ll+Mm{Nn}Oo|Pp🤣Qq'>>,
Expect<Equal<CapitalizeWords<''>, ''>>,
]
🖌 풀이
type CapitalizeWords<S extends string, W extends string = ''> = S extends `${infer Left}${infer Rest}`
? Uppercase<Left> extends Lowercase<Left>
? `${Capitalize<W>}${Left}${CapitalizeWords<Rest>}`
: CapitalizeWords<Rest, `${W}${Left}`>
: Capitalize<W>
CapitalizeWords 유틸리티는 S(문자열), W(누적되는 한 음절) 2가지를 제너릭 인자로 가진다.
S가 문자열이 아니라면 마지막이므로 W(마지막 음절)를 Capitalize 해서 반환한다.
문자열이라면 W에 Left를 쌓고, 남은 Rest 문자열에 대해 유틸리티를 재귀한다.
단, 풀이 2번째 줄은 특수문자를 구별하는 부분이다.(+띄어쓰기, 이모지 등) => 케이스 4~6
이 경우, 현재까지 쌓은 W를 Capitalize 하고, 가운데에는 특수문자(Left)를 바로 넣은 다음, 나머지 문자열에 대해 다시 유틸리티를 재귀해서 이어간다.
2. CamelCase
Implement CamelCase<T> which converts snake_case string to camelCase.
For example
type camelCase1 = CamelCase<'hello_world_with_types'> // expected to be 'helloWorldWithTypes'
type camelCase2 = CamelCase<'HELLO_WORLD_WITH_TYPES'> // expected to be same as previous one
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<CamelCase<'foobar'>, 'foobar'>>,
Expect<Equal<CamelCase<'FOOBAR'>, 'foobar'>>,
Expect<Equal<CamelCase<'foo_bar'>, 'fooBar'>>,
Expect<Equal<CamelCase<'foo__bar'>, 'foo_Bar'>>,
Expect<Equal<CamelCase<'foo_$bar'>, 'foo_$bar'>>,
Expect<Equal<CamelCase<'foo_bar_'>, 'fooBar_'>>,
Expect<Equal<CamelCase<'foo_bar__'>, 'fooBar__'>>,
Expect<Equal<CamelCase<'foo_bar_$'>, 'fooBar_$'>>,
Expect<Equal<CamelCase<'foo_bar_hello_world'>, 'fooBarHelloWorld'>>,
Expect<Equal<CamelCase<'HELLO_WORLD_WITH_TYPES'>, 'helloWorldWithTypes'>>,
Expect<Equal<CamelCase<'-'>, '-'>>,
Expect<Equal<CamelCase<''>, ''>>,
Expect<Equal<CamelCase<'😎'>, '😎'>>,
]
🖌 풀이
type CamelCase<
S extends string,
Prev extends string = ''
> = S extends `${infer F}${infer R}`
? Uppercase<F> extends Lowercase<F>
? `${Prev}${CamelCase<R, F>}`
: Prev extends '_'
? `${CamelCase<R, Uppercase<F>>}`
: `${Prev}${CamelCase<R, Lowercase<F>>}`
: Prev;
위 1번과 마찬가지로, Prev는 대문자 기준으로 분리되는 한 단어를 의미한다.
S가 문자열이 아닌 경우 마지막이므로, Prev에 누적된 단어 자체를 반환한다.
S가 문자열인 경우 먼저 특수문자를 판별해준다.(line 2)
이 경우, 이전 Prev에 이어서 특수문자(F)를 새로운 Prev에 저장하고 남은 R에 대해 유틸리티를 재귀한다. (언더바 구분 때문)
특수문자가 아닌 경우, Prev에 저장된 문자가 언더바('_') 인지 확인해준다. 여기 걸리면, 직전 문자(Prev)가 언더바고 현재 문자(F)는 알파벳인 경우인 것이다.
이 경우, Prev의 언더바는 제외하고 현재 알파벳을 Uppercase 하여 남은 R에 대해 유틸리티를 재귀한다.
아니라면 일반 문자열들의 구간이므로, 이전 Prev에 이어서 현재 알파벳을 Lowercase 하고 남은 R에 대해 유틸리티를 재귀한다.
3. C-printf Parser
There is a function in C language: printf
. This function allows us to print something with formatting. Like this:
printf("The result is %d.", 42);
This challenge requires you to parse the input string and extract the format placeholders like %d and %f.
For example, if the input string is "The result is %d.", the parsed result is a tuple ['dec'].
Here is the mapping:
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<ParsePrintFormat<''>, []>>,
Expect<Equal<ParsePrintFormat<'Any string.'>, []>>,
Expect<Equal<ParsePrintFormat<'The result is %d.'>, ['dec']>>,
Expect<Equal<ParsePrintFormat<'The result is %%d.'>, []>>,
Expect<Equal<ParsePrintFormat<'The result is %%%d.'>, ['dec']>>,
Expect<Equal<ParsePrintFormat<'The result is %f.'>, ['float']>>,
Expect<Equal<ParsePrintFormat<'The result is %h.'>, ['hex']>>,
Expect<Equal<ParsePrintFormat<'The result is %q.'>, []>>,
Expect<Equal<ParsePrintFormat<'Hello %s: score is %d.'>, ['string', 'dec']>>,
Expect<Equal<ParsePrintFormat<'The result is %'>, []>>,
]
🖌 풀이
type ControlsMap = {
c: 'char'
s: 'string'
d: 'dec'
o: 'oct'
h: 'hex'
f: 'float'
p: 'pointer'
}
type ParsePrintFormat<T extends string> =
T extends `${string}%${infer C}${infer Rest}`
? C extends keyof ControlsMap
? [ControlsMap[C], ...ParsePrintFormat<Rest>]
: [...ParsePrintFormat<Rest>]
: [];
%{문자} 가 있으면, 이 문자에 해당하는 값을 ControlsMap에서 가져와서 배열로 반환하는 문제이다.
% 다음에 오는 문자 C에 대해서, 이 값이 ControlsMap key에 있다면 이에 대한 값을 배열에 담고, 없다면 바로 유틸리티를 재귀한다.
4. Vue Props
/* _____________ Test Cases _____________ */
import type { Debug, Equal, Expect, IsAny } from '@type-challenges/utils'
class ClassA {}
VueBasicProps({
props: {
propA: {},
propB: { type: String },
propC: { type: Boolean },
propD: { type: ClassA },
propE: { type: [String, Number] },
propF: RegExp,
},
data(this) {
type PropsType = Debug<typeof this>
type cases = [
Expect<IsAny<PropsType['propA']>>,
Expect<Equal<PropsType['propB'], string>>,
Expect<Equal<PropsType['propC'], boolean>>,
Expect<Equal<PropsType['propD'], ClassA>>,
Expect<Equal<PropsType['propE'], string | number>>,
Expect<Equal<PropsType['propF'], RegExp>>,
]
// @ts-expect-error
this.firstname
// @ts-expect-error
this.getRandom()
// @ts-expect-error
this.data()
return {
firstname: 'Type',
lastname: 'Challenges',
amount: 10,
}
},
computed: {
fullname() {
return `${this.firstname} ${this.lastname}`
},
},
methods: {
getRandom() {
return Math.random()
},
hi() {
alert(this.fullname.toLowerCase())
alert(this.getRandom())
},
test() {
const fullname = this.fullname
const propE = this.propE
type cases = [
Expect<Equal<typeof fullname, string>>,
Expect<Equal<typeof propE, string | number>>,
]
},
},
})
🖌 풀이
Vue의 Props 타입을 추론하는 문제이다. 다음 링크를 참고만 하자. (풀이링크)
5. IsAny
Sometimes it's useful to detect if you have a value with any type. This is especially helpful while working with third-party Typescript modules, which can export any values in the module API. It's also good to know about any when you're suppressing implicitAny checks.
So, let's write a utility type IsAny<T>, which takes input type T. If T is any, return true, otherwise, return false.
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<IsAny<any>, true>>,
Expect<Equal<IsAny<undefined>, false>>,
Expect<Equal<IsAny<unknown>, false>>,
Expect<Equal<IsAny<never>, false>>,
Expect<Equal<IsAny<string>, false>>,
]
🖌 풀이
// 1) 타입기반 풀이
type IsAny<T> = 0 extends T & 1 ? true : false
// 2) Equal 유틸리티 풀이
type IsAny<T> = Equal<T, any>;
어떻게 풀지 고민이 됬지만, 생각보다 어렵지 않게 풀 수 있었다.
1) 타입기반 풀이는, T와 임의값(1)을 intersection으로 연결한 타입에 다른 임의값(0)이 포함되는지 여부를 확인한다.
교차 타입에 조건부를 걸면, 모든 타입에 해당되야 하므로 왠만한 T 타입에 대해 false 결과값이 나오게 된다.
하지만, any와 교차 타입이 되면 any는 하위타입(subset)처럼 반영된다. 즉, T & 1 이 any가 되는 원리를 활용한 것이다.(참고링크)
2) Equal 유틸리티 풀이는, 말 그대로 유틸리티를 활용하여 any인지 직접적으로 비교한다.
* Union & Intersection 타입의 상/하위 타입 간의 관계
type Numbers = string; // 상위(superset)
type Digits = 1 | 2; // 하위(subset)
상위(superset) 타입과 하위(subset) 타입의 관계에 있는 타입 간의 Union과 Intersection 에 대한 부연 설명이다.
소위, Union은 합집합, Intersection은 교집합의 개념으로 설명된다.
그렇기에, 상/하위 타입간의 관계에서, Intersection은 하위타입(Digits), Union은 상위타입(Numbers) 가 되는 것이다! (참고링크)
6. Typed Get
The get function in lodash is a quite convenient helper for accessing nested values in JavaScript. However, when we come to TypeScript, using functions like this will make you lose the type information. With TS 4.1's upcoming Template Literal Types feature, properly typing get becomes possible. Can you implement it?
For example,
type Data = {
foo: {
bar: {
value: 'foobar',
count: 6,
},
included: true,
},
hello: 'world'
}
type A = Get<Data, 'hello'> // 'world'
type B = Get<Data, 'foo.bar.count'> // 6
type C = Get<Data, 'foo.bar'> // { value: 'foobar', count: 6 }
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<Get<Data, 'hello'>, 'world'>>,
Expect<Equal<Get<Data, 'foo.bar.count'>, 6>>,
Expect<Equal<Get<Data, 'foo.bar'>, { value: 'foobar'; count: 6 }>>,
Expect<Equal<Get<Data, 'foo.baz'>, false>>,
Expect<Equal<Get<Data, 'no.existed'>, never>>,
]
🖌 풀이
type Get<T, K extends string> = K extends keyof T
? T[K]
: K extends `${infer KEY}.${infer R}`
? KEY extends keyof T
? Get<T[KEY], R>
: never
: never;
유틸리티는 T(대상 객체), K(키네임을 저장하는 문자열) 2가지를 제너릭 인자로 받는다.
저장된 K가 T의 키값인 경우 T[P]를, 아니라면 never를 반환한다.(마지막 케이스)
다음으로, K문자열의 점('.') 을 찾아 앞부분을 KEY 문자열로 식별하고, 이 값이 T의 키값인 경우 이 T[KEY] 와 나머지 문자열(R) 에 대해 재귀적으로 유틸리티를 적용한다.
7. String to Number
Convert a string literal to a number, which behaves like Number.parseInt.
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<OptionalKeys<{ a: number; b?: string }>, 'b'>>,
Expect<Equal<OptionalKeys<{ a: undefined; b?: undefined }>, 'b'>>,
Expect<Equal<OptionalKeys<{ a: undefined; b?: undefined; c?: string; d?: null }>, 'b' | 'c' | 'd'>>,
Expect<Equal<OptionalKeys<{}>, never>>,
]
🖌 풀이
type ToNumber<S extends string> = S extends `${infer N extends number}` ? N : never
내부 infer 타입인 N이 숫자인지 조건문으로 확인하면 된다.