Open21

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

型ガード関数

ユーザー定義の型や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

交差型

交差型 (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";
};

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

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

crypto.randomUUID()

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

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

RerrahRerrah

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