type-challenges【hard】
Simple Vue
By providing a function name SimpleVue (similar to Vue.extend or defineComponent), it should properly infer the this type inside computed and methods.
In this challenge, we assume that SimpleVue take an Object with data, computed and methods fields as it's only argument,
- data is a simple function that returns an object that exposes the context this, but you won't be accessible to other computed values or methods.
- computed is an Object of functions that take the context as this, doing some calculation and returns the result. The computed results should be exposed to the context as the plain return values instead of functions.
- methods is an Object of functions that take the context as this as well. Methods can access the fields exposed by data, computed as well as other methods. The different between computed is that methods exposed as functions as-is.
次のような SimpleVue
関数を作る問題らしい
- 引数は
data
、computed
、methods
のフィールドを持つオブジェクトだけ -
data
はメソッドで、data
内のthis
は値やメソッドにアクセスできない -
computed
のプロパティはオブジェクトのようにアクセスできる -
methods
内のthis
はdata
、computed
、methods
にアクセスでき、 プロパティはメソッドそのものとしてアクセスできる
compueted
内の this
が何にアクセスできるのかは明確になってないように思う(もしからした Vue の仕様を知っているとわかるのかも)。
ひとまず data
にアクセスできることは必須らしいので、そのつもりで実装する。
data
の戻り値、computed
、methods
の型を指定できるようにする。
declare function SimpleVue<Data, Computed, Methods>(options: {
data: () => Data,
computed: Computed,
methods: Methods
}): any;
data
の this
からプロパティにアクセスできないように、 this
の型を第一引数で指定する。
data: (this: {}) => Data
ThisType
を使って computed
、 methods
の this
に型をつける
computed
のプロパティをオブジェクトのようにアクセスさせるために、 AsValues
を定義して Computed
に適用してから ThisType
を適用する。
type AsValues<T> = {
[Key in keyof T]: T[Key] extends (...args: any[]) => infer R ? R : T[Key]
};
declare function SimpleVue<Data, Computed, Methods>(options: {
data: (this: {}) => Data,
computed: Computed & ThisType<Data>,
methods: Methods & ThisType<Data & AsValues<Computed> & Methods>,
}): any;
computed
内で computed
や methods
のプロパティにアクセスできるようにする場合は、 ThisType
に AsValues<Computed>
や Methods
を追加する
Currying 1
カリー化する。
下のように実装したらうまくいかなかった。
type CurryingType<Args extends any[], Result> =
Args extends [...infer As, infer A]
? CurryingType<As, (args: A) => Result>
: Result;
declare function Currying<Args extends any[], Result>(fn: (...args: Args) => Result):
CurryingType<Args, Result>
const curried1 = Currying((a: string, b: number, c: boolean) => true)
// const curried1: (args: string) => (args: number) => (args: boolean) => boolean
戻り値の true
が boolean
と推論されているけど、 true
そのままで推論されてほしい。
こうやったら true
になったんだけど、理由はよくわからんかった。
declare function Currying<Fn extends Function>(fn: Fn):
Fn extends (...args: infer Args) => infer Result
? CurryingType<Args, Result>
: never;
このままの実装では Currying(() => true);
の型が true
になるので、次のように修正する。
type CurryingType<Args extends any[], Result> =
Args extends [infer A]
? (arg: A) => Result
: Args extends [...infer As, infer A]
? CurryingType<As, (arg: A) => Result>
: () => Result;
const curried3 = Currying(() => true);
// const curried3: () => true
Union to Intersection
Union を Intersection に変換するので型 U
を入力側で扱って反変性を使いたい。
type X<U> = U extends U ? (x: U) => any : never
とすると、 X<'foo' | 42 | true>
は (x: 'foo') => any | (x: 42) => any | (x: true) => any
となる
type UnionToIntersection<U> =
(U extends U ? (x: U) => any : never) extends (x: infer V) => any
? V
: never;
について考えてみる。
V
は 'foo'
、42
、true
のいずれの可能性がある。
また、いずれの場合でも対応できるような型である必要があるので、V = 'foo' & 42 & true
と推論される。
よってこの実装でオッケー。
Get Required
そのものと Required
したものが一致するプロパティだけ返せばいい
type GetRequired<T> =
{ [Key in keyof T as {[K in Key]: T[Key]} extends {[K in Key]-?: T[Key]} ? Key : never]: T[Key] };
Get Optional
Get Required
の逆にしたらいい
type GetOptional<T> =
{ [Key in keyof T as {[K in Key]: T[Key]} extends {[K in Key]-?: T[Key]} ? never : Key]?: T[Key] };
Capitalize Words
文字列の先頭のみ大文字に変換するので、判定のために InString
を追加する。
下のように実装すると、長い文字列ではネストが深すぎてエラーになる。
type StringToCharUnion<S extends string> =
S extends `${infer Head}${infer Tail}`
? Head | StringToCharUnion<Tail>
: never;
type Alphabet = StringToCharUnion<'abcdefghijklmnopqrstuvwxyz'>;
type CapitalizeWords<S extends string, InString = false> =
S extends `${infer Head}${infer Tail}`
? Head extends Alphabet | Uppercase<Alphabet>
? `${InString extends false ? Capitalize<Head> : Head}${CapitalizeWords<Tail & string, true>}`
: `${Head}${CapitalizeWords<Tail & string, false>}`
: '';
type a = CapitalizeWords<'aa!bb@cc#dd$ee%ff^gg&hh*ii(jj)kk_ll+mm{nn}oo|pp🤣qq'>
// Type instantiation is excessively deep and possibly infinite.
末尾再帰にして解決する。
type CapitalizeWords<S extends string, Acc extends string = '', InString = false> =
S extends `${infer Head}${infer Tail}`
? Head extends Alphabet | Uppercase<Alphabet>
? CapitalizeWords<Tail, `${Acc}${InString extends true ? Head : Capitalize<Head>}`, true>
: CapitalizeWords<Tail, `${Acc}${Head}`, false>
: Acc;
CamelCase
簡単のために、 UpperCamelCase にしてから lowerCamelCase にすることを考える。
文字列を _
区切りにして、それぞれを Capitalize
する。
最後に全体を Uncapitalize
したら完成。
type CamelCase<S extends string, Acc extends string = ''> =
S extends `${infer Head}_${infer Tail}`
? CamelCase<Tail, `${Acc}${Capitalize<Lowercase<Head>>}`>
: Uncapitalize<`${Acc}${Capitalize<Lowercase<S>>}`>;
C-printf Parser
文字列から '%'
とその次の文字 C
を抽出する。
C
が keyof ControlsMap
なら ControlsMap[C]
を答えに追加していく。
type ParsePrintFormat<S extends string, Acc extends string[] = []> =
S extends `${string}%${infer C}${infer Rest}`
? C extends keyof ControlsMap
? ParsePrintFormat<Rest, [...Acc, ControlsMap[C]]>
: ParsePrintFormat<Rest, Acc>
: Acc;
Vue Basic Props
This challenge continues from 6 - Simple Vue, you should finish that one first, and modify your code based on it to start this challenge.
In addition to the Simple Vue, we are now having a new props field in the options. This is a simplified version of Vue's props option. Here are some of the rules.
props is an object containing each field as the key of the real props injected into this. The injected props will be accessible in all the context including data, computed, and methods.
A prop will be defined either by a constructor or an object with a type field containing constructor(s).
SimpleVue
の続きとして、次の要件を満たす props
を追加する。
-
props
の各フィールドは、this
のプロパティとして注入される-
props
の各フィールドのキーは注入されるプロパティの名前 -
props
の各フィールドの値は注入されるプロパティの値のコンストラクタか、type
というコンストラクタを保持するフィールドを持つオブジェクト- コンストラクタの配列が渡されたときは、それらの型の Union type とする
- 空オブジェクトが渡されたときは、
any
とする
-
- 注入されるプロパティは
-
props
で注入したthis
のプロパティはdata
、computed
、methods
からアクセス可能である
組み込みオブジェクトのコンストラクタの型は XXXConstructor
のような命名の型になる。(自作の型は typeof ClassA
のようになる)
まず、コンストラクタの型から値の型を取り出す GetTypeOfConstructor
を作る。
実装は、コンストラクタが特定の型のオブジェクトを返す関数としてふるまうことを用いる。
type GetObjectTypeOfConstructor<T> = T extends new (...args: any[]) => infer U ? U : never;
type N = GetObjectTypeOfConstructor<NumberConstructor>
// type N = Number
type R = GetObjectTypeOfConstructor<RegExpConstructor>
// type R = RegExp
オブジェクトの型からプリミティブな型を取り出す GetType
を作る。
実装には valueOf
というメソッドの型を利用する。
既定で、 valueOf メソッドは Object の子孫にあたるあらゆるオブジェクトに継承されています。全ての組み込みコアオブジェクトは適切な値を返すためにこのメソッドを上書きしています。もしオブジェクトがプリミティブな値を持たない場合、 valueOf はオブジェクト自身を返します。
テストケースであるような String
、 Boolean
、 Number
のようなオブジェクトがプリミティブな値を持つようなコンストラクタは、そのプリミティブな値の型を得ることができる。
type GetPrimitiveType<T> = T extends { valueOf: () => infer U} ? U : never;
type B = GetPrimitiveType<Boolean>;
// type B = boolean
type N = GetPrimitiveType<Number>;
// type N = number
type S = GetPrimitiveType<String>;
// type S = string
一方、 RegExp
のようにオブジェクトがプリミティブな値を持たないクラスや、ClassA
のような自作のクラスでは Object
を返されるので、そういう場合はコンストラクタそのものを返すようにしてやる。
type GetPrimitiveTypeIfExists<T> =
T extends { valueOf: () => infer U}
? Object extends U ? T : U
: never;
type C = GetPrimitiveTypeIfExists<ClassA>;
// type C = ClassA
type R = GetPrimitiveTypeIfExists<RegExpConstructor>;
// type R = RegExp
これらを合わせて、コンストラクタからプリミティブな型、あるいはオブジェクトの型を取り出す GetType
を次のように作る。
type GetType<T> = GetPrimitiveTypeIfExists<GetObjectTypeOfConstructor<T>>;
続いて、許容されている props
の渡し方すべての対応するような GetPropType
を実装する。
type GetPropTypeHelper<P> =
P extends { type: (infer Q)[] }
? GetType<Q>
: P extends { type: infer Q }
? GetType<Q>
: P extends (infer Q)[]
? GetType<Q>
: P extends new (...args: any[]) => any
? GetType<P>
: any;
type PropsType<P> = { [Key in keyof P]: GetPropTypeHelper<P[Key]> };
type P = PropsType<{
propA: {};
propB: { type: StringConstructor; };
propC: { type: BooleanConstructor; };
propD: { type: typeof ClassA; };
propE: { type: (NumberConstructor | StringConstructor)[]; };
propF: RegExpConstructor;
}>;
// type P = { propA: any; propB: string; propC: boolean; propD: ClassA; propE: string | number; propF: RegExp; }
最後に、props
の型を指定可能にして、 data
、 computed
、 methods
からアクセス可能にするために SimpleVue
を次のように修正して完了(名前が VueBasicProps
になってる)。
declare function VueBasicProps<Props, Data, Computed, Methods>(options: {
props: Props,
data: (this: PropsType<Props>) => Data,
computed: Computed & ThisType<Data & PropsType<Props>>,
methods: Methods & ThisType<Data & AsValues<Computed> & Methods & PropsType<Props>>,
}): any;
IsAny
わからなかったので @type-challenges/utils
の実装をカンニングした。
type IsAny<T> = 0 extends 1 & T ? true : false;
Intersection type で一方に any
を用いると、他方が unknown
でない限り下の記事にあるように any
と解釈されるらしい。
Typed Get
K
が .
で分離できないときは、 T[K]
を返す。
${infer K1}.${infer K2}
と分解できるときはオブジェクトを T[K1]
、キーを K2
として同様の処理を繰り返す。
type Get<T, K> =
K extends `${infer K1}.${infer K2}`
? K1 extends keyof T
? Get<T[K1], K2>
: never
: K extends keyof T
? T[K]
: never;
String to Number
MinusOne の実装と似ている。
整数として扱っていい文字列か判定する。
次の要件を満たしている文字列か判定する IsInteger
を実装する。
- 文字列が空文字列ではない
- 使っている文字がすべて
0
~9
の数字である - 先頭が
0
になるのは0
そのものの場合のみ
type NumChar = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
type AllNumChar<S extends string> =
S extends `${infer Head}${infer Tail}`
? Head extends NumChar
? AllNumChar<Tail>
: false
: true;
type IsInteger<S extends string> =
S extends ''
? false
: AllNumChar<S> extends false
? false
: S extends `${infer Head}${infer Tail}`
? Head extends '0'
? Tail extends ''
? true
: false
: true
: never;
Tuple の長く伸ばしていき Template literal type で文字列化した値が一致するとき、その Tuple の長さが答えになる。
type ToNumberHelper<S extends string, Acc extends any[] = []> =
`${Acc["length"]}` extends S
? Acc["length"]
: ToNumberHelper<S, [...Acc, any]>;
type ToNumber<S extends string> =
IsInteger<S> extends true
? ToNumberHelper<S>
: never;
Tuple Filter
先頭から順番に見ていく。
Head が Distributive conditional type の影響で分配されないように、配列でラップしたものを比較する
type FilterOut<T extends any[], F, Acc extends any[] = []> =
T extends [infer Head, ...(infer Tail)]
? [Head] extends [F]
? FilterOut<Tail, F, Acc>
: FilterOut<Tail, F, [...Acc, Head]>
: Acc;
Tuple to Enum Object
第二引数の状態は完全に分離して実装する。
第二引数が false
の場合は、 Key remapping を使って簡単に実装できる。
type EnumA<T extends readonly string[]> =
{ readonly [Key in T[number] as Capitalize<Key>]: Key };
type Enum<T extends readonly string[], N extends boolean = false> =
N extends false ? EnumA<T> : never;
第二引数が true
の場合を考える。
string
の配列から string
とその文字列のインデックスのタプルの配列に変換する ToStringIndexTuples
を実装する。
最後の要素から見ていき、注目している要素のインデックスは残りの要素の個数と一致することを利用する。
type ToStringIndexTuples<T extends readonly string[], Acc extends [string, number][] = []> =
T extends readonly [...(infer Init), infer Last]
? Init extends readonly string[]
? ToStringIndexTuples<Init, [[Last & string, Init["length"] & number], ...Acc]>
: never
: Acc;
type a = ToStringIndexTuples<typeof Command>;
// type a = [["echo", 0], ["grep", 1], ["sed", 2], ["awk", 3], ["cut", 4], ["uniq", 5], ["head", 6], ["tail", 7], ["xargs", 8], ["shift", 9]]
タプルの 0 個目をキー、1個目を値としてオブジェクトを作る。
type EnumB<T extends readonly string[]> =
{ readonly [KeyIndex in ToStringIndexTuples<T>[number] as Capitalize<KeyIndex[0]>]: KeyIndex[1] };
type Enum<T extends readonly string[], N extends boolean = false> =
N extends false ? EnumA<T> : EnumB<T>;
EnumA
と EnumB
の処理を共通化させる。
EnumA
の場合の処理がやや冗長になるが、実装はシンプルになった。
type Enum<T extends readonly string[], N extends boolean = false> =
{
readonly [KeyIndex in ToStringIndexTuples<T>[number] as Capitalize<KeyIndex[0]>]:
N extends true ? KeyIndex[1] : KeyIndex[0]
};
printf
C-printf Parser で使った ControlsMap
のように次の型を定義する。
type ControlsMap = {
s: string,
d: number,
}
実装自体は ParsePrintFormat
とほぼ同じ。
はじめ、このように実装したけどうまくいかなかった。
最後のケースのように%d
や %s
が複数あるときに順番が合わなくなる。
type Format<T extends string, Acc = string> =
T extends `${string}%${infer C}${infer Rest}`
? C extends keyof ControlsMap
? Format<Rest, (x: ControlsMap[C]) => Acc>
: Format<Rest, Acc>
: Acc;
type a = Format<'a%dbc%s'>
// type a = (x: string) => (x: number) => string
先頭から見ていって、 '%s'
や '%d'
を見つけたところで、まず型の Tuple に追加しておき、最後に関数の型を構築する方針に切り替える。
関数の型の構築は ComposeFunction
で行う。
type ComposeFunction<T extends any[], Acc = string> =
T extends [infer Head, ...(infer Tail)]
? ComposeFunction<Tail, (x: Head) => Acc>
: Acc
type Format<T extends string, Acc extends any[] = []> =
T extends `${string}%${infer C}${infer Rest}`
? C extends keyof ControlsMap
? Format<Rest, [ControlsMap[C], ...Acc]>
: Format<Rest, Acc>
: ComposeFunction<Acc>;
Deep object to unique
最後のケース IsTrue<Equal<keyof Foo, keyof UniqFoo & string>>
より、キーが string
以外のプロパティを追加してユニークにしたらいいのだとわかる。
ここでは、 symbol
を使った差別化を行う。
type DeepObjectToUniq<O extends object> =
{ [Key in keyof O]: O[Key] extends object ? DeepObjectToUniq<O[Key]> : O[Key] }
& { [Key in symbol]: 'uniq' };
IsFalse<Equal<UniqQuz, UniqFoo['baz']>>
が通らない。ともに { quz: 4; } & { [x: symbol]: "uniq"; }
となっている。
値の 'uniq'
としているところをいい感じにしたい。
型でランダムな構成は作れない(と思ってる。もしかしたらなんかあるかも?)ので、例えばコピー元のオブジェクトの情報を使って一意にしてみる。
type DeepObjectToUniq<O extends object, Uniq = O> =
{ [Key in keyof O]: O[Key] extends object ? DeepObjectToUniq<O[Key], Uniq> : O[Key] }
& { [Key in symbol]: Uniq };
IsFalse<Equal<UniqFoo['bar'], UniqFoo['baz']>>
が通らない。
キーが異なるので、 Uniq
にプロパティのキーも追加して対応する。
type DeepObjectToUniq<O extends object, Uniq extends any[] = [O]> =
{ [Key in keyof O]: O[Key] extends object ? DeepObjectToUniq<O[Key], [...Uniq, Key]> : O[Key] }
& { [Key in symbol]: Uniq };
Length of String 2
The type must support strings several hundred characters long (the usual recursive calculation of the string length is limited by the depth of recursive function calls in TS, that is, it supports strings up to about 45 characters long).
「普通の再帰的な計算だと45くらい文字列長で TypeScript の設定している上限にかかってしまうから、めっちゃ長い文字列でも扱えるようにしてくれ」という問題
type ComposeTupleOfStringLength<S extends string> =
S extends `${infer _}${infer Tail}`
? [any, ...ComposeTupleOfStringLength<Tail>]
: [];
type LengthOfString<S extends string> =
ComposeTupleOfStringLength<S>["length"];
こんな感じで実装すると playground では長さ 48 の文字列までは変換できた。
type a = LengthOfString<'123456789012345678901234567890123456789012345678'>
// type a = 48;
type b = LengthOfString<'1234567890123456789012345678901234567890123456789'>
// error: Type instantiation is excessively deep and possibly infinite.(2589)
末尾再帰で実装して解決。
type LengthOfString<S extends string, Acc extends any[] = []> =
S extends `${infer _}${infer Tail}`
? LengthOfString<Tail, [any, ...Acc]>
: Acc["length"];
Union to Tuple
Your type should resolve to one of the following two types, but NOT a union of them!
Union type で表現されないような型の中のどれか一つを返すように実装する必要がある。
この制約が無い場合は Permutation と同じ
次のような挙動をとる。
僕の環境では string
となったが、環境によって number
になることもあるかもしれない。(少なくとも string
でなければならない理由はなさそう)
type X = ((((x: number) => void) & ((x: string) => void))) extends (x: infer Args) => void ? Args : never;
// type X = string
やってることは Union to Intersection の逆なので、本来は type X = [x: string | number]
となるべきだと思う。
これは仕様の穴をついた実装なのかな。
Union to Intersection の途中の関数の Intersection type と組み合わせて、 Union type から Union type で表現できない型の一つを抽出する Utility type OneOfUnion
を次のように実装する。
type UnionToIntersectionVoidFunction<U> =
(U extends any ? (k: (x: U) => void) => void : never) extends ((k: infer I) => void)
? I
: never;
type OneOfUnion<U> = UnionToIntersectionVoidFunction<U> extends (x: infer Args) => void ? Args : never
type a = OneOfUnion<string | number>;
// type a = number
あとは Permutation
と同じように実装する。
type UnionToTuple<U> =
[U] extends [never]
? []
: OneOfUnion<U> extends infer X
? [X, ...UnionToTuple<Exclude<U, X>>]
: never;
参考
String Join
次のように実装してうまくいかなかった。
type Join<D extends string, P extends string[]> =
P extends [infer First, infer Second, ...infer Rest]
? Rest extends string[]
? `${First & string}${D}${Join<D, [Second & string, ...Rest]>}`
: never
: P extends [infer S]
? S & string
: '';
declare function join<D extends string, P extends string[]>(delimiter: D): (...parts: P) => Join<D, P>
delimiter
が渡された時点で型が確定するが、 P
が string[]
として扱われる。
次のように実装して P
の評価を遅らせるとよい。
declare function join<D extends string>(delimiter: D): <P extends string[]>(...parts: P) => Join<D, P>;
DeepPick
cases
に { a: number } & unknown
のようなものがあるが、 unknown
は Intersection type の単位元として振舞うので意味がない(と思う)。
Union type では一方が unknown
のときの結果は unknown
になる(この場合って零元って呼んでいいの?)
type X = number & unknown;
// type X = number
type Y = number | unknown;
// type Y = unknown
Key
を .
区切りで解析して、オブジェクトを探索していく。
Distributive conditional type で得られた結果の Union type を、 UnionToIntersection
で Intersection type に変換すると完成
type UnionToIntersection<U> =
(U extends U ? (x: U) => any : never) extends (x: infer V) => any
? V
: never;
type Get<O, Key extends string> =
Key extends `${infer Key1}.${infer Key2}`
? Key1 extends keyof O
? Record<Key1, Get<O[Key1], Key2>>
: never
: Key extends keyof O
? Record<Key, O[Key]>
: never;
type DeepPick<O, Key extends string> = UnionToIntersection<Get<O, Key>>;
Pinia
- id - just a string (required)
- state - a function which will return an object as store's state (required)
id
はただの string
state
は状態を表すオブジェクトを返す関数
declare function defineStore<State>(store: {
id: string;
state: () => State
}): State;
- getters - an object with methods which is similar to Vue's computed values or Vuex's getters, and details are below (optional)
And you should use it like this:
store.getSomething
instead of:
store.getSomething() // error
Additionally, getters can access state and/or other getters via this, but state is read-only.
getters
はオブジェクトで、 store
オブジェクトからはメソッドではなくプロパティのようにアクセスできる。
this
から getters
のほかのメソッドと readonly な state
にアクセスできる。
Getter
という型を取って、アクセスする用の型 GetterAccess
と併せて使う。
ThisType
には Readonly<State> & GetterAccess
を使用する。
declare function defineStore<State, Getter, GetterAccess = { [Key in keyof Getter]: Getter[Key] extends () => infer R ? R : never }>(store: {
id: string;
state: () => State
getters: Getter & ThisType<Readonly<State> & GetterAccess>
}): State & GetterAccess;
- actions - an object with methods which can do side effects and mutate state, and details are below (optional)
Using it is just to call it:
const returnValue = store.doSideEffect()
Actions can return any value or return nothing, and it can receive any number of parameters with different types. Parameters types and return type can't be lost, which means type-checking must be available at call side.
State can be accessed and mutated via this. Getters can be accessed via this but they're read-only.
あとは Actions
を追加して完成。
declare function defineStore<State, Getter, Actions, GetterAccess = { [Key in keyof Getter]: Getter[Key] extends () => infer R ? R : never }>(store: {
id: string;
state: () => State
getters: Getter & ThisType<Readonly<State> & GetterAccess>
actions: Actions & ThisType<State & Actions>
}): State & GetterAccess & Actions;
Camelize
キーの文字列を camelize する型を作る。
'_'
で分割し、分割した後のほうを capitalize する。
type CamelizeString<S extends string, Acc extends string = ''> =
S extends `${infer S1}_${infer S2}`
? CamelizeString<Capitalize<S2>, `${Acc}${S1}`>
: `${Acc}${S}`;
Record
と Array
でそれぞれの capitalize を実装し、かつ相互再帰の形にする。
type CamelizeArray<T extends any[], Acc extends any[] = []> =
T extends [infer Head, ...infer Tail]
? CamelizeArray<Tail, [...Acc, Camelize<Head>]>
: Acc;
type Camelize<T> =
T extends any[]
? CamelizeArray<T>:
T extends object
? { [Key in keyof T as CamelizeString<Key & string>]: Camelize<T[Key]> }
: T;
DropString
type StringToCharUnion<S extends string, Acc extends string = never> =
S extends `${infer Head}${infer Tail}`
? StringToCharUnion<Tail, Acc | Head>
: Acc;
type FilterChar<S extends string, C extends string, Acc extends string = ''> =
S extends `${infer Head}${infer Tail}`
? Head extends C
? FilterChar<Tail, C, Acc>
: FilterChar<Tail, C, `${Acc}${Head}`>
: Acc;
type DropString<S extends string, R extends string> =
R extends '' ? S : FilterChar<S, StringToCharUnion<R>>;
Split
テストケースにある、第一引数が string
のケースは特殊扱いしてよさそう。
SEP
が ''
のときは、すべての文字で分割するのでこれも特別扱いする。
SEP
が空文字列でないケースは、 Template literal type でマッチさせる(SplitSub
)
type SplitAll<S extends string, Acc extends string[] = []> =
S extends `${infer Head}${infer Tail}`
? SplitAll<Tail, [...Acc, Head]>
: Acc;
type SplitSub<S extends string, SEP extends string, Acc extends string[] = []> =
S extends `${infer S1}${SEP}${infer S2}`
? SplitSub<S2, SEP, [...Acc, S1]>
: [...Acc, S];
type Split<S extends string, SEP extends string> =
string extends S
? string[]
: SEP extends ''
? SplitAll<S>
: SplitSub<S, SEP>;
なんか末尾再帰をよく使い始めたなこいつ
IsRequiredKey
第二引数で指定されたキーが required であればよい。
対象のキーだけ抽出したものと、それを Required
したものが一致するか判定すればよい。
type IsRequiredKey<T, K extends keyof T> =
Equal<Required<Pick<T, K>>, Pick<T, K>>;
ObjectFromEntries
Key remapping を使って実装する
type ObjectFromEntries<T extends [string, any]> =
{ [U in T as U[0]]: U[1] };
IsPalindrome
長さ1以下の文字列は回文であると決める。
それ以外の長さのとき、先頭と末尾の文字が一致するなら、それらを除いた残りが回文か判定する。
一致しないなら回文ではない。
type IsPalindrome<T extends string | number, U extends string = `${T}`> =
U extends `${infer Head}${infer Tail}`
? Tail extends '' // T は長さ1
? true
: Tail extends `${infer W}${Head}`
? IsPalindrome<W, W>
: false
: true; // T は長さ0
MutableKeys
IsRequiredKey
と似てる。
Distributive conditional type を使って、 Readonly
をして一致するものだけの Union type を作れば完成
type MutableKeys<T, U = keyof T> =
U extends any
? Equal<Readonly<Pick<T, U & keyof T>>, Pick<T, U & keyof T>> extends false
? U
: never
: never;
Intersection
Tuple なのか Union type なのか処理が分かれるのが面倒なので、一律で扱うための関数を作る。
Intersection
の第一引数で与えられる Tuple の各要素はすべて ToUnion
を適用してから使うものとする。
type ToUnion<T> = T extends any[] ? T[number] : T;
引数の先頭要素を Target
、それ以外を Others
とする。
解となる Union type の要素はすべて Target
の要素でもあるので、 Target
の各要素について Others
の各要素に含まれるかを判定する。
ある要素が Others
の各要素に含まれるか判定するには、 EveryHasX
を利用する。
type EveryHasX<L extends any[], X> =
L extends [infer Head, ...infer Tail]
? X extends ToUnion<Head>
? EveryHasX<Tail, X>
: false
: true;
type IntersectionSub<Target, Others extends any[]> =
Target extends any
? EveryHasX<Others, Target> extends true
? Target
: never
: never;
type Intersection<T extends any[]> =
T extends [infer Head, ...infer Tail]
? Head extends any[]
? IntersectionSub<ToUnion<Head>, Tail>
: never
: never;
BinaryToDecimal
先頭から順番に見ていく。
次の桁をみるたびにこれまでに見た分の解を2倍し、先頭が1のときだけ1足していく。
type BinaryToDecimal<S extends string, Acc extends any[] = []> =
S extends ''
? Acc["length"]
: S extends `1${infer T}`
? BinaryToDecimal<T, [...Acc, ...Acc, any]>
: S extends `0${infer T}`
? BinaryToDecimal<T, [...Acc, ...Acc]>
: never;
Object Key Paths
オブジェクトのキーをたどって構成する path について考える。
現在見ているオブジェクトのキーが string
なら '.'
区切りで、 number なら [${number}]
の形式も追加する必要がある。
type ComposePath<P1 extends string, P2 extends string | number> =
[P1] extends [never]
? `${P2}`
: P2 extends number
? `${P1}.${P2}` | `${P1}[${P2}]` | `${P1}.[${P2}]`
: `${P1}.${P2}`;
対象のオブジェクトの値が object
なら Acc
に Path
を追加してさらに深堀り、それ以外なら Acc
と現在のパスを表す Path
を返す。
type ObjectKeyPaths<T extends object, Path extends string = never, Acc = never, Key = keyof T> =
Key extends keyof T
? T[Key] extends object
? ObjectKeyPaths<T[Key], ComposePath<Path, Key & (string | number)>, Acc | Path>
: ComposePath<Path, Key & (string | number)> | Path | Acc
: never;
Two Sum
配列の先頭から見ていって、今見ている数と足して U
になる数がないかを探す。
type NumberToLengthTuple<T extends number, Acc extends any[] = []> =
Acc["length"] extends T
? Acc
: NumberToLengthTuple<T, [...Acc, any]>;
type Minus<T extends number, U extends number, Ttuple extends any[] = NumberToLengthTuple<T> , Utuple extends any[] = NumberToLengthTuple<U>> =
[Ttuple, Utuple] extends [[any, ...infer TtupleNext], [any, ...infer UtupleNext]]
? Minus<T, U, TtupleNext, UtupleNext>
: Utuple extends []
? Ttuple["length"]
: never;
type FindX<Cand extends any[], X> =
Cand extends [infer Head, ...infer Next]
? Head extends X
? true
: FindX<Next, X>
: false;
type TwoSum<T extends number[], U extends number> =
T extends [infer Head, ...infer Tail]
? FindX<Tail, Minus<U, Head & number>> extends true
? true
: Tail extends number[]
? TwoSum<Tail, U>
: never
: false;
ValidDate
月と日の関係を次のように定義する。
type Month = '01' | '02' | '03' | '04' | '05' | '06' | '07' | '08' | '09' | '10' | '11' | '12';
type Date28 = '01' | '02' | '03' | '04' | '05' | '06' | '07' | '08' | '09' | '10' | '11' | '12' | '13' | '14' | '15' | '16' | '17' | '18' | '19' | '20' | '21' | '22' | '23' | '24' | '25' | '26' | '27' | '28';
type Date30 = Date28 | '29' | '30';
type Date31 = Date30 | '31';
type DateOfMonth = {
'01': Date31,
'02': Date28,
'03': Date31,
'04': Date30,
'05': Date31,
'06': Date30,
'07': Date31,
'08': Date31,
'09': Date30,
'10': Date31,
'11': Date30,
'12': Date31,
};
一番範囲の広い Date31
で日付を仮置きし、月を確定させる。
月が確定したら再度、日が正しいことを確認して完了。
type ValidDate<T extends string> =
T extends `${infer M}${Date31}`
? M extends Month
? T extends `${M}${DateOfMonth[M]}`
? true
: false
: false
: false;
Assign
複数アサインするケースは一旦無視して、一つのオブジェクトをアサインできるようにする。
テストケースをみると、共通のキーに対してはアサインする側の値で上書きしているのがわかる。
Conditional type の順番で解決できる。
type AssignSub<T extends Record<string, unknown>, S extends Record<string, unknown>> =
{ [Key in keyof T | keyof S]: Key extends keyof S ? S[Key] : Key extends keyof T ? T[Key] : never }
これを先頭から順番に適用していく。アサインする側がオブジェクト型出ない場合は無視する。
type Assign<T extends Record<string, unknown>, U extends any[]> =
U extends [infer Head, ...infer Tail]
? Head extends Record<string, unknown>
? Assign<AssignSub<T, Head>, Tail>
: Assign<T, Tail>
: T;
Capitalize Nest Object Keys
オブジェクトの各キーを Capitalize
し、値に CapitalizeNestObjectKeys
を適用する。
値が tuple だった場合は、 CapitalizeTuple
で tuple の各要素に CapitalizeNestObjectKeys
を適用する。
type CapitalizeTuple<T extends any[], Acc extends any[] = []> =
T extends [infer Head, ...infer Tail]
? CapitalizeTuple<Tail, [...Acc, CapitalizeNestObjectKeys<Head>]>
: Acc;
type CapitalizeNestObjectKeys<T> =
T extends object
? T extends any[]
? CapitalizeTuple<T>
: {
[Key in keyof T as Capitalize<Key & string>]: CapitalizeNestObjectKeys<T[Key]>
}
: T;
Run-length encoding
encode
'AAA'
のように同じ文字が続いたときに数字と文字の組み合わせ '3A'
に変換する。
ただし、文字が1文字だった場合は数字を省略する。
この条件を満たすように文字と数字から文字列を生成する ComposeEncode
を作る。
(後の都合で0文字の場合も省略している)
type ComposeEncode<T extends string, L extends number> =
L extends 0
? ''
: L extends 1
? T
: `${L}${T}`;
直前に注目していた文字 Pre
と、 Pre
が連続した回数 C
を保持しておく。
現在の先頭の文字が Pre
と一致しているなら C
を追加する。
そうでない場合は ComposeEncode
を使って生成した文字列を Acc
に追加していく。
type Encode<S extends string, Pre extends string = '', C extends any[] = [], Acc extends string = ''> =
S extends `${infer Head}${infer Tail}`
? Head extends Pre
? Encode<Tail, Pre, [...C, any], Acc>
: Encode<Tail, Head, [any], `${Acc}${ComposeEncode<Pre, C["length"]>}`>
: `${Acc}${ComposeEncode<Pre, C["length"]>}`;
decode
今度は数字と文字を受け取って、その数字分だけ文字つなげる ComposeDecode
を実装する。
数字がない場合は長さ1とみなすこととする。
文字列 A
に対して A["length"]
をすると number
になるので、具体的な長さを得るために tuple の C
をあわせて使っている。
type ComposeDecode<N extends string, A extends string, Acc extends string = '', C extends any[] = []> =
N extends ""
? A
: `${C["length"]}` extends N
? Acc
: ComposeDecode<N, A, `${Acc}${A}`, [...C, any]>;
デコードしたい文字列を先頭から見ていき、文字が数字か否かで処理を分ける。
数字以外の文字が出るまで N
に蓄積しておいて、ComposeDecode
に適用し Acc
を更新する。
type NumChar = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
type Decode<S extends string, Acc extends string = '', N extends string = ''> =
S extends `${infer Head}${infer Tail}`
? Head extends NumChar
? Decode<Tail, Acc, `${N}${Head}`>
: Decode<Tail, `${Acc}${ComposeDecode<N, Head>}`>
: Acc;
hard もおしまい
残りは extreme