こんにちは、株式会社ファミトラでエンジニアをしているおおさわです。
前回の記事では type と interface の違いや使い分けについて整理しました。
今回はその続きとして個人的に使う(使えそう)な型操作をまとめてみます。
前回の記事
ユニオン型
ユニオン型(|)は「どれかひとつ」を表現します。
type State = "open" | "closed" | "archived";
// => State は "open" | "closed" | "archived" のいずれかになる
交差型
交差型(&)は「両方の性質を持つ」を表現します。
type WithId = { id: string };
type WithName = { name: string };
type UserSummary = WithId & WithName;
// => UserSummary は { id: string; name: string } になる
配列
単純に複数の要素を持つコレクションを表すには配列を使います。T[] と Array は同等です。
const nums: number[] = [1, 2, 3];
// => number[] 型(要素は number)
const nums2: Array<number> = [4, 5, 6];
// => Array<number> 型(要素は number)
個人的にはシンプルな型を定義するとき T[] を、中身が複雑なときは Array を使っています。
(そもそも複雑な構造のときはArray じゃないと宣言できないかも)
// 複雑な例(ユニオン型の配列)
const results: Array<string | number> = ["ok", 200];
// => (string | number)[] 型になる
// ネストが深い例
const nested: Array<Map<string, number>> = [new Map([["a", 1]])];
// => Map<string, number>[] 型になる
タプル
順序や長さを固定したデータ構造を表すにはタプルを使います。
const point: [number, number] = [10, 20];
// => [number, number] 型(例: {0: 10, 1: 20} のような構造)
const mixed: [number, string] = [1, "Alice"];
// => [number, string] 型(要素の型が位置ごとに決まる)
readonly
変更を禁止して意図を明確にしたい場合は readonly を付けます。
const colors: readonly string[] = ["red", "green", "blue"];
// => readonly string[] 型(破壊的操作不可)
as const
リテラル型として値を固定する宣言。オブジェクトや配列に使うと型推論が広がらず、そのままリテラルで保持されます。
const ROLES = { admin: "管理者", member: "メンバー" } as const;
// => { readonly admin: "管理者"; readonly member: "メンバー" }
typeof
値から型を取り出す演算子。オブジェクト定数や関数の型をそのまま再利用できます。
const ROLES = { admin: "管理者", member: "メンバー" } as const;
type RolesType = typeof ROLES;
// => { readonly admin: "管理者"; readonly member: "メンバー" }
keyof
オブジェクト型からキーのユニオン型を取り出す演算子。
const ROLES = { admin: "管理者", member: "メンバー" } as const;
type RoleKey = keyof typeof ROLES;
// => "admin" | "member"
オプショナル
foo?: T は「プロパティが無い or あるなら T」を表します。
type Opt = { count?: number };
const a: Opt = {}; // OK(プロパティが無い)
const b: Opt = { count: 1 }; // OK
const c: Opt = { count: undefined }; // OK(undefined も許容される)
インデックスシグネチャ(辞書)
キーが動的なオブジェクトを表現するときに使います。
type Labels = { [key: string]: string };
// => 任意の string キーに対して値は string になる
const userNames: Labels = {
alice: "Alice",
bob: "Bob",
};
// => { alice: "Alice", bob: "Bob" }
const translations: { [lang: string]: string } = {
ja: "こんにちは",
en: "Hello",
};
// => { ja: "こんにちは", en: "Hello" }
数値キーのインデックスシグネチャ
{ [k: number]: V } は数値キーを持つように見えるインデックスシグネチャですが、JavaScript のオブジェクトのキーは実行時には文字列化されます。そのため「数値キーの辞書」を作るときも、実体は文字列キーになります。
type Scores = { [id: number]: number };
const scores: Scores = { 101: 80, 102: 95 };
// ランタイムでは { "101": 80, "102": 95 } に相当
連番アクセスが主目的なら配列(number[] や Array<number>)の方が自然で。
辞書として IDから値 をに変換したい、が目的なら上記のようなインデックスシグネチャを使うほうが良さそうです
条件型
高度な型表現として条件型があり、既存の型をベースにして新たな型情報に加工したりできます。
TypeScript に組み込まれているユーティリティ型も条件型を利用して実装されているものがあり、具体例として Pick は下記のように定義されています。
// 既存のユーティリティ型 Pick の実装例
// T から指定したキー K のみを取り出す
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type User = { id: number; name: string; email: string };
type UserSummary = Pick<User, "id" | "name">;
// => { id: number; name: string } 型になる
判別可能ユニオン(Discriminated Union)
「判別キー」を持たせると分岐時に網羅性チェックが効きます。
type Shape =
| { kind: "circle"; r: number }
| { kind: "rect"; w: number; h: number };
function area(s: Shape) {
switch (s.kind) {
case "circle":
return Math.PI * s.r ** 2;
case "rect":
return s.w * s.h;
default: {
const _exhaustive: never = s;
return _exhaustive;
}
}
}
// => Shape は { kind: "circle"; r: number } | { kind: "rect"; w: number; h: number } になる
ユーザー定義タイプガード
arg is Foo の形で実行時チェックと静的型の絞り込みを結びつけられます。判別可能ユニオンと並んで安全な分岐に有効。
type Shape =
| { kind: "circle"; r: number }
| { kind: "rect"; w: number; h: number };
function isRect(s: Shape): s is { kind: "rect"; w: number; h: number } {
return s.kind === "rect";
}
function area2(s: Shape) {
if (isRect(s)) {
return s.w * s.h; // ここでは rect として扱える
}
return Math.PI * s.r ** 2; // circle として扱える
}
最後に
他にも紹介できるテクニックはあるとは思いますが、ここでは普段僕が活用している & 今後活用できそうなものを中心にまとめてみました。
次回はユーティリティ型をまとめる予定です。