✍️

[TS TIP] 実用的な型定義を理解する 14選(前編)

に公開

最初に

普段TypeScriptを使用して実装している身として、
型定義は非常に重要であることを理解しているつもりです。

しかし、普段の業務では深い型定義の知識がなくても、調べればそれなりに実装できてしまう一方で、
見ただけでパッと理解できていない自分がいることに焦りを感じました。

そこで今回は、TypeScriptの型を基礎から実務で使うであろう機能を厳選し、
記事に残しておこうと思います。

本記事は 前編後編 で分けていこうと思います。

前編では 1〜7 を扱います。

では初めて行きます。

1. 型レベル関数(Type-Level Functions)の基礎を理解する

TypeScript の強みは、
「型が値を入力にして、型を返す」
という仕組みが言語レベルで存在することです。

例えば次のように、型に対する関数を定義できます。

type Nullable<T> = T | null;

これがまさに 型レベル関数。

もっと複雑なパターンも作れます。

type Arrayify<T> = T extends any ? T[] : never;

「型→型」 の変換を考えられるようになると、TypeScript がただの型チェックではなく 型演算の世界だとわかるようになります。

2. Conditional Types の実践

Conditional Types(条件付き型)は TS 使用者としては必須スキルです。

type IsString<T> = T extends string ? true : false;

type A = IsString<string>; // true
type B = IsString<number>; // false

3. Distributive Conditional Types の応用

Conditional Types は union に対して distributive(分配的)に動く性質があります。

type ToArray<T> = T extends any ? T[] : never;

type A = ToArray<string | number>; 
// string[] | number[]

const strArray: A = [
    "aaa",
    "bbb"
]
// 型OK

const numArray: A = [
    1,
    2
]
// 型OK

const mixArray: A = [
    "aaa",
    2
]
// 型NG

これは強力ですが、一つの配列にまとめたい時には暴走します。

分配を止めたい場合

type NotDistribute<T> = [T] extends [any] ? T[] : never;

type B = NotDistribute<string | number>;
// (string | number)[]

const strArray: B = [
    "aaa",
    "bbb"
]
// 型OK

const numArray: B = [
    1,
    2
]
// 型OK

const mixArray: B = [
    "aaa",
    2
]
// 型OK

[] で包むことで分配を抑制できます。

4. infer で型を抽出する(型システム最大の武器)

infer は、Conditional Types 内で使える 「型を推論して取り出す」構文です。

Promise 内の型を抽出

type Awaited<T> = T extends Promise<infer U> ? U : T;

type R = Awaited<Promise<number>>;
// number

関数の戻り値を推論

type Return<T> = T extends (...args: any[]) => infer R ? R : never;

const fn = () => ({ id: 1, name: 'a' });

type R2 = Return<typeof fn>;
// { id: number; name: string }

このテクニックは`Promise`に関しては使われることは滅多にないと思いますが、関数の戻り値 の推論は厳密な型定義が存在せず、関数の型が柔軟で戻り値が変動する場合によく使用されると思います。

5. never を「消す」「残す」を理解する

never は 存在しない を意味する特殊な型です。

しかし実際には、

  • never が union に入ると消える
  • 条件付き型の分配で重要な意味を持つ

など複雑です。

union に混ざると消える

type A = string | never; // string

never をフィルタリングに利用する

type ExcludeString<T> = T extends string ? never : T;

type B = ExcludeString<string | number | boolean>;
// number | boolean

never を利用して不要な型を“除去できます。

6. satisfies 演算子で「型は固定しないが保証はする」

TypeScript 4.9 で追加された神機能が satisfies。
これは、型の絞り込みを保持したまま型チェックを行える 機能です。

詳しくは以下

https://typescriptbook.jp/reference/values-types-variables/satisfies

では、型アノテーションと何が違うのかというと、

  • 型アノテーション:型の固定
  • satisfies:型の制約

と理解するとわかりやすいかと思います。

以下に例を示します。

型を定義

type User = {
    name: string,
    age: number,
    image?: string
}

型アノテーションの場合(型が固定される)

const userA: User = {
    name: "userA",
    age: 22
}

特徴

  • userA という変数に “User” 型を強制的に付ける
  • 推論は一切使われない(ここが重要)
  • User に存在する項目以外は入れられない
  • userA のプロパティの型は User と同じ
    • name: string
    • age: number
    • image?: string

つまり

userA.name; // string
userA.age;  // number
userA.image; // string | undefined

と言うことになります。

メリット

  • 変数の型が 明確に固定される
  • 関数引数や API の戻り値など、仕様としての型が必要な時に必須

satisfiesの場合(制約+推論保持)

const userB = {
    name: "userB",
    age: 23
} satisfies User;

特徴

  • User と同じ構造であることを “チェック” する
  • しかし userB の型そのものは “推論された型が保持される”
  • 不要なオプションプロパティは型に含まれない
    • → image?: string を書かなかったので userB の型には存在しない

userBの推論結果

const userB: {
  name: string;
  age: number;
}

userB.nameの型は?

userB.name; // "userB" | string → string(リテラルは保持される)

userB には存在しない image が型でも存在しない(変換に出てこない)

userB.image; 
// ❌ error - プロパティ "image" は userB の型に存在しない

User には image?: string があるのに、
userB の型には image が存在しない というのが satisfies 最大の特徴です。

TS Playgroundで試したい場合は以下
  1. こちらをコピペしてください。
type User = {
    name: string,
    age: number,
    image?: string
}

const userA: User = {
    name: "userA",
    age: 22
}

console.log(userA.image)

const userB = {
    name: "userB",
    age: 23
} satisfies User

console.log(userB.image)
  1. TS Playgroundの案内先のリンクで貼り付け

https://typescriptbook.jp/how-to-use-typescript-playground

  1. 最終行の console.logsatisfies による推論結果からコンパイルエラーを吐いていることを確認

7. as const を使った Literal Type 化

as const をつけると、
オブジェクトをすべて リテラル型 で固定できます。

const ROLES = ["admin", "user", "guest"] as const;
// const ROLES: readonly ["admin", "user", "guest"]

これにより…

type Role = typeof ROLES[number];
// "admin" | "user" | "guest"

typeof ROLES[number] は、ROLES の要素すべてを取り出すという意味。

オブジェクトに使えば key/値も固定可能です。

const CONFIG = {
  retry: 3,
  mode: "debug",
} as const;

まとめ(前編)

前編では主に 「型を操作する基礎」 にフォーカスしました。

  • 型レベル関数
  • 条件付き型(Conditional Types)
  • 分配型(Distributive Conditional Types)
  • infer による型抽出
  • never の扱い
  • satisfies の活用
  • as const でリテラル化

これらを理解すると、
「型を作る」というより
「型を計算する」
という感覚になってくるかと思います。

後編では、これらの基本を押さえた上での応用的な観点で書いていこうかと思います。
目的としては、「実務で一段上の品質を出せるようになる」です。

今回の記事がお役にたてたら幸いです。

NCDC テックブログ

Discussion