26
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

株式会社VISIONARY JAPANAdvent Calendar 2024

Day 24

【TypeScript】初~中級者 12の機能とテクニック【リトアニアのクリスマス・イブ】

Last updated at Posted at 2024-12-23

00. はじめに:なぜTypeScriptとリトアニア?

リトアニアでは、クリスマス・イブの晩餐に12種ほどの料理を用意する習慣があります。
Kūčiosクーチョスと呼ばれるこの伝統的な晩餐には、肉や乳製品はご法度というルールがあります1

クリスマス・イブの晩餐(筆者宅/2023)
クリスマス・イブの晩餐(筆者宅/2023)
もはやたくさんの料理をつくることが目的で、肉等禁止のルールは守っていない

本記事は、クリスマスまでをカウントするアドベントカレンダーの、24日目の記事です。
昨日23日には、 @Ai-Fukiharu さんが「Miro」について語ってくれました:

本記事では、「アドベントカレンダー24日目には特別な味付けを」という思いから、
TypeScriptテクニックという多くの人が読んでくれそうな内容に、
筆者のルーツの1つであるリトアニアの味をつけて新規性を加えるため、

リトアニアの料理や伝統的な要素になぞらえながら、
TypeScriptの12種の機能と、それらに関連するテクニックを解説します。

筆者はフロントエンドエンジニアとしてTypeScriptを2年ほど使用しています。
本記事の対象者はTypeScript初級者~中級者と幅広く想定しています。

聖なる夜に並ぶ料理のように、各章が皆様の学びの糧となりますように。
それでは、伝統を重んじる人々の目に留まらないことを祈りつつ2、ご紹介します。

本記事ではリトアニアの文化や宗教、伝統に触れますが、決してこれらを軽視したり価値判断したりする意図はありません。

01. リテラル型:Kalėdaitis

Kalėdaitisカレダイティスは、Kūčiosクーチョスで欠かせない聖なるウエハースです3
水と小麦粉でつくられ、伝統的な形や模様を持ち、淡白な味を守り続けています4

3種のKalėdaitis
3種のKalėdaitisカレダイティス5
我が家もイブに頂いている

リテラル型(Literal Types)は、「より具体的な型」です。
主に stringnumberboolean 型の特定の値を指定して、
それらのみを受け付けるようにすることができるものです6

たとえば、以下のように文字列変数を let で宣言すると、string 型になります:

let size = "small";
// let size: string

しかし、const で同様に宣言すると、その型はリテラル型となり、
ここでは指定した文字列が型になります:

const size = "small";
// const size: "small"

const で宣言された変数は値を変更できません7
そのため、TypeScriptは変数がその初期値以外の値を取らないと判断し、
初期値そのものを表すリテラル型を割り当てます。

一方、let は再代入が可能なため、将来的に値が変わる可能性を踏まえ、
stringnumber などの、より一般的な型に拡張します。
これは「Widening」と呼ばれるTypeScriptの推論ルールによるものです8

たとえば、関数のパラメータにリテラル型を用いると、許容する値を限定できるため、
想定外の文字列や数値が渡されることをコンパイル段階で防げます。
以下は、Kalėdaitisカレダイティスが厳密な伝統的手順や形状を守って作られるのと同じように、
コード上でも予期せぬ値の混入を防ぐ「型的な安全性」を確保する手法です:

function setSize(size: "small" | "medium" | "large") {
  console.log(`Selected size: ${size}`);
}

setSize("small");       // OK
setSize("medium");      // OK
setSize("extra-large"); // コンパイルエラー:
// Argument of type '"extra-large"' is not assignable to parameter of type '"small" | "medium" | "large"'.

この size は、パイプ記号(|)で繋げて書いた3つのリテラル型の、いずれかの型です。
複数の型のいずれかであることを示す型を、ユニオン型 (Union Types)と呼びます9
size には、型注釈した "small""medium" は代入できますが、
"extra-large" はそうでないため代入できず、コンパイルエラーとなります。

Discriminated Union で Narrowing

リテラル型は、判別可能なユニオン型と組み合わせると、より強力に型を表現できます。
「判別可能なユニオン型(Discriminated Union)」とは、
共通の識別子となるプロパティを持つ複数の型を、ユニオン型で組み合わせたものです10

以下はウェハーの種類 kind を識別子として、それがKalėdaitisカレダイティスかそれ以外かで
その質感 texture が異なるという、判別可能なユニオン型の例です:

type WaferType = 
  | { kind: "kalėdaitis"; texture: "thin" }
  | { kind: "other"; texture: "thick" };

この共通プロパティがリテラル型であるため、型の絞り込み(Narrowing)ができます11

これにより、単なる文字列や数値の制約にとどまらず、
複数の異なる「形」を持つオブジェクト群をひとまとめの型で管理し、
条件分岐に応じて明確に型を切り替えることが可能となります。

上記の判別可能なユニオン型 WaferType を用いて条件分岐する例を示します:

function describeWafer(wafer: WaferType) {
  if (wafer.kind === "kalėdaitis") {
    // waferはここでkind: "kalėdaitis"と判定され、textureは"thin"とわかる
    console.log("This is a traditional kalėdaitis.");
  } else {
    console.log("This is some other wafer.");
  }
}

kind"kalėdaitis" であると判別された箇所では、
wafer{ kind: "kalėdaitis"; texture: "thin" } 型に絞り込まれます。
これにより、wafer.texture へ安全にアクセスでき、
Kalėdaitisカレダイティスは必ず薄い(thin)」ということが、型レベルで保証されます。

このように、リテラル型とユニオン型は組み合わせることで
より複雑なロジックを型安全かつ明確なコードで表現でき、
予期せぬ値や状態が紛れ込むのを防ぐことができます。

02. constアサーション:Kūčiukai

Kūčiosクーチョスで欠かせないのが、小さく固く焼き上げられた伝統菓子「Kūčiukaiクチューカイ」です12
リトアニア政府観光局によれば「クリスマス・イブのクッキー」を意味するそうで、
祖母から頂くKūčiukaiクチューカイは、まるで石のように硬く、
リトアニアから日本への長い旅路を経ても、決して形が崩れることはありません。

2種類のKūčiukai(筆者宅/2023)
2種類のKūčiukai(筆者宅/2023)
上のKūčiukaiが本当に硬く、父の歯を欠けさせた実績がある

TypeScriptの const アサーション(Const Assertion)は
as const という書き方で、この「不変性」と「安定性」を型レベルで表現します。
これを使うと、オブジェクトや配列がリテラル型として推論されるとともに、
要素やプロパティが readonly という固定的な値となり、変更を型レベルで防げます13

as const を使わない場合は、要素やプロパティの型は推論されます:

const sizes = ["small", "medium", "large"];
// const sizes: string[]

sizes[0] = "tiny"; // OK

as const を使う場合は、要素やプロパティがリテラル型かつ readonly になります:

const sizes = ["small", "medium", "large"] as const;
// const sizes: readonly ["small", "medium", "large"]

sizes[0] = "tiny"; // コンパイルエラー:
// Cannot assign to '0' because it is a read-only property.

形が変わらないKūčiukaiクチューカイのように、
最初に定めた要素以外が紛れ込むことはありません。

また、以下の通りオブジェクトでも同様で、
内包するオブジェクトや配列も再帰的に readonly 化されます:

const lithuania = {
  country: "Lithuania",
  sweets: {
    kūčiukai: {
      ingredients: ["wheat flour", "poppy seeds", "water", "yeast"],
      texture: "hard",
      origin: "traditional"
    }
  },
  festival: "Kūčios"
} as const;
// const lithuania: {
//   readonly country: "Lithuania";
//   readonly sweets: {
//     readonly kūčiukai: {
//       readonly ingredients: readonly ["wheat flour", "poppy seeds", "water", "yeast"];
//       readonly texture: "hard";
//       readonly origin: "traditional";
//     };
//   };
//   readonly festival: "Kūčios";
// }

lithuania.sweets.kūčiukai.ingredients[0] = "rice flour";
// エラー:Type 'string' is not assignable to type '"wheat flour"'.

lithuania.sweets.kūčiukai.texture = "soft";
// エラー:Cannot assign to 'texture' because it is a read-only property.

as const を付けたことで、
ingredients 配列の各要素はリテラル型として推論され、
readonly が再帰的に適用されます。
これにより、後から要素を変更しようとしてもコンパイルエラーとなるため、
コード上でも定義した値や構造を揺るぎないものとして扱うことができます。

03. satisfies:Aguonų pienas

Aguonųアグオヌ pienasピエナスは、Kūčiosクーチョスの食卓によく登場する、
ケシの実(Poppy Seeds)から作られる、白くてまろやかな飲み物です14

黒いAguonos(ケシの実)とAguonų pienas
黒いAguonosアグオノス(ケシの実)とAguonųアグオヌ pienasピエナス15

TypeScript 4.9にて、satisfies という演算子が追加されました16
値が指定した型をきちんと満たしているかをコンパイル時に保証するものです。

以下で、satisfies 単体の基本的な活用例を示すために、ドリンクレシピを例に、
satisfies が型の整合性チェックにどう役立つか確認してみましょう:

type DrinkRecipe = {
  name: string;
  ingredients: string[];
};

// OK
const aguonuPienas = {
  name: "Aguonų pienas",
  ingredients: ["poppy seeds", "water"]
} satisfies DrinkRecipe;

// エラー
const errorDrink = {
  name: "Error",
  ingredients: "water", // 期待された文字列リストでないため、以下のエラーとなる:
  // Type 'string' is not assignable to type 'string[]'.
  // The expected type comes from property 'ingredients' which is declared here on type 'DrinkRecipe'
} satisfies DrinkRecipe;

ingredients を文字列で書いたため指定した DrinkRecipe 型を満たさない
errorDrink は、実際にその部分でコンパイルエラーとなっています。

型注釈との違い

satisfies 演算子と型注釈(Type Anotation)は、
どちらも値が型を満たすことを示すものですが、異なる役割と挙動があります。
以下に、その違いが分かりやすい例を提示します:

type Flavor = "vanilla" | "chocolate";
type Dessert = {
  name: string;
  flavor: Flavor;
};

// この関数はflavorがリテラル型"vanilla"であることを要求する
function acceptVanillaOnly(d: { flavor: "vanilla" }) { /* ... */ }

// --- 型注釈の場合 ---
const dessertA: Dessert = {
  name: "Aguonų pienas",
  flavor: "vanilla"
};

// dessertA.flavorはFlavor型("vanilla" | "chocolate")であり、リテラル型"vanilla"ではない
acceptVanillaOnly(dessertA);
// エラー:dessertA.flavorは"vanilla"リテラルではないため、引数に合わない
// Argument of type 'Dessert' is not assignable to parameter of type '{ flavor: "vanilla"; }'.
//   Types of property 'flavor' are incompatible.
//      Type 'Flavor' is not assignable to type '"vanilla"'.
//        Type '"chocolate"' is not assignable to type '"vanilla"'.

// --- satisfiesの場合 ---
const dessertB = {
  name: "Aguonų pienas",
  flavor: "vanilla"
} satisfies Dessert;

// dessertB.flavorは"vanilla"リテラル型として推論され続ける
acceptVanillaOnly(dessertB); 
// OK:dessertB.flavorは厳密に"vanilla"であり、要求を満たす

型注釈は値を特定の型にピッタリと合わせるものです。
satisfies は型適合を確認するだけで、値自体の厳密な型情報は消しません。
そのため、型注釈よりも、細い型安全性と柔軟な推論結果を両立できます。

as const satisfies

Aguonųアグオヌ pienasピエナスは、Kūčiukaiクチューカイを浸して頂くことがあります:

satisfies 演算子も、as const と組み合わせて使うことが少なくありません。
as const satisfies 型 という記法で、
「不変かつ厳密な値の集合」(as const)と「その集合が正しい型規格を満たしているか」(satisfies)を同時に保証できるようになります。
つまり、以下のように各種設定ファイルなどで定数をつくるときに便利です:

export const colors = {
   Kūčiukai: "#ffffff",
   Aguonų: ["#000000", "#ffffff"], // 配列でもOK
   // satisfiesによる型安全性の確認
   Onion: "red",                   // satisfiesにより型を保証されているためエラー:
   // Type '"red"' is not assignable to type '`#${string}`'.
} as const satisfies {
  [key: string]: `#${string}` | (`#${string}`)[];
};

// as constによる値固定化の確認
colors.Aguonų = "#ff0000"; // as constによりradonlyであるためエラー:
// Cannot assign to 'Aguonų' because it is a read-only property.

// 配列が引数なければならない関数
function mapArray(arr: string[]) { return arr.map((v) => v) }

// as constによるwidening阻止の確認
mapArray(colors.Kūčiukai); // Kūčiukaiは配列でないため、当然エラー
mapArray(colors.Aguonų);   // as constによりwideningされないのでOK(型注釈などで実装した場合はエラーになる)

04. ジェネリクス:Silkė su svogūnais

Silkėシルケ su svogūnaisスヴォグーナイスは、Kūčiosクーチョスで欠かせない伝統のニシン料理です。
キリスト教文化の影響で、Kūčiosクーチョスでは肉が避けられるようになり、
代わりに魚が定着した17おかげで、特にこの料理は多くの家庭で愛されています。

Silkė su svogūnais(ニシンの玉ねぎ添え)
Silkėシルケ su svogūnaisスヴォグーナイス(ニシンの玉ねぎ添え)18
黄色いのはじゃがいも

ジェネリクス(Generics)もまた、TypeScriptを語るうえで欠かせない代表的な機能です。
ジェネリクスを用いると、一度定義した型パターンを異なる型に適用することで、
型安全性を保ちつつ柔軟な拡張や再利用が可能となります。

まずは単純な例で、ジェネリクスがどのように型を汎用化できるか見てみましょう:

// ジェネリクスを使った配列作成関数
function createList<T>(item1: T, item2: T): T[] {
    return [item1, item2];
}

// 文字列リストを作成
const stringList = createList("Silkė", "Cepelinai");

// 数値リストを作成
const numberList = createList(2024, 1224);

console.log(stringList); // ["Silkė", "Cepelinai"]
console.log(numberList); // [2024, 1224]

// 複合リストを作成
const list = createList("A", 42); // 以下のエラーになる:
// Argument of type 'number' is not assignable to parameter of type 'string'.(2345)

createList<T><T> がポイントです。
ジェネリック型(汎用型)T は、引数の型から自動的に推論され、
stringnumber など、あらゆる型でリストを作成できる、
ユーティリティ(共通)関数へと柔軟に変化します。
また、完全に異なる型を混ぜるとコンパイルエラーとなることから、
安全性が確保されています。

ジェネリック型名には、慣習的に T などの大文字アルファベットを使用します19

これにより、一度定義したこの createList 関数を、
文字列リスト・数値リスト・その他の型リストなど、
さまざまな場面で再利用できます。

まさに、基本のSilkėシルケ su svogūnaisスヴォグーナイスに様々な副菜・調味料を組み合わせることで
無数のレシピへと展開していくKūčiosクーチョスの食卓のように、
同じ関数が多彩な「味わい」を生み出すのです。

伝統的なSilkė su svogūnaisのレシピ
伝統的なSilkėシルケ su svogūnaisスヴォグーナイスのレシピたち20
1000件以上のレシピが存在するほど、多彩な組み合わせが可能

createList() のようなシンプルな関数は、
プロジェクト全体で共通的な機能(ユーティリティ)として再利用できます。
将来的に新たなデータ型が追加されても、この関数は同じロジックを維持したまま、
新たな「型の味」に対応できるのです。

次に、ジェネリクスを応用した例をいくつかご紹介します。

複数ジェネリクス:広がる組み合わせ

ジェネリクスは1つの型引数にとどまりません。
複数の型引数を組み合わせることで、より高度なデータ操作が可能となります。

たとえば、以下の zip 関数は2つの配列を組み合わせ、要素をペア化します:

function zip<T, U>(listA: T[], listB: U[]): Array<[T, U]> {
    const length = Math.min(listA.length, listB.length);
    const result: Array<[T, U]> = [];
    for (let i = 0; i < length; i++) {
        result.push([listA[i], listB[i]]);
    }
    return result;
}

// 文字列の料理名と数値の年号をペア化
const dishes = ["Silkė su svogūnais", "Cepelinai"];
const years = [2024, 1224];
const paired = zip(dishes, years);

console.log(paired);
// [ [ "Silkė su svogūnais", 2024 ], [ "Cepelinai", 1224 ] ]

ここでは TU の2種類のジェネリック型を用いることで、
異なる型のデータを整然とまとめることができます。
これにより、ユーザーIDとユーザー情報、日付とイベント、キーと値など、
無数の組み合わせに対応可能な汎用ツールとなります。

これは、料理の多種多様な組み合わせを生み出す過程と似ています。
1つの「調理法(関数)」を、さまざまな「型(食材)」で組み合わせることで、
無数のバリエーションを実現できるのです。

interfaceでの応用:更なる拡張性

ジェネリクスは、関数だけでなくクラスやインターフェース、型エイリアスなど、
TypeScriptで定義可能な、あらゆる型定義要素で利用可能です。

たとえば、以下のインターフェース ApiResponse<T> は、
データ部分 data の型を汎用的に扱えます:

interface ApiResponse<T> {
    status: number;
    data: T;
}

function fetchItem<T>(url: string): Promise<ApiResponse<T>> {
    // 実装は省略(HTTPリクエストを送って結果を返す想定)
    return fetch(url)
        .then(res => res.json())
        .then(json => ({ status: 200, data: json as T }));
}

interface Recipe {
    name: string;
    origin: string;
    // ... 他のプロパティ
}

fetchItem<Recipe>("/api/recipe")
    .then(response => {
        console.log(response.data.name); // TがRecipeとして扱える
    });

ApiResponse<T>T の部分を他のデータ型に差し替えられます。
関数と同様、調理法(インターフェース定義)を他の食材(型)に適用できる構造です。

TypeScriptに標準で存在する Array<T>Promise<T> などの型も
ジェネリクスによって汎用化されており21、これら組み込み型の存在は、
ジェネリクスがTypeScript言語全体を支える基礎的な概念であることを示しています。

また、ReactやAngular、NestJSなどのフレームワーク、各種ライブラリも
ジェネリクスを駆使して汎用性の高いAPIを提供しています22

まさにエコシステム全体が、基本の「型料理法」を拡張しているのです。

型制約:質の保証

ジェネリクスは「自由さ」を生みますが、
時に「特定の条件を満たす型だけ許可したい」となることもあります。
そこで登場するのが extends などを用いた型制約です。

たとえば、グローバル展開を視野に入れたメニューや商品情報を考えてみましょう。
リトアニア語の「Silkė su svogūnais」に英語や日本語の翻訳を必ず用意する、
といったルールを型で強制する例を示します:

type RequiredLocales = "lt" | "en" | "ja";

function createLocalizedRecipe<
    T extends { name: string; origin: string },
    U extends Record<RequiredLocales, string>
>(baseItem: T, localizedNames: U): T & { localizedNames: U } {
    return { ...baseItem, localizedNames };
}

// リトアニア発のシルケ料理データ
const silkeBase = { name: "Silkė su svogūnais", origin: "Lithuania" };
// ローカライズ辞書には必ずlt, en, jaが必要
const silkeTranslations = {
    lt: "Silkė su svogūnais",
    en: "Herring with Onions",
    ja: "ニシンの玉ねぎ添え"
} as const;
// 問題なくオブジェクトが生成される
const localizedSilke = createLocalizedRecipe(silkeBase, silkeTranslations);

// 以下はエラー(jaが欠けているため)
const cepelinaiBase = { name: "Cepelinai", origin: "Lithuania" }
const cepelinaiTranslations = {
    lt: "Didžkukuliai",
    en: "Cepelinai"
};
createLocalizedRecipe(cepelinaiBase, cepelinaiTranslations);
// Argument of type '{ lt: string; en: string; }' is not assignable to parameter of type 'Record<RequiredLocales, string>'.
// Property 'ja' is missing in type '{ lt: string; en: string; }' but required in type 'Record<RequiredLocales, string>'.

ここでは、Tnameorigin を必ず持ち、
URequiredLocales 内のすべての言語キーを持つ必要があります。
つまり、欠けた言語があればコンパイルエラーが発生するため、
こうした制約のあるジェネリクスは、品質を型レベルで自動的に保証できます。
将来的に言語が増えても、型制約を変更するだけで、一貫して品質を保証し続けられます。

型制約はこれだけにとどまらず、条件型 ( T extends ... ? ... : ... )や
後述するMapped Types、Utility Typesと組み合わせることで、
さらに高度な抽象化やエラーチェックが可能です。
こうした仕組みは、規模拡大や要件変化に対応するうえで強力な武器となります。

Kūčiosクーチョスにおいて肉や乳製品を避けるルールを守りつつ多様な料理が展開されるように、
型制約はジェネリクスが紡ぐ「型の食卓」に一定の秩序と品質基準を与えるのです。

05. keyof:Koldūnai

Koldūnaiコルドゥーナイはリトアニアで親しまれるダンプリングの一種で23
中に肉やチーズ、キノコなど様々な具が包まれた小さな包み料理です24

Koldūnai su grybais(キノコ入り餃子)
Koldūnaiコルドゥーナイ su grybaisグリーバイス(キノコ入り餃子)25

keyof 型演算子は、オブジェクトという皮の中に何が詰まっているかを
型レベルで、キー一覧としてユニオン型で取り出すものです。

たとえば以下を見てみましょう:

type Dish = {
  name: string;
  ingredients: string[];
  origin: string;
};

type DishKeys = keyof Dish; // "name" | "ingredients" | "origin"

Koldūnaiコルドゥーナイが「肉」「チーズ」「キノコ」などいくつもの具材オプションを持つように、
keyof DishDish 型が持つキー名群リテラルのユニオン型を返します。

プロパティへのアクセス

以下のように、関数の引数に keyof を使うことで、
オブジェクトのプロパティを安全に指定できます:

function getProperty(obj: Dish, key: keyof Dish) {
  return obj[key]; // keyがTに確実に存在することが型で保証される
}

const dish: Dish = {
  name: "Koldūnai",
  ingredients: ["flour", "water", "mushrooms"],
  origin: "Lithuania"
};

// OK
const dishName = getProperty(dish, "name"); 
// "name"キーが確実に存在するため、安全

// NG
const dishColor = getProperty(dish, "color"); // コンパイルエラー:
// Argument of type '"color"' is not assignable to parameter of type 'keyof Dish'.

keyof は、オブジェクト型を操作するTypeScriptにおける基礎的な機能です。
Kūčiosクーチョスが12皿で成り立つように、keyof も他の機能と組み合わせるものです。
以降の章でも keyof を使用しているので、その用法に注目してみてください。

06. typeof:Kibinai

Kibinaiキビナイは、Koldūnaiコルドゥーナイのように具を包むものですが、ダンプリングというよりはパイです26
Kūčiosクーチョスに登場することは珍しいのですが、おいしいのでここで紹介してしまいます27

Kibinai(2018/トラカイ城周辺のレストラン)
Kibinai(2018/トラカイ城周辺のレストラン)
Kibinaiは美味しかったし、トラカイ城は綺麗だったし、最高!

typeof 型演算子は、見た目は keyof 型演算子と似ていますが、機能は全く異なります。
これは、値から型情報を抽出するものです。

JavaScriptにも同名演算子があります。
結果は文字列としてのみ返され、以下のように基本的な型しか返しません:

JavaScriptにおける各種値の typeof 実行結果
console.log(typeof 1991);           // "number"
console.log(typeof "Lietuva");      // "string"
console.log(typeof true);           // "boolean"
console.log(typeof undefined);      // "undefined"
console.log(typeof null);           // "object" (注意! 言語仕様のバグ)
console.log(typeof {});             // "object"
console.log(typeof []);             // "object" (配列もobjectと判定される)
console.log(typeof function () {}); // "function"

値が型より先にあるところが、これまで紹介した機能と異なります。
料理名の変数から、その型がなにか取得して確認しましょう:

const dishName = "Pyragėliai";

// 値dishNameから型を取り出す
type DishNameType = typeof dishName; 
// type DishNameType = "Pyragėliai"

ここでは文字列定数からリテラル型を抽出できています。
このように値から型をつくれるようになると、たとえば以下の場面で応用できます:

  • 定数の型化
  • APIレスポンスにおけるモックデータからの型生成
  • 型の動的生成によるコード簡略化
  • モジュール間の依存解決
  • 型ガード

以下の応用例を見て、その可能性の一端を見てみましょう。

配列からユニオン型の作成

以下のように、1次元配列を、constアサーションを使いWideningを解いた状態で
typepf[number] ルップアップを利用すると、要素をユニオン型にできます:

// 配列の定義
const dishes = ["Kibinai", "Koldūnai", "Kūčiukai"] as const;
// dishes: readonly ["Kibinai", "Koldūnai", "Kūčiukai"]
// as constがないと stirng[] になる点に注意

// 型の取得
type Dish = (typeof dishes)[number];
// Dish = "Kibinai" | "Koldūnai" | "Kūčiukai"

// ルックアップの補足
type Dish0 = (typeof dishes)[0];
// Dish0 = "Kibinai"
type Dish12 = (typeof dishes)[1 | 2];
// Dish12 = "Koldūnai" | "Kūčiukai"

この用法は、以下のような場面で便利です:

  • スタイルやテーマの管理
  • APIレスポンスの配列を型として扱う
  • フォームのバリデーション
  • データベースのカラム

filterによる型フィルター

これまではtypeof の結果を型として受け取っていましたが、
以下の通り、JavaScriptのように文字列として受け取ることも可能です:

const array = ["Kibinai", 1991, "Koldūnai", 1253, "Kūčiukai"];
// array: array: (string | number)[]
const filtered = array.filter((val) => typeof val === "string");
// filtered: string[] = ["Kibinai", "Koldūnai", "Kūčiukai"]

ここでは stringnumber の両方が要素に含まれている配列に対して
.filter() で要素を走査して、typeof val === "string" で文字列のみを抽出することで、
フィルター後配列が文字列配列 string[] になることを確認できています。

値が型を満たすか判別できるものとして、instanceof 演算子もあります。
typeof では値がプリミティブ型(string など)かを判別する際に使用しますが、
instanceof ではオブジェクトがクラスのインスタンスかを判定する際に使用します28

この用法は、以下のような場面で便利です:

  • データの正規化
  • フロントエンドのDOM操作
  • APIレスポンスの型フィルタリング
  • ユニオン型の絞り込み

型ガードへの応用

未知の料理を食べるときは不安になるように、外部から取得したデータは信用なりません。
その値が期待していた型を満たすか実行するまでわからないことがあります。
そこで用いられるのが、typeof などによる型ガード(Type Guard)です:

const input: unknown = "Kibinai";

console.log(input.toUpperCase()) // コンパイルエラー:
// 'input' is of type 'unknown'.

// typeof型ガードの使用
if (typeof input === "string") {
  console.log(input.toUpperCase()); // KIBINAI
}

// ユーザー定義型ガードの使用
function isString(value: unknown): value is string {
  return typeof value === "string";
}
if (isString(input)) {
  console.log(input.toUpperCase()); // KIBINAI
}

型ガードがない状態では inputunknown 型であるために、string の扱いにはならず
toUpperCase() メソッドを使用しようとすると、コンパイルエラーになります。
そこで、typeof により型を確認し、その型が "string" であることを確認し、
if でその条件下における実行であることを明示することで、
TypeScriptは if 下では inputstring であることを判断できるため、
toUpperCase() メソッドを使用しても、エラーになりません。

また、その下部では、is を使った、型述語(Type Predicates)による
「ユーザー定義型ガード(User-Defined Type Guard)」という手法も使っています29
ここでは値の型を確認していますが、主にオブジェクトや配列の、構造の確認に用います。
typeof を使用した型ガードと同様に、エラーなくメソッドを実行できています。

この is によるユーザー定義型ガードは、
型注釈を用いて強引に型推論を変えてしまうものです。

たとえば、以下の通り、明らかに実装では number か確認している関数でも、
型注釈に [引数] is string を加えてもコンパイルエラーにはならず、
TypeScriptは関数が string か確認したものと認識します:

const array = ["Kibinai", 1991, "Koldūnai", 1253, "Kūčiukai"];

function isString(value: unknown): value is string {
  return typeof value === "number"; // 明らかに型注釈と矛盾した実装
}

array.forEach((val) => {
  if (isString(val)) {
    console.log(val, typeof val, "isString", val.toUpperCase());
  } else {
    console.log(val, typeof val, "isNumber", val*2);
  }
})

// output:
// [LOG]: "Kibinai",  "string",  "isNumber",  NaN 
// [ERR]: "Executed JavaScript Failed:" 
// [ERR]: val.toUpperCase is not a function 

実行すると当然、上記のエラーになります。
そのため、is を用いたユーザー定義型ガードを実装する際には、
実装と型定義に矛盾が無いことを必ず確認すべきでしょう。

07. Mapped Types:Silkė su grybais

Mapped Typesは { [P in K]: ... } の構文を使い、
ユニオンを元に新たな型を生成する機能です30

Genericsと同様に強力な機能であるために、非常に多様な場面で使えます。
応用的なコード例を見ることで、その一端に触れてみましょう:

type Writable<T> = { -readonly [K in keyof T]: T[K] };

ここでは、keyof T でGeneric型 T のキーを取得し、それを K として走査しています。
-readonly はプロパティから readonly 修飾子を取り除く記号です。
つまり、読み取り専用だったオブジェクト型を、書き込み可能な型へと変換します。

Mapped Typesでは、以下の通り追加のプロパティを定義できません:

type Dish = {
  [K in string]: string;
  name: string; // コンパイルエラー:
  // A mapped type may not declare properties or methods.
};
Silkė su grybais ir pupelėmis(ニシンのキノコと豆添え)
Silkėシルケ su grybaisグリーバイス irイル pupelėmisプペレミス(ニシンのキノコと豆添え)31

Silkėシルケ su grybaisグリーバイス(ニシンのキノコ添え)では
ベースのニシン料理にキノコを加え、風味や食感を「再構成」しているように、
Mapped Typesは元の型をもとにプロパティを自在に組み直し、
新たな型を生み出す強力な仕組みなのです。

08. Utility Types:Silkė su burokėliai

TypeScriptには、GenericsやMapped Typesを用いて実装された
便利なUtility Types(ユーティリティ型)が数多く用意されています。

Silkė burokėlių patale(ニシンのビーツ乗せ)
Silkėシルケ burokėliųブロケーリウ pataleパタレ(ニシンのビーツ乗せ)32

コードは lib.es5.d.ts を参照しています33

Genericsで柔軟に型を適用し、Mapped Typesで動的に型構造を再構成し、
Utility Typesで多様なシナリオを手軽に表現する――
こうした仕組みは、リトアニア料理が伝統的な素材を基盤に、
玉ねぎ、キノコ、ビーツといったさまざまな要素を組み合わせて、
多彩な風味や食感を生み出す過程と似ています。

PartialとRequired:添えるべきかどうか

/**
 * Make all properties in T optional
 */
type Partial<T> = {
    [P in keyof T]?: T[P];
};

Partial<T>T のすべてのプロパティをオプショナル(?)にします34
このように、Mapped Typesを使うことで、
将来の拡張や不完全なデータへの対応が容易になります。

逆に、すべてのプロパティを必須にする Required も存在します:

/**
 * Make all properties in T required
 */
type Required<T> = {
    [P in keyof T]-?: T[P];
};

これで、T の全プロパティが必須となります。
「必ずビーツを添える」ルールを型レベルで強制できるようなものです。

Readonly:食べちゃダメ!

/**
 * Make all properties in T readonly
 */
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

Readonly<T>T のすべてのプロパティを読み取り専用に変換します。
Mapped Typeで定義されており、副作用のない安全なデータ構造を簡単に表現できます35

RecordとPick:思い通りの組み合わせ

/**
 * Construct a type with a set of properties K of type T
 */
type Record<K extends keyof any, T> = {
    [P in K]: T;
};

Record<K, T> は指定したキーと値型で、新たな「材料の対応表」を作ることができます。
たとえば、Record<string, number> は「食材名:必要量」のような辞書型を表現できます。
リトアニアの食卓で、各家庭が独自のスパイスやハーブ、野菜を組み合わせて、
それぞれの「味の辞書」を紡ぎ出すイメージです。

/**
 * From T, pick a set of properties whose keys are in the union K
 */
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

一方、Pick<T, K> は既存の型 T から必要な「要素」だけを選び出すことで、
たとえば豊富な副菜から特定の食材を抽出し、新たな一皿として提供するようなもの。
ビーツや玉ねぎ、キノコなどの素材から好きなものをピックアップする感覚です。

Omit:引き算の重要性

/**
 * Construct a type with the properties of T except for those in type K.
 */
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

Omit<T, K> は逆に、T から指定したプロパティ K を除外します。
これは、特定の要素を「入れない」ことで、味や風味を調整する行為に似ています。
たとえば、玉ねぎが苦手な人向けに玉ねぎを抜いたSilkėシルケ料理を提供するように、
Omit は不要な要素を型レベルで取り除くことで、要件に合わせて「引き算」します。

09. Index Signature:Mišrainė

GenericsやMapped Types、Utility Typesは、
オブジェクトのプロパティを柔軟に扱う手段を提供しました。
しかし、キー名が事前にわからないオブジェクトもあります。

TypeScriptは、Index Signature(インデックス型)でこの問題に対処します36
たとえば [key: string]: T という書き方で、
「キー名は自由だが、値の型は T」というルールを表します。

ビーツや豆、コハダなどを用いたMišrainė(筆者宅/2021)
ビーツや豆、コハダなどを用いたMišrainėミシュライネ(筆者宅/2021)
今年の社内忘年会では、じゃがいもを使ったものを提供しました

Mišrainėミシュライネに決まった材料はありません。
野菜、豆、穀物、時には卵や魚まで、多様な食材が入りえます。
共通するのは、すべてが細かく刻まれ、混ざり合って一皿を構成することです。

以下は、Index SignatureでMišrainėミシュライネを表した例です。
このサラダは、どんな食材名でも受け入れますが、値は常に同じ型構造を保ちます:

type IngredientCategory = 'vegetable' | 'grain' | 'bean' | 'other';
type IngredientTexture  = 'crunchy' | 'soft' | 'firm';
interface IngredientInfo {
    category: IngredientCategory;
    texture: IngredientTexture;
};

interface Mišrainė {
    [ingredient: string]: IngredientInfo;
}

const salad: Mišrainė = {
    beet: { category: 'vegetable', texture: 'soft' },
    bean: { category: 'bean', texture: 'firm' }
};

// 後から別の材料も追加可能
salad.cucumber = { category: 'vegetable', texture: 'crunchy' };
salad.bread = { category: 'grain', texture: 'firm' };

キー名(食材名)は自由、でも値の型 IngredientInfo は一定。
こうして、未知のキーを扱う柔軟性と、一定の型ルールを両立できます。

Index Signatureでは、Mapped Typesとは異なり、
以下の通り、追加のプロパティを定義することができます:

interface Dish {
    [ingredient: string]: string;
    name: string;
}

Index Signatureは、GenericsやMapped Typesとも異なる方向での柔軟性をもたらします。
多様な材料を受け入れつつ一貫した型構造を維持する、まさにMišrainėミシュライネのような存在です。

他の形による表現

Index Signatureの書き方として [key: string]: T を紹介しましたが、
Utility Typesである Record<K, T> やMapped Typesでも同様に表現できます。
たとえば、以下の3つの型は、同じ意味になります:

type IndexSignature = { [x: string]: number };
type Recorded       = Record<string, number>;
type MappedTypes = { [x in string]: number };
// いずれも { [x: string]: number; } と推論される

実際に、推論された型を見ると、いずれも { [x: string]: number; } の形になっています。
ただ、読みやすさと慣習や、以下のようなパラメータに対する制約があることから、
シンプルに実装するために、[key: string]: T の記法が用いられます:

type Literal = ""
type IndexSignatureError = { [x: Literal]: number }; // コンパイルエラー:
// An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.
type RecordedLieral      = Record<Literal, number>;
type MappedTypesLiteral  = { [x in Literal]: number };

再帰によるネストへの対応

Index Signatureの値に自身を含むことで、ネストできるようになります。
この再帰的な構造は、複雑な階層的データを自然に表現できるようになり、
フロントエンドではCSSセレクタを構造で示せるようになります37

以下は、Mišrainėミシュライネの複雑な構造を、ネストしたIndex Signatureで示したものです:

interface Mišrainė {
  base?: string;
  [ingredient: string]: string | Mišrainė | undefined;
}

const mišrainė: Mišrainė = {
  base: 'じゃがいも',
  'ドレッシング': {
    base: 'マヨネーズ',
    '胡椒': '黒胡椒',
    'ハーブ': 'バジルとオレガノ'
  },
};

neverでプロパティ除外

never を使用することで、指定したプロパティを設けなくさせることができます。
以下では、私の嫌いなトマトをMišrainėミシュライネに絶対に含めないようにしています:

type Mišrainė = {
  // トマトプロパティを除外するために `tomato` を `never` に設定
  tomato?: never;
  [key: string]: any;
};

const mišrainė: Mišrainė = {
  lettuce: "fresh",
  cucumber: "crisp",
  tomato: "ripe", // コンパイルエラー:
  // Type 'string' is not assignable to type 'undefined'.
  // The expected type comes from property 'tomato' which is declared here on type 'Mišrainė'
};

再帰と除外の、更なる応用

再帰と除外を応用することで、以下のような複雑な構造をつくることができます:

// リーフノード: quantityのみを持つ
interface LeafIngredient {
  quantity: string;
};

// ネストノード: 他の具材を持ち、quantityを持たない
type NestedIngredient = {
  [ingredient: string]: Ingredient;
} & { [key in keyof LeafIngredient]? : never };

// 再帰的な型
type Ingredient = LeafIngredient | NestedIngredient;

// トップレベルではquantityを禁止
type TopLevelIngredients = {
  [K in string as K extends keyof LeafIngredient ? never : K]: Ingredient;
};

// 正しいオブジェクト例
const complexRecipe: TopLevelIngredients = {
  vegetables: {
    quantity: "500g"
  },
  marinade: {
    herbs: { quantity: "10g" },
    spices: { quantity: "5g" }
  },
  dressing: {
    base: { quantity: "100ml" },
    extras: {
      lemon: { quantity: "2 slices" },
      honey: { quantity: "1 tbsp" }
    }
  }
};

// エラーとなるオブジェクト例
const ng1: TopLevelIngredients = { 
  quantity: "1g" // コンパイルエラー(ルートにquantityは書けない):
  // Type 'string' is not assignable to type 'Ingredient'.
  // The expected type comes from property 'quantity' which is declared here on type 'TopLevelIngredients'
}

const ng2: TopLevelIngredients = { 
  a: { 
    quantity: "1g", 
    b: {} // コンパイルエラー('quantity' と他のプロパティを同時に持つことはできない):
    // Object literal may only specify known properties, and 'b' does not exist in type 'LeafIngredient'.
    // The expected type comes from property 'a' which is declared here on type 'TopLevelIngredients'
  } 
}

const ng3: TopLevelIngredients = { 
  a: { 
    quantity: {a: ""}, // コンパイルエラー(quantityはstringしか書けない):
    // Type '{ a: string; }' is not assignable to type 'string'.
    // The expected type comes from property 'quantity' which is declared here on type 'Ingredient'
  }
}

この TopLevelIngredients の構造には、以下のルールがあります:

  • 値の型は以下のいずれかである
    • リーフノード(ここでは LeafIngredient = { quantity: string }
    • ネストノード(リーフノードかネストノードの、どちらかを値に持つ)
  • トップレベルにはリーフノードを含まない
  • ネストノードのキー名には、リーフノードのキーを含まない
  • 再帰的にネストを許容する

このような構造は、アプリケーションの設定やテーマ設定など、階層的なオプションを持つ設定ファイルで、特にスタイルシートのテーマ設定において、各セクションにのみ特定のプロパティ(例えば、色やフォントサイズ)を許可する場合で有用です。

10. Conditional Types:Šaltibarščiai

リトアニア料理の代表格の1つが、ピンク色の冷製ビーツスープŠaltibarščiaiシャルティバルシチです。
「冷たいボルシチ」と呼ばれるこの料理は、ビーツ・ケフィア・ディルなどが混ざり合い、
トッピングやビーツの量などの条件によって味のバリエーションが生まれます。
また、ポーランドではChłodnik litewskiと呼ばれ、土地柄でも風味に変化があります。

Šaltibarščiai
Šaltibarščiaiシャルティバルシチェイ38

Conditional Types(条件型)は以下の通り、条件次第で型を選び分けるものです39

type IsDish<T> = T extends Dish ? true : false;

extends を用いて、三項演算子のように ?: を使い、条件分岐しています。
ここでは、Generic型 TDish に割り当て可能かで分岐しており、
可能な場合は true リテラル型を、そうでない場合は false リテラル型を返しています。

Šaltibarščiaiシャルティバルシチが、ビーツを増やせばより鮮やかで甘みある味に、
ディルを控えればさっぱりとした飲み口に、と条件次第で味わいが変化するように
T の「状態・条件」に応じて型が変わるのです。

ユーティリティ型の作成

Conditional Typesは高度なユーティリティ型を実装するのにも向いています。
たとえば never と組み合わせて、以下のように特定のプロパティを除外できます:

type Without<T, U> = T extends U ? never : T;

type Ingredients = "carrots" | "potatoes" | "tomato";

type MyIngredients = Without<Ingredients, "tomato">;
// MyIngredients = "carrots" | "potatoes"

Conditional Typesで never を返すことで、その型をユニオン型から削除できます。
それにより、私の苦手なトマトをŠaltibarščiaiシャルティバルシチの具材から除くことに成功しました。

Union Distribution

Conditional Typesでは、型パラメータがユニオン型である場合に
Union distribution(ユニオンの分配)が発生します。
先ほど例に出した、料理かどうかチェックする IsDish<T> でもそれを確認できます:

type IsDish<T> = T extends Dish ? true : false;

type A = IsDish<Dish>;          // true
type B = IsDish<string>;        // false
type C = IsDish<Dish | string>; // boolean

IsDish<T> は、与えられた型 TDish
割り当て可能なら true リテラルを、
そうでないなら false リテラルを返すものでした。

実際に、割り当て可能な A とそうでない B はその通りに返されています。
しかし、Dish | string をあてた場合には、boolean を返されています。
これは、Dishstring をそれぞれ確認した結果、true | false となり、
それが boolean の定義であるため、この結果となりました。

シェフがŠaltibarščiaiシャルティバルシチェイの各具材を個別に刻んだり混ぜたりするように、
TypeScriptはConditional Typesにおいてユニオン型の要素を個別に評価していくのです。

深い型の再帰的な走査

Šaltibarščiaiシャルティバルシチェイが多様な条件で複雑なバリエーションを産むように、
Conditional Typesも以下のように再帰することでネストの深い複雑な型に対応できます:

type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

// 使用例
interface Dish {
  name: string;
  ingredients: {
    vegetables: string[];
    meats: string[];
  };
}

type PartialDish = DeepPartial<Dish>;
/*
{
  name?: string;
  ingredients?: {
    vegetables?: string[];
    meats?: string[];
  };
}
*/

11. infer:Juoda Ruginė Duona

業務スーパーに行ったら、なんと黒いライ麦パンが売っていました。
リトアニアではJuodaユオダ Ruginėルギネ Duonaドゥオナ(黒いライ麦パン)と言い、
パンの日やパン祭りがあるほどに親しまれています40

業務スーパーのライブレッド
業務スーパーのライブレッド41
冷凍で売っていました

infer 演算子は、Conditional Typesの中で型を推論(Infer)し、一部を抽出するものです。
これにより、複雑な型から特定の部分を取り出して再利用することができます。
例えば、関数の戻り値の型を抽出する例を考えてみます:

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

const bake = () => "黒パンは美味しい";

type Result = MyReturnType<typeof bake>; // Resultはstring型

ここでは、infer を使用しているUtility Types ReturnType を新たに定義し、
関数 bake() の戻り値の型を抽出し、string 型であることを確認しています。
Conditional Types(Šaltibarščiaiシャルティバルシチェイ)に inferJuodaユオダ Ruginėルギネ Duonaドゥオナ)を含むことで、
型システムの味わいが深まり、複雑な型の相互作用がスムーズに調和します。

なお、実際の ReturnType は次のように実装されています:

/**
 * Obtain the return type of a function type
 */
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

配列の要素の型を抽出

infer を使って配列の要素の型を抽出することもできます。
黒パンの材料リストから材料の型を取り出す場合は、以下のようになります:

type ElementType<T> = T extends (infer E)[] ? E : never;

type DuonaIngredients = string[];

type Ingredient = ElementType<DuonaIngredients>; // string

Utility Types「Awaited」の紹介

TypeScript 4.5で新たなUtility Types Awaited が追加されました42
Promise<T> の解決値の型を簡単に抽出できるもので、
これにより、複雑な非同期処理の型管理がより直感的かつ効率的になります。

Awaited のソースは以下です43

/**
 * Recursively unwraps the "awaited type" of a type. Non-promise "thenables" should resolve to `never`. This emulates the behavior of `await`.
 */
type Awaited<T> = T extends null | undefined ? T : // special case for `null | undefined` when not in `--strictNullChecks` mode
    T extends object & { then(onfulfilled: infer F, ...args: infer _): any; } ? // `await` only unwraps object types with a callable `then`. Non-object types are not unwrapped
        F extends ((value: infer V, ...args: infer _) => any) ? // if the argument to `then` is callable, extracts the first argument
            Awaited<V> : // recursively unwrap the value
        never : // the argument to `then` was not callable
    T; // non-object or non-thenable

実装は複雑に見えますが、使用方法は以下の通り簡単で、ネストにも対応できます:

type NestedPromiseType = Promise<Promise<number>>;
type ResolvedNestedType = Awaited<NestedPromiseType>; // number

ここでは、Promise<number> から Awaited を使用して number 型を抽出しています。

非同期関数の戻り値の型を取得する際にも Awaited を活用できます。
以下の例では、非同期関数 fetchDuona の戻り値から解決値の型を抽出しています:

async function fetchDuona(): Promise<{ name: string; ingredients: string[] }> {
  return {
    name: "黒ライ麦パン",
    ingredients: ["ライ麦粉", "", "", "酵母"],
  };
}

type FetchDuonaReturnType = Awaited<ReturnType<typeof fetchDuona>>;
// { name: string; ingredients: string[] }

ReactコンポーネントのProps型抽出

Reactを使用している場合、とある家庭の黒パンの材料を調べるように、
以下の通り、コンポーネントのProps型を抽出することもできます:

import React from 'react';

type ExtractProps<T> = T extends React.ComponentType<infer P> ? P : never;

type DuonaProps = {
  name: string;
  ingredients: string[];
};

const DuonaComponent: React.FC<DuonaProps> = ({ name, ingredients }) => (
  <div>
    <h1>{name}</h1>
    <ul>
      {ingredients.map((ingredient) => (
        <li key={ingredient}>{ingredient}</li>
      ))}
    </ul>
  </div>
);

type Props = ExtractProps<typeof DuonaComponent>; // DuonaPropsと同じ

12. Type Assertion:Cepelinai

型アサーションの説明には、リトアニアの国民食「Cepelinaiツェペリナイ」が最適です44
Cepelinaiツェペリナイはすりおろしたじゃがいもを「成形」して、中に肉を詰めた料理です。
その形状は飛行船「ツェッペリン」に似ており、名前の由来にもなっています。

Cepelinai
Cepelinaiツェペリナイ45
リトアニアに来たら、まずはこれを食べましょう

型アサーション(Type Assertion)は、TypeScriptの型を「成形」する機能です46
ViteReact プロジェクト開始時に生成される src/main.tsx でも使われています47

src/main.tsx
(前略)
reactDom.createRoot(document.getElementById('root') as HTMLElement).render(
    <React.StrictMode>
        <App />
    </React.StrictMode>,
);

ここでは、 document.getElementById()HTMLElement | null を返すため、
そのままでは null の可能性を考慮する必要があります。
そこで、 as HTMLElement により返り値の型を HTMLElement に成形することで、
reactDom.createRoot() に渡す際の型エラーを回避しています48

型アサーションの as は、import / export 宣言における as や、
constアサーション as const における as とは異なります。

Cepelinaiツェペリナイがすりおろしたじゃがいもを練り上げて形をつくるように、
型アサーションはデータの型を意図した形に成形することを説明しました。

しかし、形を整えるだけでは真の完成には至りません。
次に、型アサーションの具体的な応用テクニックを掘り下げるとともに、
Cepelinaiツェペリナイとの共通の注意事項についても詳しく見ていきましょう。

型推論が不完全な場面での補完

Kūčiosクーチョスの厳格なルールの中では、使える食材に制約があります。
この制約のなかで創意工夫を凝らして満足感ある料理を完成させることが重要です。
型アサーションも同じように、型推論が「材料不足」で機能しない場面を補完します。

TypeScriptの型推論は非常に優秀ですが、外部からの入力や、
型情報が不足しているライブラリを扱う際には限界があります。
そのようなときは、型アサーションを適切に使用することで、
以下のように型情報を補完することができます:

const response: unknown = await fetch("https://api.example.com/data");
const data = response as { name: string; age: number }; // 必要な型を明示
console.log(`名前: ${data.name}, 年齢: ${data.age}`);

他にも、DOM要素の取得で null の可能性を除外したい場合に利用できます。
以下の例では、型推論では null の可能性が排除されない状況を、
非nullアサーション演算子 ! で補正し、 nullundefined は無いと主張します:

const element = document.getElementById("root")!;
console.log(element.innerHTML); // null と undefined の可能性を除外

AssemblyScriptでの事例

また、稀な例ですが、最近 AssemblyScript(以下、AS) を触ったので、
そこで型アサーションを使用した話も紹介させてください。

TypeScriptベースの言語であるASでは、TypeScriptと同様の形式で、
拡張子 .tsでプログラムを記述してWebAssemblyを出力することができます49

以下はASで画像をグレースケールにする関数です:

export function grayscale(imageData: Uint8ClampedArray): Uint8ClampedArray {
    const len = imageData.length;
    const result = new Uint8ClampedArray(len); // 8 ビットの符号なし整数値のビュー

    for (let i = 0; i < len; i += 4) {
        const r = imageData[i];
        const g = imageData[i + 1];
        const b = imageData[i + 2];

        const gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b) as u32; // 符号なしに修正

        result[i] = gray;
        result[i + 1] = gray;
        result[i + 2] = gray;
        result[i + 3] = imageData[i + 3];
    }

    return result;
}

型アサーションを使用しているのは gray を宣言するところです。
new Uint8ClampedArray() で生成された result は、符号なし整数値の配列です50
Math.round() の返却型 number はASでは64桁の浮動小数点数 f64 に変換されます51

残念ながら、ASでは satisfies を使えません。
使用しようとすると、次のエラーになります:

satisfies を使おうとした場合のエラー
ERROR TS1012: Unexpected token.
    :
 41 │ ) satisfies u32;
    │   ^
    └─ in assembly/index.ts(41,11)

FAILURE 1 parse error(s)

すりおろしたじゃがいもは、水分を落とさなければ生地にはできません。
水分いっぱいの gray を、乾いた状態を求める result の要素に渡すと、
余計な水分(符号)を含んでいる f64u32 に型変換しようとするため、
コンパイラは「生地が水っぽすぎると」文句を言い、以下のエラーになります:

型アサーションしない場合のエラー
ERROR AS200: Conversion from type 'f64' to 'u32' requires an explicit cast.
    :
 41 │ result[i] = gray;
    │             ~~~~
    └─ in assembly/index.ts(41,21)

ERROR AS200: Conversion from type 'f64' to 'u32' requires an explicit cast.
    :
 42 │ result[i + 1] = gray;
    │                 ~~~~
    └─ in assembly/index.ts(42,25)

ERROR AS200: Conversion from type 'f64' to 'u32' requires an explicit cast.
    :
 43 │ result[i + 2] = gray;
    │                 ~~~~
    └─ in assembly/index.ts(43,25)

FAILURE 3 compile error(s)

そこで、 gray の宣言にて as u32 で水を絞り切ることで、生地(型)が
正しく成形され、コンパイラという厳しいシェフの検査にも合格するわけです。

なお、型アサーションせずに型注釈した場合でも、
同様に明示的なキャストを求められるエラーになりました52

型アサーションせず型注釈した場合のエラー
ERROR AS200: Conversion from type 'f64' to 'u32' requires an explicit cast.
    :
 39 │ const gray: u32 = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
    │                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    └─ in assembly/index.ts(39,27)

FAILURE 1 compile error(s)

型アサーションを使うことが最良のコーディングにつながるとは言いませんが、
ひとまずの問題を解決する手段になることを覚えても良いかもしれません。

注意点

Cepelinaiツェペリナイは、じゃがいもと挽肉のハーモニーが最高で非常においしいのですが、
肉を含むため、Kūčiosクーチョスの厳格な食のルールには適しません。
型アサーションも非常に強力ですが、型安全性を損なう観点から好まれません。
たとえば、以下のような無茶な型アサーションも書けてしまいます:

const raw: unknown = "じゃがいも";
const potato: number = raw as number; // じゃがいもの実態は文字列であり、数字ではない

型アサーションは、あくまでコンパイラに対して「これは大丈夫だ」と主張するだけです。
本当に大丈夫かをチェックしてくれるわけではなく、実行時のエラーの原因になりえます。

以下のダブルアサーションと呼ばれる方法も、同様にコンパイラは通してしまいます:

const potato: number = ("じゃがいも" as unknown) as number;

「水分を絞った」と主張しても、中身は水分たっぷりのじゃがいもです。
もちろん生地の中から具が漏れ出し、エンジニアとしての信頼も失墜します。
中身が不安な場合には、if で切り分けするのも一つの手です:

const raw: unknown = "じゃがいも";

if (typeof raw === "number") {
    const potato: number = raw; // 型チェック後なので安全
    console.log(`安全に数値として使用できます: ${potato}`);
} else {
    console.error("値は数値ではありません");
}

そもそも、TypeScriptの型推論は優秀です。
以下のコードでは、そもそも自動的に potatonumber であると推論されます:

// 型アサーションあり
const potato: number = Math.round(0.299 * 255) as number;

// 型アサーションなし
const potato = Math.round(0.299 * 255); // 自動推論される!

芋料理を食べすぎるとお腹に負担をかけてしまうように、
余計な型アサーションを使うと、不要なエラーを招く可能性があります。
コンパイラという料理の相棒を信じましょう。

型アサーションを使う際は「本当に必要か」「中身が正しいか」を確認しましょう。
TypeScriptは、素直に型を注釈するだけで多くの場合きれいに仕上げてくれます。
もし型アサーションを使う時があるとすれば、それはあくまで最終手段として、
あなたがコンパイラよりもその値や型についてよく理解できているときでしょう。


13. さいごに

最後までお読みいただきありがとうございます。

この12皿が皆さんのTypeScriptメニューに新しい風味を加えられ、
日々の実務に役立つインスピレーションを与えられたなら幸いです。

明日は25日クリスマス、Qiitaアドベントカレンダー2024の最終日です。
@fuchiy さんのDocker環境についての話にも注目しましょう。


99. 脚注

リトアニア文化についての出典は主に母親ですが、客観性に欠けないよう、
なるべくリトアニア語で書かれた、ソースとなるページをご紹介します。
また、TypeScript機能の表現では、なるべくパブリックなドキュメントを参考にしています。

  1. 12という数字は十二使徒か1年の月の数に由来し、料理は卵や動物油も避けるそう:
    KŪČIOS - Kauno tautinės kultūros centras
    Kūčios – Vikipedija
    クリスマス・イブの12品の晩餐 - Wikipedia
    リトアニア便り » クリスマス・イブ「クーチョス」

  2. リトアニアを含めたバルト諸国では、キリスト教が広まる以前に、バルト民族独自の多神教信仰があった:
    Baltic Religion - Lietuvos nacionalinis kultūros centras
    リトアニアの宗教 - Wikipedia
    キリスト教が広まってからしばらくたったあとに、過去の信仰を重んじ、それを復興する「バルト・ネオペイガニズム」と呼ばれる運動があった:
    Baltų religija - Romuva
    バルト・ネオペイガニズム - Wikipedia
    本記事では、Kūčiosクーチョス(リトアニアの冬の晩餐における習慣)が、キリスト教のみにおけるものと扱っているように見えるが、起源はキリスト教が広まる以前にあると考えられている:
    Kas yra Kūčios? | Lakrima laidojimo namai
    Kūčios - viena svarbiausių lietuvių šeimos švenčių
    Lithuanian Kūčios: awe-inspiring rites in anticipation of Christmas
    そのため、伝統を重んじる(ネオ)ペイガンの反感を買うことを恐れて、序文を足早に切り上げている。

  3. Kalėdaitisカレダイティスはイエス・キリストの体を象徴し、Kūčiosクーチョスの晩餐で家族が分かち合う聖なる存在とされている:
    kalėdaitis - Visuotinė lietuvių enciklopedija
    呼び方はKalėdaitisカレダイティスの他にもたくさんあり、我が家ではPlotkelėsプロトキャーレスと呼んでいる。

  4. つくるのはかなり大変みたい:
    Kuklusis kalėdaitis atkeliauja iš vienuolynų | Šiaulių kraštas

  5. 画像は英Wikipediaから拝借:
    Christmas wafer - Wikipedia

  6. リテラル型についての説明は公式ドキュメントを参考にしている:
    TypeScript: Handbook - Literal Types

  7. 公式ドキュメントを参考にした表現とした:
    TypeScript: Documentation - Variable Declaration
    参照する値が不変であるわけではない点に注意。
    たとえば、同じ名前のオブジェクトを再度宣言することはできないが、プロパティの値を変更することはできる。

  8. 実は公式ドキュメントでは「Widening」と呼ばれていないが、公式サイトのプレイグラウンドに記載があるので、公式用語ではないが一般的に使われている語彙であると考えてよさそう:
    TypeScript Playground - Type Widening and Narrowing

  9. ユニオン型の詳しい説明については公式ドキュメントを参照されたい:
    TypeScript: Handbook - Unions and Intersection Types

  10. Discriminated Unionは公式ドキュメントで、その邦訳である判別可能なユニオン型は有用な解説として知られるオープンソースドキュメント「TypeScript Deep Dive」で用いられている:
    TypeScript: Documentation - Narrowing
    判別可能なUnion型 | TypeScript Deep Dive 日本語版

  11. 公式ドキュメントにWideningについては立項されていないが、Narrowingについてはある:
    TypeScript: Documentation - Narrowing
    intypeof による型ガード(Type Guard)のためのNarrowingもある。

  12. Kūčiukaiの発音について、文章では「クチューカイ」と「クーチュカイ」の2通りがよく見られるが、私の耳に従い「クチューカイ」を採用した。

  13. constアサーションはTypeScript 3.4で追加された:
    TypeScript: Documentation - TypeScript 3.4

  14. Aguonųアグオヌ pienasピエナスはハチミツや塩を使って味を調えることもあるそう:
    Aguonų pienas

  15. 画像はリトアニアのニュースサイト「tv3.lt」から拝借:
    Išdavė skaniausią aguonų pieno receptą: užsirašykite Kūčioms | tv3.lt

  16. TypeScript 4.9の新機能については以下を参照されたい:
    Announcing TypeScript 4.9 - TypeScript
    TypeScript: Documentation - TypeScript 4.9

  17. キリスト教普及前は肉料理もOKで、料理の種類も9種類だったとのこと:
    Lietuviškos Kūčių tradicijos: valgiai ir jų simbolika | MENIU
    Žuvis Kūčioms: paprasti receptai tradiciniam Kūčių stalui

  18. 画像はリトアニアの園芸・農業サイトDerlingas.ltから拝借:
    Silkė su svogūnais – šventinio Kūčių stalo karalienė | Derlingas.lt
    ニシンはかつてのリトアニアでは珍味で、調理されることはなかったそう。

  19. TypeScript公式ドキュメントを見ると、最初のジェネリック型名が Type だったところから、後々 T などになっているので、おそらく Type の略称として T を使用していると考えられる:
    TypeScript: Documentation - Generics

  20. 画像はリトアニアの料理レシピサイトLa Maistasから拝借:
    tradicine silke su svogunais | La Maistas
    ほかのレシピを検索すると多くても800とかなので、Silke su svogunaisのレシピは本当に豊富なことがわかる。

  21. たとえばTypeScriptの Array なら Array<T> のようにジェネリクスを使っている:
    TypeScript/src/lib/es5.d.ts at main · microsoft/TypeScript

  22. たとえばReactならGitHubの packages/react/ReactHooks.js を見ると、useStateにジェネリクスを使っていることがわかる:
    react/packages/react/src/ReactHooks.js at main · facebook/react

  23. ダンプリングとは、餃子などの、練った小麦粉などを茹でたり焼いたりしたもののこと:
    ダンプリング - Wikipedia

  24. Koldūnaiはリトアニア料理というわけでもない:
    カルドゥヌイ - Wikipedia

  25. 画像はリトアニアの園芸・農業サイトDerlingas.ltから拝借:
    Koldūnai su grybais - tradicinis Kūčių patiekalas

  26. さらにいうと、生地がバターを練り込むショートクラストペイストリーに近いので、パイというよりはペイストリーというほうが正しそうだが、これ以上は料理の専門家に任せることとする。

  27. リトアニアに来ると、まずCepelinaiなど芋料理を多く食べることになるため、次の日から食べる「!(芋)料理」が非常に美味しく感じる。

  28. TypeScriptにおける instanceof の活用については、Narrowingについての公式ドキュメントが参考になる:
    TypeScript: Documentation - Narrowing

  29. Type PredicatesもUser-Defined Type Guardも公式ドキュメントで使用されている言葉である:
    TypeScript: Documentation - Narrowing(旧ドキュメント)
    TypeScript: Documentation - Advanced Types(新ドキュメント)

  30. 公式ドキュメントの表現を参考にしています:
    TypeScript: Documentation - Mapped Types

  31. 画像はLa Maistasから拝借:
    Silkė su grybais ir pupelėmis - receptas | La Maistas

  32. 画像はLa Maistasから拝借:
    Greita silkė burokėlių patale - receptas | La Maistas

  33. GitHubから確認できます:
    TypeScript/src/lib/es5.d.ts at main · microsoft/TypeScript

  34. オプショナルとは、通常は設定されている必要のあるプロパティを、プロパティを設定するかは任意にすることができる機能である:
    TypeScript: Documentation - Object Types

  35. 副作用のない安全なデータ構造とは、オブジェクトのプロパティを書き換えないことで、予期せぬバグや動作を防ぎ、データフローを明確化する手法のことを指している。

  36. Index Signatureについての表現は公式ドキュメントを参照している:
    TypeScript: Documentation - Object Types

  37. TypeScript Deep Driveによれば、JSライブラリにおけるCSSの共通パターンでも、この再帰が使われている:
    Index signature(インデックス型) | TypeScript Deep Dive 日本語版

  38. La Maistasより拝借:
    Šaltibarščiai - receptas | La Maistas

  39. Conditional Typesの詳細は
    TypeScript: Documentation - Conditional Types

  40. 2月5日がパンの日で、民間伝承でもパンが重要な立ち位置にあるそうです:
    Juoda lietuviška duona išlieka mylimiausia – patarimai, padėsiantys ja mėgautis ilgiau
    パンを逆さまに置くと、家が火事になるとか……:
    Tauragės krašto muziejus » Vasario 5-ąją minima Šv. Agotos, Duonos diena

  41. 画像は業務スーパーから拝借:
    ライブレッド - 商品紹介|プロの品質とプロの価格の業務スーパー

  42. 下記の公式ドキュメントに記載がある:
    TypeScript: Documentation - TypeScript 4.5

  43. ソースは以下を参照:
    TypeScript/src/lib/es5.d.ts at main · microsoft/TypeScript · GitHub

  44. 私が好きなリトアニア料理。芋料理なので食べすぎに注意。

  45. 本当においしい。好きすぎていつも画像を撮り忘れてしまう。画像は以下から拝借:
    ツェペリナイ - Wikipedia

  46. 後述するが、型アサーションは型を書き換えるわけではない。
    あくまでコンパイル時に型を主張するのみで、実行には影響しない。
    型アサーションについての説明は以下を参考にしている:
    型アサーション「as」(type assertion) | TypeScript入門『サバイバルTypeScript』
    型アサーション「Type Assertion(型アサーション) | TypeScript Deep Dive 日本語版

  47. Viteは高速なフロントエンドビルドツールで、はじめてTypeScriptに触る場合にもオススメ:
    Vite | Next Generation Frontend Tooling
    また、 create-react-app で開始した React プロジェクトでも src/index.tsx で同様に as HTMLElement で型アサーションが使用されている。

  48. as 以外に、アングルブラケット構文(Angle-bracket Syntax)でも型アサーションを書けるが、この記法はJSXの書き方と似ているため避けられがちで、 as がよく用いられる。

  49. AssemblyScriptの公式ドキュメントのポータビリティ章に記載がある:
    Using the compiler | The AssemblyScript Book

  50. Uint8ClampedArray は 8 ビットの符号なし整数値のビューで、設定値は0と255その範囲内に制限される:
    TypedArray | The AssemblyScript Book

  51. 具体的には、 Number クラスの静的メンバーとして F32F64 が定義されており、これらはそれぞれ32ビットおよび64ビットの浮動小数点数型を表す:
    Number | The AssemblyScript Book
    ASの型について詳細が気になる場合は以下を参照されたい:
    Types | The AssemblyScript Book

  52. サバイバルTypeScriptによれば、TypeScriptにおける型アサーションはキャストと異なる:
    型アサーション「as」(type assertion) | TypeScript入門『サバイバルTypeScript』
    キャストは実行時に型を変換するものである。
    しかし、型アサーションは実行時に影響せず、コンパイラに型を主張するのみで、型変換しない。

26
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?