TypeScriptの型 入門/初級/演習
npm install uhyoの続きでこれを読みます。
まずはこれ
const と let の型推論の違い知らなかった
JavaScriptにおけるconstは変数に再代入されないことを保証するものですから、aにはずっと'foo'が入っていることが保証され、aの型を'foo'とできます。一方、constではなくletやvarを使って変数を宣言した場合は変数をのちのち書き換えることを意図していると考えられますから、最初に'foo'が入っていたからといって変数の型を'foo'型としてしまっては、他の文字列を代入することができなくて不便になってしまいます。そこで、letやvarで変数が宣言される場合、推論される型はリテラル型ではなく対応するプリミティブ型全体に広げられます
構造的部分型という語彙を得た
一方、TypeScriptでは構造的部分型を採用しているため、次のようなことが可能です。
interface MyObj { foo: string; bar: number; } interface MyObj2 { foo: string; } const a: MyObj = {foo: 'foo', bar: 3}; const b: MyObj2 = a;
MyObj2というのはfooプロパティだけを持つオブジェクトの型ですが、MyObj2型変数にMyObj型の値aを代入することができています。MyObj型の値はstring型のプロパティfooを持っているためMyObj2型の値の要件を満たしていると考えられるからです。ちなみに、一般にこのような場合MyObjはMyObj2の部分型であると言います。
こんな意図ありげな実装があったなんて知らなかった。
変数bに代入しようとしている{foo: 'foo', bar: 3}はfooプロパティがstring型を持つため、先ほどの説明からすれば、barプロパティが余計であるもののMyObj2型の変数に代入できるはずです。しかし、オブジェクトリテラルの場合は余計なプロパティを持つオブジェクトは弾かれてしまうのです。
これは、多くの場合余計なプロパティを持つオブジェクトリテラルを意図的に用いることが少なく、ミスである可能性が高いからでしょう。実際、TypeScriptの型システムを順守している限り、このような余計なプロパティは存在しないものと扱われるためアクセスする手段が無く、無駄です。どうしてもこのような操作をしたい場合はひとつ前の例のように別の変数に入れることになります。一度値を変数に入れるだけで挙動が変わるというのは直感的ではありませんが、TypeScriptでは入口だけ見ていてやるからあとは自己責任でということなのでしょう。
可変長引数使ったこと無いかも
const func = (foo: string, ...bar: number[]) => bar; func('foo'); func('bar', 1, 2, 3); // エラー: Argument of type '"hey"' is not assignable to parameter of type 'number'. func('baz', 'hey', 2, 3);
any型は何でもありな型であり、プログラマの敗北です。
ブレなくて好き
いわゆる多相型に関連するものです。TypeScriptにもジェネリクスがあります。
多相型という語彙を初めて得たのでぐぐってみたらおもしろい記事出てきた
たまにアロー関数でジェネリクス使う書き方忘れちゃうのよね
const f: <T>(obj: T)=> void = func;
可変長タプルおもろいな
また、TypeScriptのタプル型は、可変長のタプル型の宣言が可能です。それはもはやタプルなのかという疑問が残りますが、これは実質的には最初のいくつかの要素の型が特別扱いされたような配列の型となります。
type NumAndStrings = [number, ...string[]]; const a1: NumAndStrings = [3, 'foo', 'bar']; const a2: NumAndStrings = [5]; // エラー: Type 'string' is not assignable to type 'number'. const a3: NumAndStrings = ['foo', 'bar'];
こんなことできるんか
さらに、オプショナルな要素を持つタプル型もあります。これは、[string, number?]のように型に?が付いた要素を持つタプル型です。この場合、2番目の要素はあってもいいし無くてもいいという意味になります。ある場合はnumber型でなければなりません。
type T = [string, number?]; const t1: T = ['foo']; const t2: T = ['foo', 3];
実は、最近(TypeScript 3.0)になってタプル型の面白い使い道が追加されました。それは、タプル型を関数の可変長引数の型を表すのに使えるというものです。
TSの柔軟性すごいな
タプル型と可変長引数とジェネリクス
ここらへんから話難しくなる。
理解の確認のために、手元でサンプルコードを再現できるか確かめる。
function bind<T, U extends any[], R>(
func: (arg: T, ...rest: U) => R,
value: T
): (args: U) => R {
return (args) => func(value, ...args)
}
さて、ジェネリクスなど、ここまで説明してきた要素の多くは型のある言語なら普通にあるものだと思います。しかし、ここで紹介するunion型を持っている言語はそこまで多くないのではないかと思います。
そうなんや。DartにUnionなかった気がするけどそれはDartが貧弱だからだと思ってた。
Reactが書きやすいのはTSが頑張ってくれているおかげでもあるなあ
また、&&や||が短絡実行するという挙動を用いたテクニックもJavaScriptではよく使われますが、これもTypeScriptは適切に型検査してくれます。上の関数funcは次のようにも書くことができます。
これまで見たように、プリミティブ型ならunion型の絞り込みはけっこういい感じに動いてくれます。しかし、やはりオブジェクトに対してもいい感じにunion型を使いたいという需要はあります。そのような場合に推奨されているパターンとして、リテラル型とunion型を組み合わせることでいわゆる代数的データ型(タグ付きunion)を再現する方法があります。
代数的データ型(タグ付きunion)という語彙を得た。ReducerのAction typeね。
never型の使いどこよくわかってない。これはなるほどな例。
function func(): never { throw new Error('Hi'); } const result: never = func();
関数の返り値の型がnever型となるのは、関数が値を返す可能性が無いときです。これは返り値が無いことを表すvoid型とは異なり、そもそも関数が正常に終了して値が返ってくるということがあり得ない場合を表します。上の例では、関数funcは必ずthrowします。ということは、関数の実行は中断され、値を返すことなく関数を脱出します。特に、上の例でfuncの返り値を変数resultに代入していますが、実際にはresultに何かが代入されることはあり得ません。ゆえに、resultにはnever型を付けることができるのです。
多くの場合、bar?: number;よりもbar: number | undefinedを優先して使用することをお勧めします。前者はbarが無い場合に本当に無いのか書き忘れなのか区別ができず、ミスの原因になります。後者の場合は書き忘れを防ぐことができます。
Component propsのinterfaceとかは結構?で定義しちゃうなー
オプショナルなプロパティの挙動は、exactOptionalPropertyTypesコンパイラオプションが有効かどうかによって変わります。デフォルトではこのオプションは無効で、比較的最近 (TypeScript 4.4) 追加されたオプションということもあり、無効にしているプロジェクトの方が多いでしょう。
一方で、exactOptionalPropertyTypesが有効の場合、オプショナルなプロパティにundefinedを入れることができなくなります。
親切な記事だなあ
実は、オブジェクト型の記法で関数型を表現する方法があります。
知らんかった
[...T]とTの違い
細かい解説すごいな
Lookup Types T[K]
これそんな名前ついてたんや
さて、以上の2つと同時に導入されたのがmapped typeと呼ばれる型です。日本語でどう呼べばいいのかはよく分かりません。mapped typeは{[P in K]: T}という構文を持つ型です。ここでPは型変数、KとTは何らかの型です。ただし、Kはstringの部分型である必要があります。例えば、{[P in 'foo' | 'bar']: number}という型が可能です。
「読めるようになりたい」と思ってた文法。in obj
と似たような感じのinだな
type PropNullable<T> = { [P in keyof T]: T[P] | null }
あーだから、任意のオブジェクト型を{ [K in keyof T]: T[K] }
で表現できるので、これをなんやかんやすればいろんな型を表現できるよーてことか
逆に、修飾子を取り除くこともTypeScript2.8から可能になりました。そのためには、?やreadonlyの前に-を付けます。例えば、すべてのプロパティから?を取り除く、いわばPartial<T>の逆のはたらきをするRequired<T>は次のように書けます。
type Required<T> = {[P in keyof T]-?: T[P]};
-?はそういう意味だったのか
自分で書いて理解
function propsStringify<T>(obj: T): { [K in keyof T]: string } {
const result = {} as { [K in keyof T]: string }
for (const key in obj) {
result[key] = String(obj[key])
}
return result
}
自分で書いて理解
function pickFirst<T>(obj: { [K in keyof T]: Array<T[K]> }): {
[K in keyof T]: T[K]
} {
const result = {} as any
for (const key in obj) {
result[key] = obj[key][0]
}
return result
}
DeepReadonly
むずい
type DeepReadonly<T> = T extends any[] ? DeepReadonlyArray<T[number]> : T extends object ? DeepReadonlyObject<T> : T; interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {} type DeepReadonlyObject<T> = { readonly [P in NonFunctionPropertyNames<T>]: DeepReadonly<T[P]>; }; type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T];
type DeepReadonly<T> =
T extends any[] ? DeepReadonlyArray<T[number]> :
T extends object ? DeepReadonlyObject<T> :
T;
Tが配列型→DeepReadonlyArray<T[number]>
Tがobject型→DeepReadonlyObject<T>
Tがプリミティブ型→T
DeepReadonlyArray<T>は、要素の型であるTをDeepReadonly<T>で再帰的に処理し、配列自体の型はReadonlyArray<T>により表現しています。ReadonlyArray<T>というのは標準ライブラリにある型で、各要素がreadonlyになっている配列です。T[number]というのは配列であるTに対してnumber型のプロパティ名でアクセスできるプロパティの型ですから、すなわち配列Tの要素の型ですね。
じっくり読んでもわからん
試す
const a = [1, 2, 3, 4, 5]
type A = DeepReadonly<typeof a>
このときAはDeepReadonlyArray<number>
と推論されている。そりゃそうだ。
これがわかんないのかな。
interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}
DeepReadonlyArray<T>はReadonlyArray<DeepReadonly<T>>の部分型であるのみ。
DeepReadonlyArray<number>はReadonlyArray<DeepReadonly<number>>の部分型
DeepReadonly<number>はそのままnumberなので、これはReadonlyArray<number>、つまり「readonlyなnumberの配列」と同義。
つまりTが配列だとReadonly<T[number]>
で返ってくる
もうちょい実験
const a = [1, 2, 3, 4, 5]
const b = [a, a, a]
type B = DeepReadonly<typeof b>
このときBはDeepReadonlyArray<number[]>
と推論されている。
type C = B[number]
としてみると、CはDeepReadonlyArray<number>
と推論されている。うまく再帰的にReadonly定義されている。
あとはわかるな
再帰が直感的に理解できてない、苦手みたい
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : T;
ReturnType<T>は、Tが関数の型のとき、その返り値の型となります。ポイントは、関数の型の返り値部分にあるinfer Rです。このようにinferキーワードを用いることでconditional typeの条件部分で型変数を導入することができます。導入された型変数は分岐のthen側で利用可能になります。
つまり、このReturnType<T>は、Tが(...args: any[]) => R(の部分型)であるときRに評価されるということです。then側でしか型変数が使えないのは、else側ではTが(... args: any[]) => Rの形をしていないかもしれないことを考えると当然ですね。このことから分かるように、この機能は型に対するパターンマッチと見ることができます。
inferも読めるようになりたかったやつ。infer RのRは新しい変数なのね。Rがあるとして〜っていう表現か。
めっちゃおもろいな、こんなことできるんや
type ExtractHelloedPart<S extends string> = S extends `Hello, ${infer P}!` ? P : unknown; // type T1 = "world" type T1 = ExtractHelloedPart<"Hello, world!">; // type T2 = unknown type T2 = ExtractHelloedPart<"Hell, world!">;
ようやく読み終わった
重厚ですごい、改めてありがとうございます
次はこれ
条件型はどちらか片方を返すはずなのに、まさかの両方とは、これは反則もいいところです。この動作を説明するのがunion distributionなのです。
なんとなく使ってたけど、型の計算に分配法則が働いているのね
もう1つ注意しなければいけない点があり、これが条件型の大変ややこしいところでもあります。それは、今まで説明したようなunion distributionが発生するのは条件部分の型が型変数である場合のみであるという点です。
ちょっとむずいな
None / Some / Option型、例示にぴったりの型だな。何か元ネタがあるのかな。
type IsNever<T> = T extends never ? true : false; // T1はneverになる type T1 = IsNever<never>;
...
これを避ける方法はついさっき説明したばかりなので省略します。
こうかな?
type IsNever<T> = T[] extends never[] ? true : false
探してみたら一応あった
type Foo = { foo: string }; type Bar = { bar: number }; type FooBar = Foo | Bar; // FooBarArrは{}になる type FooBarArr = {[P in keyof FooBar]: Array<FooBar[P]>}; // ↓これがエラーにならない! const val1: FooBarArr = {};
上の例では、keyof FooBarは"foo" & "bar"という型になります。
"foo" | "bar"
じゃないんだな...
type FooBar = Foo & Bar
とするとkeyof FooBar = "foo" | "bar"
となる。この関係面白いな
type Foo = { foo: string; hoge: string }
type Bar = { bar: number; hoge: string }
type FooBar = Foo | Bar
とすると keyof FooBar = 'hoge'
になる
具体的には、[P in keyof T]で型変数Tの型が配列だった場合に、全てのプロパティをマップするのではなく要素の型のみをマップしてくれるのです。ではやってみましょう。
ここらへんの話、TypeScriptがすごいのとuhyoさんの説明がうまいのとでめっちゃおもしろい
Readonly<T>の型変数Tに配列やタプルの型を入れると、readonly配列やreadonlyタプルの型になります。
なるほど
keyof 型という型はかならずstring | number | symbolの部分型になります。keyof anyはキーとしてありえる全ての型となり、やはりstring | number | symbolになります。最初からstring | number | symbolと書いてもよいですが、将来的にキーになり得る型が追加されたときのためかこれをベタ書きするのは避けてkeyof anyとしているようです
なるほど
/**
* Exclude from T those types that are assignable to U
*/
type Exclude<T, U> = T extends U ? never : T;
/**
* Extract from T those types that are assignable to U
*/
type Extract<T, U> = T extends U ? T : never;
こう見るとnever型の表現力すごいな。0を発見したときの感動ってこんな感じだったんかな。
これはなかなかよく使うので、これにOmit<T, K>みたいな名前を付けることも結構あるようです。
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
これいつから本体に追加されたんだろ。ExcludeしたものをPickするってなるほどだな。
T extends (...args: any[]) => anyという条件はTが関数の型でなければいけないということを言っています
なるほど
T extends (...args: unknown[]) => unknown
じゃないのか?
自分で書いて理解
type Param<T extends (...args: any[]) => any> = T extends (
...args: infer P
) => any
? P
: never
type Return<T extends (...args: any[]) => any> = T extends (
...args: any[]
) => infer R
? R
: never
読み終わった
次のステップ
「TypeScriptの型中級」はまだ書いていませんが、その代わりに入門・初級をマスターしたみなさんが次に読むと良い記事をご案内します。TypeScriptの型演習 演習問題ができました。力試しに解いてみましょう。
TypeScriptの型推論詳説 型推論を詳しく理解したい方はこちら。
TypeScriptのreadonlyプロパティを使いこなす readonlyについて深掘りする記事です。
TypeScriptで超型安全なBuilderパターン TypeScriptの型の応用例です。
TypeScriptで配列が指定した要素を全部持つか調べる方法 TypeScriptの型の応用例です。
TypeScriptで最低一つは必須なオプションオブジェクトの型を作る これも応用例です。
TypeScriptで最低n個の要素を持った配列の型を宣言する方法 これまた応用例です。
TypeScriptの型レベル連結リスト活用術:型を変えられるコンテナを作る さらに応用例です。
次からはこれを潰していきますかー。
すごい長いスクラップになりそう。
ひっかかったところをコメントします
3-1 配列からMapを作る
自分の回答
function mapFromArray<T extends object>(
arr: T[],
key: keyof T
): Map<T[keyof T], T> {
const result = new Map()
for (const obj of arr) {
result.set(obj[key], obj)
}
return result
}
uhyoさんの回答例
function mapFromArray<T, K extends keyof T>(arr: T[], key: K): Map<T[K], T> {
const result = new Map();
for (const obj of arr) {
result.set(obj[key], obj);
}
return result;
}
自分の回答だと下記のように推論される
const dataMap: Map<string | number, {
id: number;
name: string;
}>
これに対し、uhyoさんの回答例だとMapのkeyの型がより厳密に推論されている。
const dataMap: Map<number, {
id: number;
name: string;
}>
次元の違う引数2つに対して型引数1つで対処しているところに無理がありそう。印象の話だけど。
あとTはobjectに縛らなくていいのか...よくわからん
3-5 undefinedな引数
回答例
type Func<A, R> = undefined extends A ? (arg?: A) => R : (arg: A) => R;
引数の型の中だけでconditional typeを使ってごにょごにょしてたので解けなかった。
あと変だなーと思いつつA extends undefinedってやってた。なるほど逆にすればよいのか。
4-1. 無い場合はunknown
自分の回答
const getFoo = <S extends object>(
obj: S
): S extends { foo: infer R } ? R : unknown => {
return obj.foo
}
uhyoさんの回答例
function getFoo<T extends object>(
obj: T
): T extends { foo: infer E } ? E : unknown {
return (obj as any).foo;
}
return (obj as any).foo;
が書けなかった。負けていいところがわかってない。
4-3. Unionはいやだ
自分の回答
class EventDischarger<E> {
emit<Ev extends keyof E>(
eventName: Ev,
payload: [Ev] extends [keyof E] ? E[Ev] : never
) {
// 省略
}
}
uhyoさんの回答例
type Spread<Ev, EvOrig, E> = Ev extends keyof E
? EvOrig[] extends Ev[]
? E[Ev]
: never
: never;
class EventDischarger<E> {
emit<Ev extends keyof E>(eventName: Ev, payload: Spread<Ev, Ev, E>) {
// 省略
}
}
union distributionを防げばいいんだな〜というなんとなくのイメージは湧いていたけど、なんとなくの域を出なかった。一旦distributeさせて、させなかった場合と比較することで、単一unionかどうかを判定させている。
なんでSpreadってい名前がついているかよくわからない。unionを分解しているという意味?
4-8. オプショナルなキーだけ抜き出す
回答例
type PickUndefined<Obj> = {
[K in keyof Obj]-?: undefined extends Obj[K] ? K : never
}[keyof Obj];
type MapToNever<Obj> = {
[K in keyof Obj] : never
}
type OptionalKeys<Obj> = PickUndefined<MapToNever<Obj>>
1回MapToNeverを経由するのうますぎる