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; この代入を忘れると不具合の原因になるかも...

Effective TypeScriptでは型アノテーションの方が型アサーションより推奨.型アサーションでは型エラーが簡単に踏みつぶされてしまう.
const a: number = {}; // エラー
const b = {} as number; // エラーじゃない!
``

型ガード関数
ユーザー定義の型や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"

ジェネリクスがユニオン型になっていた場合は,ユニオン型のそれぞれの型に対してextendsによる条件型が評価される.

交差型
交差型 (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";
};
const CustomError = {
create: () => {
const error = new Error() as CustomError;
error.name = "CustomError";
return error;
},
};
// エラーを投げるときはファクトリー関数を呼び出す.
throw CustomError.create();

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;

crypto.randomUUID()
配列の要素にIDを振りたいとき,nanoidを使わなくても標準APIで用意されているこの関数を使えば良い.

ちなみにUUIDは32桁だがnanoidは21桁.短くしたいときはnanoidを使ったほうが良さそう.

ユニオン型を使った状態管理
一つのオブジェクトにある状態でしか使わないプロパティがあるのであれば,それはユニオン型として別々のオブジェクトとして表現すべき.
// "data"や"message"がどの"state"の値で必須なのかが分からない
type Bad = {
state: "yes" | "no" | "half";
data?: number;
message?: string;
};
type YesData = {
state: "yes";
data: number;
};
type NoData = {
state: "no";
message: string;
};
type HalfData = {
state: "half";
};
// 全ての型で共通の"state"プロパティの値が決まれば,実体の型も決まる
type Better = YesData | NoData | HalfData;

タプル型
固定長の配列の型.タプル型の長さは固定であり,その要素の型が定義されている.
type Tuple = [number, string, number];
const t1: Tuple = [1, "a", 2]; // ok
const t2: Tuple = [1, 2, 3]; // no
const t3: Tuple = [1, 2]; // no

長さが固定のはずのタプルだが,要素にスプレッド構文を使うと型を指定した可変長のリストを表現できる.
// 最初はnumber,そのあとは0個以上の任意の数のstringが並ぶ可変長のタプル
type Mixed = [number, ...string[]];

関数のある引数でほかの引数の型を固定する
オーバーロードもどき.オーバーロードは型の並びで関数を切り替えるが,関数がとりうる一部の引数の値によってほかの引数の型を決定して,関数を切り替える.
// 第1引数のパスによって,第2引数のペイロードのありなし・型を変えたい
proc("/");
proc("/users", { name: "Tanaka" });
proc("/clubs", { id: 1, name: "Vissel" });
// function proc(path: string, data?: unknown) {} だとアバウトすぎる
// パスとその引数がとりうる型のペアを示す型を宣言
type Args = {
"/": null;
"/users": { name: string };
"/clubs": { id: number; name: string; };
};
// keyofsやextendsによる型制約,レスト構文とタプル型を使って引数を制御する
function proc<T extends keyof Args>(
path: T,
...data: Args[T] extends null ? [] : [Args[T]]
) {}

Template Literal Types
テンプレートリテラル(`
で囲まれた文字列.${}
で変数を埋め込める)の埋め込み変数の位置に型指定を行ったもの.文字列リテラル型の一部の文字が特定の型であるように制限したいときに便利.
// "-united"が末尾につく文字列の型.実質正規表現/.*-united/に一致する文字列.
type UnitedClub = `${string}-united`;
const uniteds: UnitedClub[] = [
"fukushima-united",
"jef-united",
"kochi-united",
"kagoshima-united",
];
// "J"の後に数字が来る文字列の型
type JLeague = `J${number}`;
const leagues: JLeague[] = ["J1", "J2", "J3"];
// "gamba-osaka","crezo-osaka","fc-osaka"のユニオン型
type OsakaClub = `${"gamba" | "crezo" | "fc"}-osaka`;
const osakas: OsakaClub[] = [
"gamba-osaka",
"crezo-osaka",
"fc-osaka",
];