[Type-challenges] 난이도 Medium - (5)
Github 챌린지 문제들을 풀고 관련된 내용을 정리하면서, 부족했던 타입스크립트 기본지식을 다지고자 한다. (주 1-2회)
https://github.com/type-challenges/type-challenges
📘 목차 - Medium
- Percentage Parser
- Drop Char
- MinusOne
- PickByType
- StartsWith
- EndsWith
- PartialByKeys
📘 문제 및 풀이
1. Percentage Parser
Implement PercentageParser. According to the /^(\+|\-)?(\d*)?(\%)?$/ regularity to match T and get three matches.
The structure should be: [plus or minus, number, unit] If it is not captured, the default is an empty string.
For example:
type PString1 = ''
type PString2 = '+85%'
type PString3 = '-85%'
type PString4 = '85%'
type PString5 = '85'
type R1 = PercentageParser<PString1> // expected ['', '', '']
type R2 = PercentageParser<PString2> // expected ["+", "85", "%"]
type R3 = PercentageParser<PString3> // expected ["-", "85", "%"]
type R4 = PercentageParser<PString4> // expected ["", "85", "%"]
type R5 = PercentageParser<PString5> // expected ["", "85", ""]
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type Case0 = ['', '', '']
type Case1 = ['+', '', '']
type Case2 = ['+', '1', '']
type Case3 = ['+', '100', '']
type Case4 = ['+', '100', '%']
type Case5 = ['', '100', '%']
type Case6 = ['-', '100', '%']
type Case7 = ['-', '100', '']
type Case8 = ['-', '1', '']
type Case9 = ['', '', '%']
type Case10 = ['', '1', '']
type Case11 = ['', '100', '']
type cases = [
Expect<Equal<PercentageParser<''>, Case0>>,
Expect<Equal<PercentageParser<'+'>, Case1>>,
Expect<Equal<PercentageParser<'+1'>, Case2>>,
Expect<Equal<PercentageParser<'+100'>, Case3>>,
Expect<Equal<PercentageParser<'+100%'>, Case4>>,
Expect<Equal<PercentageParser<'100%'>, Case5>>,
Expect<Equal<PercentageParser<'-100%'>, Case6>>,
Expect<Equal<PercentageParser<'-100'>, Case7>>,
Expect<Equal<PercentageParser<'-1'>, Case8>>,
Expect<Equal<PercentageParser<'%'>, Case9>>,
Expect<Equal<PercentageParser<'1'>, Case10>>,
Expect<Equal<PercentageParser<'100'>, Case11>>,
]
🖌 풀이
// your answers
type PercentageParser<T> = T extends `${infer F}${infer R}`
? F extends "+" | "-"
? [F, ...(R extends `${infer X}%` ? [X, "%"] : [R, ""])]
: ["", ...(T extends `${infer Y}%` ? [Y, "%"] : [T, ""])]
: ["", "", ""];
위 풀이가 생각보다 심플한 해결법을 주었다.
먼저, 첫 글자인 F가 +/-에 해당하면 이를, 아니면 빈 문자열을 시작으로 배열을 반환한다.
이후, 전자면 나머지 R을, 후자면 전체 T를 단언타입과 %에 해당하는지 여부에 따라 분기해주면 된다.
모든 경우에 해당하지 않으면, 빈 문자열들로 이루어진 배열을 반환한다.
2. Drop Char
Drop a specified char from a string.
For example:
type Butterfly = DropChar<' b u t t e r f l y ! ', ' '> // 'butterfly!'
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
// @ts-expect-error
Expect<Equal<DropChar<'butter fly!', ''>, 'butterfly!'>>,
Expect<Equal<DropChar<'butter fly!', ' '>, 'butterfly!'>>,
Expect<Equal<DropChar<'butter fly!', '!'>, 'butter fly'>>,
Expect<Equal<DropChar<' butter fly! ', ' '>, 'butterfly!'>>,
Expect<Equal<DropChar<' b u t t e r f l y ! ', ' '>, 'butterfly!'>>,
Expect<Equal<DropChar<' b u t t e r f l y ! ', 'b'>, ' u t t e r f l y ! '>>,
Expect<Equal<DropChar<' b u t t e r f l y ! ', 't'>, ' b u e r f l y ! '>>,
]
🖌 풀이
type DropChar<S, C> = S extends `${infer F}${infer R}`
? F extends C
? DropChar<R, C>
: `${F}${DropChar<R, C>}`
: S
이전에 계속 풀었던 String 타입을 재귀적으로 푸는 문제이다. 다만, 앞에 F를 붙이면서 안쪽에서 DropChar로 재귀하고 있는 점이 다르다.
3. MinusOne
Given a number (always positive) as a type. Your type should return the number decreased by one.
For example:
type Zero = MinusOne<1> // 0
type FiftyFour = MinusOne<55> // 54
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<MinusOne<1>, 0>>,
Expect<Equal<MinusOne<55>, 54>>,
Expect<Equal<MinusOne<3>, 2>>,
Expect<Equal<MinusOne<100>, 99>>,
Expect<Equal<MinusOne<1101>, 1100>>,
Expect<Equal<MinusOne<0>, -1>>,
Expect<Equal<MinusOne<9_007_199_254_740_992>, 9_007_199_254_740_991>>,
]
🖌 풀이
풀이는 이 링크를 참고하자. 숫자를 뒤집어서 0이 나오면 이를 최대한 줄이고, 그것이 아니면 이것의 -1 한 숫자를 반환한다.
0~9까지 숫자에 맵핑되는 -1 숫자를 9~0으로 다시 만든다. (다소 노가다성의 풀이다)
4. PickByType
From T, pick a set of properties whose type are assignable to U.
For Example
type OnlyBoolean = PickByType<{
name: string
count: number
isReadonly: boolean
isEnable: boolean
}, boolean> // { isReadonly: boolean; isEnable: boolean; }
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
interface Model {
name: string
count: number
isReadonly: boolean
isEnable: boolean
}
type cases = [
Expect<Equal<PickByType<Model, boolean>, { isReadonly: boolean; isEnable: boolean }>>,
Expect<Equal<PickByType<Model, string>, { name: string }>>,
Expect<Equal<PickByType<Model, number>, { count: number }>>,
]
🖌 풀이
type PickByType<T, U> = {
[P in keyof T as T[P] extends U ? P : never] : T[P];
}
as에서 키 뿐만이 아니라 위처럼 값을 가지고도 분기를 할 수 있다. T[P]가 U에 해당하는 경우가 아니면 키값을 never로 지정한다.
5. StartsWith
Implement StartsWith<T, U> which takes two exact string types and returns whether T starts with U
For example
type a = StartsWith<'abc', 'ac'> // expected to be false
type b = StartsWith<'abc', 'ab'> // expected to be true
type c = StartsWith<'abc', 'abcd'> // expected to be false
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<StartsWith<'abc', 'ac'>, false>>,
Expect<Equal<StartsWith<'abc', 'ab'>, true>>,
Expect<Equal<StartsWith<'abc', 'abc'>, true>>,
Expect<Equal<StartsWith<'abc', 'abcd'>, false>>,
Expect<Equal<StartsWith<'abc', ''>, true>>,
Expect<Equal<StartsWith<'abc', ' '>, false>>,
Expect<Equal<StartsWith<'', ''>, true>>,
]
🖌 풀이
type StartsWith<T extends string, U extends string> = T extends `${U}${string}` ? true : false;
재귀로 풀지 않아도 쉽게 풀 수 있는 문제였다. T가 U와 string을 합친 리터럴에 해당하는지 여부를 반환한다.
6. EndsWith
Implement EndsWith<T, U> which takes two exact string types and returns whether T ends with U
For example:
type a = EndsWith<'abc', 'bc'> // expected to be true
type b = EndsWith<'abc', 'abc'> // expected to be true
type c = EndsWith<'abc', 'd'> // expected to be false
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<EndsWith<'abc', 'bc'>, true>>,
Expect<Equal<EndsWith<'abc', 'abc'>, true>>,
Expect<Equal<EndsWith<'abc', 'd'>, false>>,
Expect<Equal<EndsWith<'abc', 'ac'>, false>>,
Expect<Equal<EndsWith<'abc', ''>, true>>,
Expect<Equal<EndsWith<'abc', ' '>, false>>,
]
🖌 풀이
type EndsWith<T extends string, U extends string> = T extends `${string}${U}` ? true : false
위의 StartsWith 문제를 반대로 풀어주면 됬다.
7. PartialByKeys
Implement a generic PartialByKeys<T, K> which takes two type argument T and K.
K specify the set of properties of T that should set to be optional. When K is not provided, it should make all properties optional just like the normal Partial<T>.
For example
interface User {
name: string
age: number
address: string
}
type UserPartialName = PartialByKeys<User, 'name'> // { name?:string; age:number; address:string }
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
interface User {
name: string
age: number
address: string
}
interface UserPartialName {
name?: string
age: number
address: string
}
interface UserPartialNameAndAge {
name?: string
age?: number
address: string
}
type cases = [
Expect<Equal<PartialByKeys<User, 'name'>, UserPartialName>>,
Expect<Equal<PartialByKeys<User, 'name' | 'age'>, UserPartialNameAndAge>>,
Expect<Equal<PartialByKeys<User>, Partial<User>>>,
// @ts-expect-error
Expect<Equal<PartialByKeys<User, 'name' | 'unknown'>, UserPartialName>>,
]
🖌 풀이
type Merge<T> = {
[K in keyof T]: T[K];
}
type PartialByKeys<T, K extends keyof T = keyof T> = Merge<{
[P in keyof T as P extends K ? P : never]?: T[P];
} & {
[P in keyof T as P extends K ? never : P]: T[P];
}>;
풀이는 예상한대로 였지만, 마지막에 두 타입을 Singular Type으로 묶어주기 위해선 Merge 과정이 필요하다.