Closed41

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 関数を作る問題らしい

  • 引数は datacomputedmethods のフィールドを持つオブジェクトだけ
  • data はメソッドで、 data 内の this は値やメソッドにアクセスできない
  • computed のプロパティはオブジェクトのようにアクセスできる
  • methods 内の thisdatacomputedmethods にアクセスでき、 プロパティはメソッドそのものとしてアクセスできる

compueted 内の this が何にアクセスできるのかは明確になってないように思う(もしからした Vue の仕様を知っているとわかるのかも)。
ひとまず data にアクセスできることは必須らしいので、そのつもりで実装する。

data の戻り値、computedmethods の型を指定できるようにする。

declare function SimpleVue<Data, Computed, Methods>(options: {
  data: () => Data,
  computed: Computed,
  methods: Methods
}): any;

datathis からプロパティにアクセスできないように、 this の型を第一引数で指定する。

data: (this: {}) => Data

ThisType を使って computedmethodsthis に型をつける

https://www.typescriptlang.org/docs/handbook/utility-types.html#thistypetype

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 内で computedmethods のプロパティにアクセスできるようにする場合は、 ThisTypeAsValues<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

戻り値の trueboolean と推論されているけど、 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'42true のいずれの可能性がある。
また、いずれの場合でも対応できるような型である必要があるので、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] };
かしかし

Required Keys

問題

GetRequired を使うと簡単

type RequiredKeys<T> = keyof GetRequired<T>;

Optional Keys

問題

type OptionalKeys<T> = keyof GetOptional<T>;
かしかし

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 を抽出する。
Ckeyof 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 のプロパティは datacomputedmethods からアクセス可能である

組み込みオブジェクトのコンストラクタの型は 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 というメソッドの型を利用する。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/valueOf

既定で、 valueOf メソッドは Object の子孫にあたるあらゆるオブジェクトに継承されています。全ての組み込みコアオブジェクトは適切な値を返すためにこのメソッドを上書きしています。もしオブジェクトがプリミティブな値を持たない場合、 valueOf はオブジェクト自身を返します。

テストケースであるような StringBooleanNumber のようなオブジェクトがプリミティブな値を持つようなコンストラクタは、そのプリミティブな値の型を得ることができる。

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 の型を指定可能にして、 datacomputedmethods からアクセス可能にするために 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;
かしかし

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 の実装と似ている。

https://zenn.dev/link/comments/5a2b831826da34

整数として扱っていい文字列か判定する。
次の要件を満たしている文字列か判定する 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>;

EnumAEnumB の処理を共通化させる。
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;

参考

https://zenn.dev/dqn/articles/union-to-tuple
https://github.com/type-challenges/type-challenges/issues/3112

かしかし

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 が渡された時点で型が確定するが、 Pstring[] として扱われる。
次のように実装して 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}`;

RecordArray でそれぞれの 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 なら AccPath を追加してさらに深堀り、それ以外なら 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;
このスクラップは2022/10/21にクローズされました