k-tokitoh

2022-05-08

type challenges - medium

type-challenges/type-challenges

get return type

type MyReturnType<T extends (...args: any) => unknown> = T extends (
  ...args: any
) => infer U
  ? U
  : never;

omit

type MyOmit<
  T extends { [key in K]: unknown },
  K extends string | number | symbol
> = { [P in MyExclude<keyof T, K>]: T[P] };

type MyExclude<T, U> = T extends U ? never : T;

readonly 2

type MyReadonly2<T, K extends keyof T = keyof T> = {
  readonly [P in K]: T[P];
} & { [Q in MyExclude<keyof T, K>]: T[Q] };

type MyExclude<T, U> = T extends U ? never : T;

deep readonly

type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends Record<string, unknown> | unknown[]
    ? DeepReadonly<T[P]>
    : T[P];
};

いわゆる key-value なオブジェクトどうやって判定するねんと思ったら Record か。

tuple to union

type TupleToUnion<T extends unknown[]> = T[number];

chainable options

// todo

last of array

type Last<T extends unknown[]> = T extends [...infer R, infer L] ? L : never;

pop

type Pop<T extends unknown[]> = T extends [...infer R, infer L] ? R : never;

promise all

// todo

type lookup

// todo

trim left

最初の文字が空白ならそれを取り除いて再帰的に呼び出す、最初の文字が空白でなくなった時点でその文字列を返す。

type WhiteSpace = " " | "\n" | "\t";
type TrimLeft<S extends string> = S extends `${infer F}${infer R}`
  ? F extends WhiteSpace
    ? TrimLeft<R>
    : S
  : "";

trim

infer で最初のを取り出すのは、配列型でも string でもできる。

type FirstOfArray<T extends unknown[]> = T extends [infer F, ...infer R]
  ? F
  : never;
type A = FirstOfArray<["a", "b", "c"]>; // "a"

type FirstOfString<T extends string> = T extends `${infer F}${infer R}`
  ? F
  : never;
type X = FirstOfString<"xyz">; // "x"

しかし最後のを取り出すのは、配列型ではできるが、string ではできない。

type LastOfArray<T extends unknown[]> = T extends [...infer R, infer L] ? L : never
type C = LastOfArray<["a", "b", "c"]>  // "c"

type LastOfString<T extends string> = T extends `${...infer R}, ${infer L}` ? L : never  // error

それゆえ TrimLeft と対象に TrimRight を定義することはできなかった。

文字列をひっくり返すことはできそうなので、TrimLeft => Reverse => TrimLeft => Reverse することにした。

type WhiteSpace = " " | "\n" | "\t";
type Trim<S extends string> = Reverse<TrimLeft<Reverse<TrimLeft<S>>>>;
type TrimLeft<S extends string> = S extends `${infer F}${infer R}`
  ? F extends WhiteSpace
    ? TrimLeft<R>
    : S
  : "";
type Reverse<
  S extends string,
  A extends string = ""
> = S extends `${infer F}${infer R}` ? Reverse<R, `${F}${A}`> : A;

capitalize

// todo

replace

// todo

replace all

// todo

append argument

// todo

permutation

// todo

length of string

// todo

flatten

// todo

append to object

// todo

absolute

数値の加減乗除はできないので 0 に対する加減算でふつうに絶対値を取得することはできないな。

再帰の回数で数値をインクリメントすることはできるけど、それでは負の値は扱えないな。

設問条件やテストケースから察するに、文字列のパターンマッチでいけばよさそう。

パターンマッチはテンプレートリテラルと infer の組み合わせでできる。

type Absolute<T extends number | string | bigint> = `${T}` extends `-${infer A}`
  ? A
  : `${T}`;

string to union

これは仕様で、テンプレートリテラルの中で infer を複数連続して用いると、最後以外は一文字ずつバインド、最後に infer された型変数に残りの文字列全体がバインドされるらしい。

type First<T extends string> = T extends `${infer F}${infer S}${infer R}`
  ? F
  : never;
type Second<T extends string> = T extends `${infer F}${infer S}${infer R}`
  ? S
  : never;
type Rest<T extends string> = T extends `${infer F}${infer S}${infer R}`
  ? R
  : never;

type first = First<"hello">; // "h"
type second = Second<"hello">; // "e"
type rest = Rest<"hello">; // "llo"

文字列が短い場合にはどうなるか。

type first_of_he = First<"he">; // "h"
type second_of_he = Second<"he">; // "e"
type rest_of_he = Rest<"he">; // ""

type first_of_h = First<"h">; // never
type second_of_h = Second<"h">; // never
type rest_of_h = Rest<"h">; // never

空文字の infer が成り立てば空文字を infer する。空文字の infer さえ成り立たないと割り当て不可能という判定になる。

この性質を利用して一文字ずつ取り出しして、再帰的に union でがっちゃんこしていけばよい。

type StringToUnion<T extends string> = StringToUnionBase<T, never>;
type StringToUnionBase<
  T extends string,
  U extends string
> = T extends `${infer F}${infer R}` ? StringToUnionBase<R, U | F> : U;

merge

// todo

kebab case

template literals & infer で一文字ずつとりだして大文字ならハイフンと小文字に置き換えて accumulator として置換後の文字列を伸ばしていけばいい。

大文字の判定なんて便利なものはないので、大文字の union に対する割り当て可能性で判定するしかないだろう。

置換はどうするか? 別の問題で index を求めるのがあったので、upper/lower それぞれを 26 要素の配列型にしておき、大文字の配列型における index を求めて、小文字の配列型に index signature でアクセスすればよさそう。

type Upper = [
  "A",
  "B",
  "C",
  "D",
  "E",
  "F",
  "G",
  "H",
  "I",
  "J",
  "K",
  "L",
  "M",
  "N",
  "O",
  "P",
  "Q",
  "R",
  "S",
  "T",
  "U",
  "V",
  "W",
  "X",
  "Y",
  "Z"
];
type Lower = [
  "a",
  "b",
  "c",
  "d",
  "e",
  "f",
  "g",
  "h",
  "i",
  "j",
  "k",
  "l",
  "m",
  "n",
  "o",
  "p",
  "q",
  "r",
  "s",
  "t",
  "u",
  "v",
  "w",
  "x",
  "y",
  "z"
];
type KebabCase<S extends string> = S extends `${infer F}${infer R}`
  ? F extends Upper[number]
    ? KebabCaseBase<R, `${ToLower<F>}`>
    : KebabCaseBase<R, `${F}`>
  : "";
type KebabCaseBase<
  S extends string,
  A extends string
> = S extends `${infer F}${infer R}`
  ? F extends Upper[number]
    ? KebabCaseBase<R, `${A}-${ToLower<F>}`>
    : KebabCaseBase<R, `${A}${F}`>
  : A;
type ToLower<S extends Upper[number]> = Lower[IndexOf<Upper, S>];
type IndexOf<T extends unknown[], U extends unknown> = IndexOfBase<T, U, []>;
type IndexOfBase<T, U, Counter extends null[]> = T extends [infer F, ...infer R]
  ? Equal<F, U> extends true
    ? Counter["length"]
    : IndexOfBase<R, U, [...Counter, null]>
  : -1;

という感じで自力クリアしたけど、他の回答みたら Record つかえばこんなややこしいことしないで済むと気づいた…。

type Mapper = {
  A: "a";
  B: "b";
  C: "c";
  D: "d";
  E: "e";
  F: "f";
  G: "g";
  H: "h";
  I: "i";
  J: "j";
  K: "k";
  L: "l";
  M: "m";
  N: "n";
  O: "o";
  P: "p";
  Q: "q";
  R: "r";
  S: "s";
  T: "t";
  U: "u";
  V: "v";
  W: "w";
  X: "x";
  Y: "y";
  Z: "z";
};

type KebabCase<S extends string> = S extends `${infer F}${infer R}`
  ? F extends keyof Mapper
    ? KebabCaseBase<R, `${ToLower<F>}`>
    : KebabCaseBase<R, `${F}`>
  : "";
type KebabCaseBase<
  S extends string,
  A extends string
> = S extends `${infer F}${infer R}`
  ? F extends keyof Mapper
    ? KebabCaseBase<R, `${A}-${ToLower<F>}`>
    : KebabCaseBase<R, `${A}${F}`>
  : A;
type ToLower<S extends keyof Mapper> = Mapper[S];

そしてさらに UpperCase/LowerCase という utility types があるの、知らなかった..!

type KebabCase<S extends string> = S extends `${infer F}${infer R}`
  ? F extends Lowercase<F>
    ? KebabCaseBase<R, `${F}`>
    : KebabCaseBase<R, `${Lowercase<F>}`>
  : "";
type KebabCaseBase<
  S extends string,
  A extends string
> = S extends `${infer F}${infer R}`
  ? F extends Lowercase<F>
    ? KebabCaseBase<R, `${A}${F}`>
    : KebabCaseBase<R, `${A}-${Lowercase<F>}`>
  : A;

注意すべきは以下。

diff

// todo

any of

// todo

is never

// todo

is union

最初 union を配列に置き換えて length とるとか? と思ったけど、そういう回答例は見当たらなくて以下。

union の場合にのみ union distribution が発生することを利用する。テクいな…。

type IsUnion<T> = IsUnionBase<T, T>;
type IsUnionBase<T, C> = T extends infer TN
  ? [C] extends [TN]
    ? false
    : true
  : never;

T がstring | numberなら TN にはstring, numberがそれぞれ分配してバインドされる。

これらが元のstring | numberと一致しないということを炙り出したい。

TN extends Cだと一致しないけど割り当て可能と判定されてしまうので、TN(string)を extends の右側、C(string | number)を extends の左側に書くことに注意する。

しかしシンプルにC extends TNと書くと C(string | number)が分配されてしまい、

string extends string ? true : false | number extends string ? true : false

すなわちtrue | falseを返してしまう。

これを避けるためにC extends TNではなく[C] extends [TN]としてあげれば完成。

replace keys

// todo

remove index signature

// todo

percentage parser

// todo

drop char

// todo

minus one

// todo

pick by type

// todo

starts with

template literals と infer を用いたマッチングで素直にできる。

type StartsWith<T extends string, U extends string> = T extends `${U}${infer R}`
  ? true
  : false;

ends with

starts with と同様。

type EndsWith<T extends string, U extends string> = T extends `${infer R}${U}`
  ? true
  : false;

partial by keys

// todo

required by keys

まず、以下のように mapped type の key の部分で conditional type をつかうことができる。

type MyExclude<T, U extends number | string | symbol> = {
  [k in keyof T as k extends U ? never : k]: T[k];
};

type Man = { name: string; age: number };
type AgeOnly = MyExclude<Man, "name">; // type AgeOnly = { age: number; }

これを利用して以下のように書いてみる。

type RequiredByKeys<
  T extends Record<string | number | symbol, any>,
  K extends number | string | symbol = keyof T
> = {
  [P in keyof T as P extends K ? P : never]-?: T[P];
} & {
  [P in keyof T as P extends K ? never : P]?: T[P];
};

これだと以下のように交差型になってしまい、テストがとおらない。

type Intersection = RequiredByKeys<User, "name">;

// type Intersection = {
//     name: string;
// } & {
//     age?: number | undefined;
//     address?: string | undefined;
// }

交差型をまとめるために、infer して mapped type をとおしてあげれば OK.

type RequiredByKeys<
  T extends Record<string | number | symbol, any>,
  K extends number | string | symbol = keyof T
> = {
  [P in keyof T as P extends K ? P : never]-?: T[P];
} & {
  [P in keyof T as P extends K ? never : P]?: T[P];
} extends infer R
  ? { [P in keyof R]: R[P] }
  : never;

mutable

// todo

omit by type

// todo

object entries

// todo

shift

// todo

tuple to nested object

// todo

reverse

// todo

flip arguments

// todo

flatten depth

// todo

bem style string

product 的なのどうやってつくればええねんと思ったけど、

type Countries = ["us", "jp"];
type Country = Countries[number]; // "us" | "jp"

これで配列型から union に展開してくれるのを、ひとつの型の記述の中で複数用いると、product な感じで union に展開してくれるようだ。

まずふつうに template literals つかいつつ union に展開する。

type BE<B extends string, E extends string[]> = `${B}__${E[number]}`;
type be = BE<"b", ["e1", "e2"]>; // "b__e1" | "b__e2"

続いて複数を埋め込んで展開する。

type BEM<
  B extends string,
  E extends string[],
  M extends string[]
> = `${B}__${E[number]}--${M[number]}`;
type bem = BEM<"b", ["e1", "e2"], ["m1", "m2"]>; // "b__e1--m1" | "b__e1--m2" | "b__e2--m1" | "b__e2--m2"

しかしこれだと E, M のいずれかが空配列だった場合、never を返却してしまう。上記で 2 * 2 => 4 通りの union になっていたのが、n * 0 => 0 通りになるということだろう。

これに対応するため、conditional type で空配列の場合を別途扱う。まず E のみの場合。

type BE<B extends string, E extends string[]> = E extends []
  ? B
  : `${B}__${E[number]}`;
type be0 = BE<"b", ["e1", "e2"]>; //  "b__e1" | "b__e2"
type be1 = BE<"b", []>; // "b"

続いて M も。これで完成。

type BEM<
  B extends string,
  E extends string[],
  M extends string[]
> = E extends []
  ? M extends []
    ? B
    : `${B}--${M[number]}`
  : M extends []
  ? `${B}__${E[number]}`
  : `${B}__${E[number]}--${M[number]}`;

in order traversal

// todo

flip

// todo

fibonacci sequence

// todo

all combinations

// todo

greater than

型のコードでは条件判定は extends を用いてしか行うことができない。なので数字の大小の問題を割り当て可能性の問題に置き換える必要がある。

3 extends 3 ? true : falseのように数字の同一性を判定できることに着目する。

再帰をつかって extends の右側をカウントアップしていくことを考える。

値を直接インクリメントすることはできないが、n 個の要素からなる配列型を n+1 個の要素からなる配列型にして再帰的な呼び出しをすることは可能なので、それを利用する。

type GreaterThan<T extends number, U extends number> = GreaterThanBase<
  T,
  U,
  []
>;
type GreaterThanBase<
  T extends number,
  U extends number,
  R extends null[]
> = T extends R["length"]
  ? false
  : U extends R["length"]
  ? true
  : GreaterThanBase<T, U, [...R, null]>;

zip

// todo

is tuple

// todo

chunk

// todo

fill

// todo

trim right

// todo

without

配列の型を一気に得ることはできないので、どれかの配列をベースにして再帰的に組み立てる必要がある。

型引数を T, U とした場合、

type Without<
  T extends number[],
  U extends number | number[]
> = U extends number[]
  ? WithoutBase<T, U[number], []>
  : U extends number
  ? WithoutBase<T, U, []>
  : never;
type WithoutBase<
  T extends number[],
  U extends number,
  V extends number[]
> = T extends [infer F, ...infer R]
  ? F extends U
    ? R extends number[]
      ? WithoutBase<R, U, V>
      : never
    : R extends number[]
    ? F extends number
      ? WithoutBase<R, U, [...V, F]>
      : never
    : never
  : V;

number | number[]extends numberで分岐したときに、割り当て不可能な方をnumber[]と推論してくれないみたい。

なのでそういう箇所で conditional type を再度書いて推論を効かせるということをたくさんやる羽目になってしまった。

どうもunknown | unknown[]にしているとこの問題は回避できるらしい。仕様?

type Without<
  T extends unknown[],
  U extends unknown | unknown[]
> = U extends unknown[] ? WithoutBase<T, U[number], []> : WithoutBase<T, U, []>;
type WithoutBase<
  T extends unknown[],
  U extends unknown,
  V extends unknown[]
> = T extends [infer F, ...infer R]
  ? F extends U
    ? WithoutBase<R, U, V>
    : WithoutBase<R, U, [...V, F]>
  : V;

trunc

// todo

index of

一発で配列の中身を走査することはできないので、再帰で回す方針とする。

index を出力するために、呼び出しごとに値をインクリメントする必要がある。しかし値を+1 することはできないので、代わりに配列型を用意して要素をひとつずつ追加していき、一致する要素がみつかった時点でその配列型の length を返す。

type IndexOf<T extends unknown[], U extends unknown> = IndexOfBase<T, U, []>;
type IndexOfBase<T, U, Counter extends null[]> = T extends [infer F, ...infer R]
  ? Equal<F, U> extends true
    ? Counter["length"]
    : IndexOfBase<R, U, [...Counter, null]>
  : -1;

join

一発で join した型を得ることはできなそうなので再帰でやる。

type Delimiter = string | number;
type Join<T extends string[], D extends Delimiter> = T extends [
  infer F,
  ...infer R
]
  ? F extends string
    ? R extends string[]
      ? JoinBase<R, D, F>
      : never
    : never
  : never;
type JoinBase<
  T extends string[],
  D extends Delimiter,
  A extends string
> = T extends [infer F, ...infer R]
  ? F extends string
    ? R extends string[]
      ? JoinBase<R, D, `${A}${D}${F}`>
      : never
    : never
  : A;

F, R を infer したときにそれぞれstring, string[]と推論してくれないようなので追って extends で推論させることで再帰呼び出しを可能にした。

unknown とかをつかったら以下のとおり少し減らせたけど、多少は extends で型を推論させることが必要と思われる。

type Delimiter = string | number;
type Join<T extends unknown[], D extends Delimiter> = T extends [
  infer F,
  ...infer R
]
  ? F extends string
    ? JoinBase<R, D, F>
    : never
  : never;
type JoinBase<
  T extends unknown[],
  D extends Delimiter,
  A extends string
> = T extends [infer F, ...infer R]
  ? F extends string
    ? JoinBase<R, D, `${A}${D}${F}`>
    : never
  : A;

last index of

// todo

unique

// todo

map types

// todo

construct tuple

// todo

number range

// todo

combination

// todo

subsequence

// todo