プロを目指す人のための TypeScript 入門
オブジェクトの基本とオブジェクトの型
一部のみ
オブジェクトとは
- 近年はオブジェクトの書き換えは禁忌される傾向にある
readonly
やas const
の積極的な利用を検討
- スプレッド演算子
- ネストしたオブジェクトの参照先は変わらない
- ネストしたオブジェクトも含めて全部複製する標準的な方法は今のところない
オブジェクトの型
- 近年は interface より type を優先して使う傾向あり
インデックスシグネチャ
- 使うな。代わりに Map を使え。
- 「どんな名前のプロパティにもアクセスできる」という特性により型安全を破壊してしまうため
// ブラケットを使った定義
type PriceData = {
[key: string]: number;
};
// Recordを使った定義
type PriceData = Record<string, number>;
変数から型を作る
typeof
を使う- 変数が持つ型を取得する
- 非常に強力な機能
- ただし乱用すべきでない
- Source of truth をどこに置 くか、で利用を判断する
- 定数など、値が根源であるものには最適
- Source of truth をどこに置 くか、で利用を判断する
部分型関係
部分型とは
- B が A の部分型であるということは
- B は A としても使えるということ
- B が A の持っている全ての性質を持っているということ
- B が A を「包含する」の方が直感的かも?
- partial という言葉から受けるイメージとは逆なので注意
- 構造的部分型 structural subtyping
- プロパティを実際に比較して動的に決める
- TypeScript はコレ
- 名前的部分型 nominal subtyping
- 明示的に宣言されたものだけが部分型とみなされる
型引数を持つ型
- ジェネリック型、型関数とも呼ばれる
type User<T> = {
namr: string;
child: T;
};
- 「型引数を持つ型(ジェネリック型)」は:
- 型を作るためのもの
- それ自体は型ではない
<>
を用いて全ての型引数を指定することで、初めて型となる- 構造にのみ言及する
- ある種の抽象化
- 型引数に制約をかけるには
extends
を使う
type User<T extend HasName> = {}
- オプショナルな型引数を設定するには
= 型
を使う
type User<T = SomeType> = {};
配列
- Iterable
- 配列、Map、文字列などは Iterable である
for-of
文などで扱える
- 配列のインデックスアクセスは避けろ
for-of
文などを使え
分割代入
- デフォルト値の設定は undefined にのみ適用される点に注意
const a = { b: null };
const { b = 123 } = a; // bはnullのままになる
その他の組み込みオブジェクト
Map
- ただのオブジェクトよりも連想配列として優れている
- キーとしてオブジェクトを使える
- メソッド
set
get
has
delete
clear
keys
values
entries
Set
- キーだけで値のない Map
- メソッド
add
delete
has
WeakMap, WeakSet
- キーとして使えるのはオブジェクトのみ
- 列挙系のメソッド(
keys
,values
,entries
)がない- Gabage Collection を可能にするため
- Gabage Collection されるオブジェクトをキーにしたいときは使うと良い
プリミティブなのにプロパティがあるように見える件
- 実はプリミティブに対してプロパティーアクセスを行うたびに一時的にオブジェクトが作られている
const str = 'hello';
console.log(str.length); // 5
- 実は
{}
型は中身が本当にオブジェクトであるかを確認しない
type HasLength = { length: number };
const a: HasLength = 'asdf'; // ok
- 真にオブジェクトである値のみを扱いたいときは
object
型を使う
type HasLength = { length: number } & object;
const a: HasLength = 'asdf'; // error
雑学(プロパティアクセス可能かどうか)
value != null
で以下に絞り込める{[key: string]: unknown}
(同義 →Record<string, unknown>
)
- どんなプロパティ名でアクセスしても unknown 型になるの意
- JS の仕様上、null と undefined 以外の値はプロパティアクセスが可能
TypeScript の関数
関数の作り方
- 関数は、関数オブジェクトという値が変数に代入されたものである
- このことはどの方法で関数を作ったとしても共通する
- 関数宣言で作る - function declaration
function myFunc(a: number): number {}
// 返り値がない関数
function myFunc(a: number): void {}
- 関数式で作る - function expression
- hoisting は行われない
- 使う機会はほぼない
const myFunc = function (a: number): number {};
- アロー関数式で作る - arrow function expression
- hoisting は行われない
const myFunc = (a: number): number => {};
- メソッド記法で作る
- 部分型の扱いで問題点があるため、原則使うな。通常の記法を使え。
const obj = {
// メソッド記法
myFunc(a: number): number {},
// 通常の記法
myFunc: (a: number): number => {},
};
可変長引数とスプレッド構文
- この二つは組み合わせで使われることが多い
const sum = (...args) => {
return otherFunc(...args);
};
高階関数
- higher-order function
- コールバック関数を受け取る関数のこと
map
やfilter
などは全て高階関数
関数の型
- 関数型という
- 引数部分はアロー関数と同じ記法が使える
- 引数名の情報はエディタ支援を充実させるために書くもの
type MyFunc = (num: number) => string;
- 返り値の型は推論される。
- ただし、明示的に書くことで意図がコンパイラに正しく伝わるので、よりわかりやすいエラーメッセージを得られる。
- Source of truth をどこにおくかで判断するとよい
引数の型注釈を省略する
- 逆方向の型推論(Contextual Typing)が働く場合
- つまり、式の型が先にわかっている場合
- コールバック関数などでよく見かける
コールシグネチャ
- ほぼ使わ れないので雑学程度に
- 「プロパティを持つ関数」を定義するために使う
type MyFunc = {
isUsed: boolean;
(arg: number): void;
};
関数型の部分型
- 関数の部分型が成立するための 3 条件
- 返り値
- 共変(covariant)の位置にあるので、
- 順方向(自然な方向)の部分型が成立していること
- 引数
- 反変(contravariant)の位置にあるので、
- 逆方向の部分型が成立していること
- 引数の数
- 引数の数が少ない関数型はより引数が多い関数型の部分型となる
- 返り値
ジェネリクス
- 型引数を受け取る関数(ジェネリック関数)を作る機能
- どの書き方をした場合でも、型引数リストが実引数リストの直前に置かれる
// 関数宣言
function repeat<T>(element: T, length: number): T[] {}
// 関数式
const repeat = function <T>(element: T, length: number): T[] {};
// アロー関数式
const repeat = <T>(element: T, length: number): T[] => {};
- 下記の要件を満たすために使われることが多い
- 入力の値はなんでもいい
- 入力の型によって出力の方が決まる
extends
やオプショナル型引数も使える- 使用時に型引数を省略した場合は、実引数の型から推論される
- つまり「好きな値で呼び出せばいい感じに型を推論してくれる」
高度な型
ユニオン型とインターセクション型
ユニオン型
型Tまたは型U
- 「型としてない」と「実際にない(値としてない)」は違うので注意
- 例えば型としてはプロパティがなくても実 際に値は存在することもある
- ユニオン型に共通するプロパティがある場合、そのプロパティの型は、ユニオン構成要素の各プロパティの型を合成したユニオン型になる。
- 関数の場合も、だいたいそんな感じ
インターセクション型
型Tでありかつ型Uでもある値
- 「オブジェクト型を拡張した新しい型を作る」という用途で使われることが多い
type Animal = {
species: string;
age: number;
};
// インターセクション型(Animal型を拡張している)
type Human = Animal & {
name: string;
};
プリミティブ型 & プリミティブ型
のインターセクション型は never 型になる(あり得ないから)- ただし、
Animal & string
などは never 型にならない。説明略。型チェックは効くから気にするな。
- ただし、
オプショナルプロパティ
以下のような型があったとして
type Human = {
// 1
age?: number;
// 2
age: number | undefined;
};
- 許容されるパターン
- 1 の場合
- プロパティが存在しない
- プロパティが number
- プロパティが undefined
- ただし exactOptionalPropertyTypes が有効ならこれはエラーになる
- 2 の場合
- プロパティが number or undefined (プロパティがないことは認められない)
- 1 の場合
- 使い分けは省略を許すかどうかで決めると良い
リテラル型
4種類のリテラル型
- リテラル型とはプリミティブ型をさらに細分化した型のこと
- 4 種類ある
// 文字列のリテラル型
type A = 'foo';
// 数値のリテラル型
type B = 123;
// 真偽値のリテラル型
type C = true;
// BigIntのリテラル型
type D = 3n;
テンプレートリテラル型
- 文字列型の一種
- リテラル型とはやや異なる性質
let myString: `Hello, ${string}`;
// ok
myString = 'Hello World';
// not ok
myString = 'Hello World again';
- contextual typing で使うのが自然
function getHelloStr(): `Hello ${string}` {
// こうしておけば返り値が特定の文字列形式であることを強要できる
}
ユースケース
- 関数のオプション指定な どで、ユニオン型+リテラル型、の組み合わせは頻出
function myFunc(plusOrMinus: 'plus' | 'minus') {}
- 予断だが、オプションによる処理分岐が適しているのは、関数の一部分の処理が違う場合だ。全く違ってくるなら関数を分けろ。
widening
- リテラル型が自動的に対応するプリミティブ型に置き換わること
- 以下の場合に発生する
let
を使った場合- オブジェクトのプロパティの場合
- 防ぐには
- 変数の場合、
const
を使うか明治的にリテラル型を指定しておく - オブジェクトプロパティの場合
- プロパティに
readonly
をつける - オブジェクト全体に
as const
をつける
- プロパティに
- 変数の場合、
型の絞り込み
- 等価演算子を使う
if (v === 'plus') {
}
- conditional value の存在を確認してから使う
type Animal {
name?: string
}
if (animal?.name) {
}
typeof
を使う- 型としての
typeof
とは異なるので注意
- 型としての
if (typeof v === 'undefined') {
}
instanceof
を使う- クラスの場合
in
を使う- プロパティの有無により絞り込みを行う
- タグ付きユニオンを使う
- a.k.a 代数的データ型、alberaic data types、ADT
- 極めて基本的な設計パターンである
- 複数種類のデータが混ぜて扱われる場合に、それぞれを別の型で現したうえで、タグとなるプロパティを型に埋め込んでおく
- それらのユニオン型を作れば「扱われうる全てのデータ」を表す型ができる
- データを使う側はタグを頼りに型の絞り込みを行う
- ランタイムに型情報を取得できない問題を克服するための手法とも言える
- switch 文を使う
- 特にタグ付きユニオンと相性が良い
- ユーザ定義の型ガード関数を使う
- with a type predicate
- 真偽値で結果を返したい場合
- with an assertion signature
- 失敗時にはエラーを投げたい場合
- with a type predicate
keyof 型、lookup 型
lookup 型
T[K]
- T はオブジェクト型、K は文字列リテラル、というパターンが多い
- 型情報を DRY にする意図
- 使いすぎは良くない
- 「T というオブジェクトの K プロパティからとった値を引数として渡してほしい」という意思表示をしたい場合に使うと良い
keyof 型
- オブジェクト型からそのオブジェクトのプロパティ名の型を得る
- 結果は文字列のリテラル型となる
- keyof は非常に奥が深い
- 型から別の型を作れるため、「型レベル計算」の第一歩となるものだから
// 定数的なオブジェクトから型情報を生成することで、
// `値`をデータの源とすることができる
const conversionTable = {
mm: 1,
m: 1e3,
km: 1e6,
};
type Unit = keyof typeof conversionTable; // 'mm' | 'm' | 'km'