<書評> type-challengesでeasyからmediumまで81問解いて学んだTSの型プログラミング

published_at: 2022-08-04

概要

type-challenges というTS文法の練習問題リポジトリがある。

興味本位で解いてみたら楽しくなってきてeasyレベルからmediumレベルまで81問解いてしまった。

その際にTSについての学びが色々あったので共有する。

学び一覧

  • mapped types
  • conditional types
  • infer
  • minus operator
  • union distribution
  • never
  • template literal types
  • length property
  • index(numeric) signature
  • bottom type

mapped types

公式doc: https://www.typescriptlang.org/docs/handbook/2/mapped-types.html 型引数が主にユニオン型orオブジェクトの場合に利用することが多い。型引数をイテレーションして結果出力する。

1type MappedTypes<T extends object> = { [P in keyof T]: T[P] }; 2type s = MappedTypes<{ key: 'test'; value: 'test' }>; 3// type s = { 4// key: 'test'; 5// value: 'test'; 6// } 7 8type MappedTypes2<T extends string> = { [P in T]: string }; 9type t = MappedTypes2<'test' | 'tt'>; 10// type t = { 11// test: string; 12// tt: string; 13// }

conditional types

公式doc: https://www.typescriptlang.org/docs/handbook/2/conditional-types.html 型の条件分岐を実現する。

1TypeA extends TypeB ? true : false

公式docによるとこの条件はextendsの左の型が右の型に代入可能かどうかを判定している。

When the type on the left of the extends is assignable to the one on the right, then you’ll get the type in the first branch (the “true” branch); otherwise you’ll get the type in the latter branch (the “false” branch).

そのためこれはもちろんfalse

1type test = string extends number ? true : false; 2// => false

こちらは原則から考えてtrueになる。

1type test = 't' extends string ? true : false; 2// => true

bottom typeの節でneverやundefinedをconditional typesで比較検証する。

infer

公式doc 型を割り出してその後利用できる。

conditional typesとセットで利用する。

例えば、type-challengesのLast of Array の問題では下記のように解ける。

型引数Tをconditional typesで分岐させ、その中でinferを利用して推論した変数L(配列の最後の要素)を利用する。

1type Last<T extends any[]> = T extends [...infer _, infer L] ? L : never; 2type s = Last<[]>; 3// => never 4 5type t = Last<[1, 2, 3]>; 6// => 3

minus operator

公式doc mapped types内のreadonlyや?を削除するための修飾子例えば、type-challengesのMutable の問題では下記のように解ける。

1type Mutable<T> = { 2 -readonly [K in keyof T]: T[K]; 3}; 4 5type s = Mutable<{ readonly test: 1 }>; 6// type s = { 7// test: 1; 8// }

union distribution

conditional typesmapped typesにunion型を渡すと1つずつ処理される。

conditional typesでは条件の左部分が型変数である場合にunion型が分配される。

1type Equal<U> = U extends 1 ? U : never; 2type s = Equal<1 | 2>; 3// type s = 1

例のコードでは型変数Uにunion型が渡っており1だけを返す。つまり下記が行われている。

1type TypeA = (1 extends 1) ? U : never 2type TypeB = (2 extends 1) ? U : never

mapped typesでは以下のような形の場合にunion distributionが発生する。

1{ [P in keyof T]: X}
1type Distribution<T> = { [P in keyof T]: T[P] }; 2type val = Distribution<{ foo: string } | { bar: number }>; 3// type val = Distribution<{ 4// foo: string; 5// }> | Distribution<{ 6// bar: number; 7// }>

never

サバイバルTypeScriptより

TypeScriptのneverは「値を持たない」型。 1️⃣特性1: neverへは何も代入できない 2️⃣特性2: neverは何にでも代入できる 💥常に例外を起こす関数の戻り値に使える

TypeScript Deep Diveより

プログラミング言語の設計には、bottom型の概念があります。 それは、データフロー解析を行うと現れるものです。 TypeScriptはデータフロー解析(😎)を実行するので、決して起こりえないようなものを確実に表現する必要があります。 never型は、このbottom型を表すためにTypeScriptで使用されます。

Wikipedia より

データフロー解析(英: Data-flow analysis)は、プログラム内の様々な位置で、取りうる値の集合に関する情報を収集する技法である。

制御フローグラフ (CFG)を使って変数の値が伝播するかどうかなどの情報を集め、利用する。

このようにして集められた情報はコンパイラが最適化に利用する。

データフロー解析の基本は到達定義 (reaching definition) である。

template literal types

JSのテンプレートリテラルを使って展開した値をそのまま型として定義できる。

1type Join<T extends string, U extends string> = `${T}-${U}`; 2type joinedStr = Join<'foo', 'bar'>; 3// type joinedStr = "foo-bar"

conditional types + inferを利用していこんな風にも書ける。

1type TemplateLiteral<T extends string> = T extends `${infer F}${infer _L}` 2 ? F 3 : never; 4type str = TemplateLiteral<'foo'>; 5// type str = "f"

type-challengesで文字列操作するときに初めて知って表現力に驚いたな。

length property

配列やタプルの要素数をカウントして返す

1type val = [1, 3]['length']; 2// type val = 2

Arrayのinterfaceにlengthプロパティが存在する。

1interface Array<T> { 2 /** 3 * Gets or sets the length of the array. This is a number one higher than the highest index in the array. 4 */ 5 length: number; 6}

https://github.com/microsoft/TypeScript/blob/main/lib/lib.es5.d.ts#L1220-L1402

index (numeric) signature

配列(orタプル)TにおいてT[number]がunion型で返ることから知った 公式doc にも記載ある。

stackoverflow の記事では、Arrayは下記のようなinterfaceになっていて numberを使ってindexをまとめて取得できるよう。

https://github.com/microsoft/TypeScript/blob/main/lib/lib.es5.d.ts#L388-L392 かな?

bottom type

neverは値を持たないということを表す型でありbottom型とも呼ばれる。集合論で表すと空集合。

never型には何も代入できない(値持たないから)が、never型は他の型に代入できる。

他の全ての型のサブタイプ(部分型)となる。

is-aの関係。逆はできない(他の型をnever型に代入)

1let neverVal: never = 1 as never; 2// never 型は bottom type なのでstring型に代入可能 3 4let str: string = neverVal;

ちなみにunkhownは全ての型のスーパータイプなのでどんな型でも代入可能

1let str = 'str'; 2 3// unkhown 型は super type なのでstring型を代入可能 4let neverVal: unknown = str;

TS型の階層性 に詳しく説明がある。

今後知りたいこと

  • type widening
  • homomorphic mapped types
  • union distributionをキャンセルする
  • etc...

まとめ

type-challenges でTSの型プログラミングにどっぷりハマった。

また再帰的に解く問題が多かったので自然と再帰的に考えられたのもとても良かった。

この経験を通じてTSが好きになったのでもっと深掘りたいと思えたのは大きな収穫。