Open28

TypeScript備忘録

RerrahRerrah

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(_ => _);  // 型推論される
RerrahRerrah

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
RerrahRerrah

satisfiesと組み合わせて型チェック,型注釈,readonlyにするとより厳格に管理できる.

// string配列型を満たすreadonlyなstring配列を定義
const s = ["abc", "def", "ghi"] as const satisfies string[];
RerrahRerrah

unknown

「型が不明」な型.any型(なんでも型)と同じく何でも代入できる.しかしそのプロパティやメソッドを呼び出そうとすると,「型が不明」なためエラーが起きる.それらを行うためにはtypeofinstanceofで型チェック,またはasで型アサーションをする.

RerrahRerrah

never

存在しない値を表す型.あるいは決して返らない関数の戻り値の型(C++の[[noreturn]]の状態).
never型にnever以外の何かを代入するとエラーになる.逆にあらゆる方の変数にneverを代入できる.

RerrahRerrah

Reactの関数コンポーネント (React.FC) が子コンポーネントを入れ子として保持したくない(セルフクロージングでのみ使用可能にしたい)場合,propsの型定義にchildren?: neverプロパティを設定する.

type Props = {
  children?: never
};

const MyComponent: React.FC<Props> = (props) => <div></div>;

// <MyComponent></MyComponent> はエラーになる
<MyComponent />
RerrahRerrah

構造的部分型

TypeScriptは構造的部分型 (Structural Subtyping) の言語であり,型の構造が一致するかどうかで派生型かどうかを判定する.一方C++などでは公称型 (Nominal Subtyping) の言語であり,継承などによって派生型かどうかを判定する.

TypeScriptでは以下のA型のオブジェクトaB型と部分的に同じ構造を持つため,変数bに代入が可能.

type A = {
  n: number;
  s: string;
};

type B = {
  n: number;
};

const a: A = {
  n: 1,
  s: "hoge"
};

const b: B = a;
RerrahRerrah

なお上記の例において,A型のオブジェクトリテラルB型の変数に代入しようとしたときは,余剰プロパティチェックにより,不必要なプロパティが定義されているとしてエラーとなる.

const b: B = {
  n: 1,
  s: "hoge"  // エラー: プロパティ"s"はB型に存在しない
};
RerrahRerrah

型アサーションのas

コンパイラに対してある値の型を推論したものから上書きして教える方法として型アサーション (as 型)がある.これはキャストではなく,あくまでもコンパイラに「そういう型と思ってくれ」と表明しているだけに過ぎない.

空のオブジェクトに対して型アサーションを行い,一部のプロパティの初期化を遅延させることができる.これはしかし場合によってはプロパティへの値の設定し忘れによるバグが生じる恐れがあるため,濫用は厳禁.

type A = {
  n: number;
};

const empty = {} as A;
// empty.n = 0;  この代入を忘れると不具合の原因になるかも...
RerrahRerrah

Effective TypeScriptでは型アノテーションの方が型アサーションより推奨.型アサーションでは型エラーが簡単に踏みつぶされてしまう.

const a: number = {};  // エラー
const b = {} as number;  // エラーじゃない!
``
RerrahRerrah

型ガード関数

ユーザー定義の型やunknownな値に対してtypeofinstanceofによる型チェックを行っても,そのあとの型推論が上手く動作しない.そこでコンパイラに値が何の型か推論するための情報を渡すための型ガード関数を定義する.
型ガード関数では関数の戻り値の型定義を変数 is 型の形式で記述する(PythonのTypeGuard[型]と同等).また関数の戻り値を真偽値とすることで,関数の引数に与えた変数が戻り値の型定義を満たすかどうかを示す.

const isNumber = (v: unknown): v is number => typeof v === "number";
RerrahRerrah

ジェネリクス型の制約のextends

ジェネリクス型に対してあるプロパティを保持してほしいなどの制約を課すときにextendsキーワードを用いる(C#のwhereと同じ).

type Needed = {
  n: number;
};

const func = <T extends Needed>(v: T) => {};

func({ n: 123 });
func({ n: "123" });  // エラー
RerrahRerrah

条件型のextends

ジェネリクス型に対し指定した型に割り当てが可能などうかによって,三項演算子のように型を切り替えることができる.

type Conditional<T> = T extends number ? number : string;

type N = Conditional<123>;  // 型は"number"
type S = Conditional<{}>;    // 型は"string"
RerrahRerrah

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

RerrahRerrah

交差型

交差型 (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;
};*/
RerrahRerrah

ここでいう "intersectioin" は集合の積演算のこと.つまりプロパティの型の積演算が行われていると考えるのが良い.

  • string & number: 共通部分がないためneverになる.
  • string & string: stringで共通しているためstringになる.
  • string & "StringLiteral": "StringLiteral"stringでもあるので,その積集合である"StringLiteral"になる.

https://zenn.dev/luvmini511/articles/008915362779e5

RerrahRerrah

Branded Type

同じstring型から派生する型でも,それぞれで値を交互に代入することを不可能にしたいときがある.例えばIdNameはどちらも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;  エラー
RerrahRerrah

コンパニオンオブジェクトパターン

TypeScriptでは型定義と同名の関数を定義できる.これにより型のファクトリー関数を型名と同名で定義することができ,コンストラクター関数のようにオブジェクトを生成することができる.

クラスを定義するほどでもなく,単にファクトリー関数のみほしいときに有用.

https://typescriptbook.jp/tips/companion-object

RerrahRerrah

例えばカスタムエラーを定義するときだとこんな感じになる.

type CustomError = Error & {
  name: "CustomError";
};

const CustomError = {
  create: () => {
    const error = new Error() as CustomError;
    error.name = "CustomError";
    return error;
  },
};

// エラーを投げるときはファクトリー関数を呼び出す.
throw CustomError.create();
RerrahRerrah

crypto.randomUUID()

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

https://developer.mozilla.org/ja/docs/Web/API/Crypto/randomUUID

RerrahRerrah

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

RerrahRerrah

ユニオン型を使った状態管理

一つのオブジェクトにある状態でしか使わないプロパティがあるのであれば,それはユニオン型として別々のオブジェクトとして表現すべき.

// "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;
RerrahRerrah

タプル型

固定長の配列の型.タプル型の長さは固定であり,その要素の型が定義されている.

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
RerrahRerrah

長さが固定のはずのタプルだが,要素にスプレッド構文を使うと型を指定した可変長のリストを表現できる.

// 最初はnumber,そのあとは0個以上の任意の数のstringが並ぶ可変長のタプル
type Mixed = [number, ...string[]];
RerrahRerrah

関数のある引数でほかの引数の型を固定する

オーバーロードもどき.オーバーロードは型の並びで関数を切り替えるが,関数がとりうる一部の引数の値によってほかの引数の型を決定して,関数を切り替える.

// 第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]]
) {}
RerrahRerrah

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",
];