TypeScript備忘録
satisfies
値が指定した型を充足するかチェックする.型注釈でも同じようなことができる.しかしプロパティにユニオン型を含むオブジェクトの場合,型注釈ではunknown
型と判定され型推論できないが,satisfies
だと実際に設定されている値に応じて推論してくれる.
type A = {
t: number | number[];
};
const a: A = {
t: [1]
};
// a.t.map(_ => _); は型推論されずエラー
const b = {
t: [1]
} satisfies A;
b.t.map(_ => _); // 型推論される
as const
オブジェクトリテラルの定義後にas const
をつけることによって,そのオブジェクトの全プロパティはreadonly
となる ("const assertion").これはプロパティを再帰的にreadonly
に設定できる.
なお,各プロパティの値にリテラル地が設定されている場合,そのプロパティはリテラル型になる.
const a = {
a: 1, // ←"1"型
b: {
c: "hoge" // ←"hoge"型
}
} as const;
// a.a, a.b, a.b.cがreadonly
satisfies
と組み合わせて型チェック,型注釈,readonlyにするとより厳格に管理できる.
// string配列型を満たすreadonlyなstringタプルを定義
const s = ["abc", "def", "ghi"] as const satisfies string[];
unknown
型
「型が不明」な型.any
型(なんでも型)と同じく何でも代入できる.しかしそのプロパティやメソッドを呼び出そうとすると,「型が不明」なためエラーが起きる.それらを行うためにはtypeof
やinstanceof
で型チェック,またはas
で型アサーションをする.
never
型
存在しない値を表す型.あるいは決して返らない関数の戻り値の型(C++の[[noreturn]]
の状態).
never
型にnever
以外の何かを代入するとエラーになる.逆にあらゆる方の変数にnever
を代入できる.
Reactの関数コンポーネント (React.FC
) が子コンポーネントを入れ子として保持したくない(セルフクロージングでのみ使用可能にしたい)場合,propsの型定義にchildren?: never
プロパティを設定する.
type Props = {
children?: never
};
const MyComponent: React.FC<Props> = (props) => <div></div>;
// <MyComponent></MyComponent> はエラーになる
<MyComponent />
構造的部分型
TypeScriptは構造的部分型 (Structural Subtyping) の言語であり,型の構造が一致するかどうかで派生型かどうかを判定する.一方C++などでは公称型 (Nominal Subtyping) の言語であり,継承などによって派生型かどうかを判定する.
TypeScriptでは以下のA
型のオブジェクトa
はB
型と部分的に同じ構造を持つため,変数b
に代入が可能.
type A = {
n: number;
s: string;
};
type B = {
n: number;
};
const a: A = {
n: 1,
s: "hoge"
};
const b: B = a;
なお上記の例において,A
型のオブジェクトリテラルをB
型の変数に代入しようとしたときは,余剰プロパティチェックにより,不必要なプロパティが定義されているとしてエラーとなる.
const b: B = {
n: 1,
s: "hoge" // エラー: プロパティ"s"はB型に存在しない
};
as
型アサーションのコンパイラに対してある値の型を推論したものから上書きして教える方法として型アサーション (as 型
)がある.これはキャストではなく,あくまでもコンパイラに「そういう型と思ってくれ」と表明しているだけに過ぎない.
空のオブジェクトに対して型アサーションを行い,一部のプロパティの初期化を遅延させることができる.これはしかし場合によってはプロパティへの値の設定し忘れによるバグが生じる恐れがあるため,濫用は厳禁.
type A = {
n: number;
};
const empty = {} as A;
// empty.n = 0; この代入を忘れると不具合の原因になるかも...
型ガード関数
ユーザー定義の型やunknown
な値に対してtypeof
やinstanceof
による型チェックを行っても,そのあとの型推論が上手く動作しない.そこでコンパイラに値が何の型か推論するための情報を渡すための型ガード関数を定義する.
型ガード関数では関数の戻り値の型定義を変数 is 型
の形式で記述する(PythonのTypeGuard[型]
と同等).また関数の戻り値を真偽値とすることで,関数の引数に与えた変数が戻り値の型定義を満たすかどうかを示す.
const isNumber = (v: unknown): v is number => typeof v === "number";
extends
ジェネリクス型の制約のジェネリクス型に対してあるプロパティを保持してほしいなどの制約を課すときにextends
キーワードを用いる(C#のwhere
と同じ).
type Needed = {
n: number;
};
const func = <T extends Needed>(v: T) => {};
func({ n: 123 });
func({ n: "123" }); // エラー
extends
条件型のジェネリクス型に対し指定した型に割り当てが可能などうかによって,三項演算子のように型を切り替えることができる.
type Conditional<T> = T extends number ? number : string;
type N = Conditional<123>; // 型は"number"
type S = Conditional<{}>; // 型は"string"
交差型
交差型 (Intersection Type) はユニオン型の逆で,二つの型を合成する.
異なるプリミティブ型またはリテラル型の交差型はnever
になる.
オブジェクト型とオブジェクト型の交差型は,それぞれの全オブジェクトのプロパティを合成したものになる.同名同型のプロパティは1つだけ定義される.同名異型(プリミティブ型またはリテラル型同士)のプロパティはnever
となる.
type Never = string & number; // never
type A = {
n: number;
s: string;
};
type B = {
n: number;
m: number;
};
type C = A & B;
/* type C = {
n: number;
s: string;
m: number;
};*/
ここでいう "intersectioin" は集合の積演算のこと.つまりプロパティの型の積演算が行われていると考えるのが良い.
-
string & number
: 共通部分がないためnever
になる. -
string & string
:string
で共通しているためstring
になる. -
string & "StringLiteral"
:"StringLiteral"
はstring
でもあるので,その積集合である"StringLiteral"
になる.
Branded Type
同じstring
型から派生する型でも,それぞれで値を交互に代入することを不可能にしたいときがある.例えばId
とName
はどちらもstring
で表現したいが,意味としては異なる型であるため区別したい.このときTypeScriptは部分構造型の性質を回避するよう,交差型を利用してそれぞれの型を区別させる方法 (Branded Type) がある.never
型のプロパティを持つオブジェクトとの交差型で定義した例を以下に示す.
type Id = string & { Id: never };
type Name = string & { Name: never };
const createId = (id: string): Id => id as Id;
const createName = (name: string): Name => name as Name;
let id = createId("123");
const name = createName("hoge");
const s: string = id; // string として取得可能
// id = "aaa"; エラー
// id = name; エラー
コンパニオンオブジェクトパターン
TypeScriptでは型定義と同名の関数を定義できる.これにより型のファクトリー関数を型名と同名で定義することができ,コンストラクター関数のようにオブジェクトを生成することができる.
クラスを定義するほどでもなく,単にファクトリー関数のみほしいときに有用.
例えばカスタムエラーを定義するときだとこんな感じになる.
type CustomError = Error & {
name: "CustomError";
};
function CustomError(): CustomError {
const error = new Error() as CustomError;
error.name = "CustomError";
return error;
}
// エラーを投げるときはファクトリー関数を呼び出す.
throw CustomError();
Mapped types
リテラルのユニオン型の各リテラル値をプロパティー名とするオブジェクトを作成するにはmapped typesを使用できる.
type Fruit = "apple" | "orange" | "grape";
type Box = { [K in Fruit]: number };
const box: Box = {
apple: 1,
orange: 2,
grape: 3,
};
keyof
Mapped typesの逆で,オブジェクトのプロパティー名からリテラルのユニオン型を作るときはkeyof
型演算子を使う.
type Box = {
apple: number;
orange: number;
grape: number;
};
// type Fruit = "apple" | "orange" | "grape"; と同じ
type Fruit = keyof Box;