これは、筆者の「2025振り返り用ひとりアドカレ」記事の一つです。
はじめに
筆者はTypeScriptを実務および個人開発で数年使っています。
しかし、まだまだ知らなかったりするのも多いため、本年お世話になったものをはじめ、理解に詰まったりした内容などを備忘録としてまとめていきます。
keyof typeof <対象要素>
typeof:対象要素の値から型を抽出(値から型情報を取り出す)し、
keyof:そのプロパティ(キー)名の文字列リテラルかつユニオン型を取得する。
- 事例コード
const targetObject ={
en: "Butterfly",
fr: "Papillon",
it: "Farfalla",
es: "Mariposa"
};
type targetObjectKeysType = keyof typeof targetObject;
// type targetObjectKeysType = "en" | "fr" | "it" | "es"
1. typeofで対象要素の 値から型を抽出(値から型情報を取り出す) する
- TypeSciptでは、型レベルの処理となるので
stringとなります- ※TypeScriptの
typeofは値から型を抽出する型演算子で型定義の文脈でのみ使用可能
- ※TypeScriptの
- JavaScriptでは、値レベルの処理となるのでランタイム時に
"string"(文字列値)となります
const targetObject ={
en: "Butterfly",
fr: "Papillon",
it: "Farfalla",
es: "Mariposa"
};
type targetObjectType = typeof targetObject;
/**
type targetObjectType = {
en: string;
fr: string;
it: string;
es: string;
}
*/
2. keyofで(対象要素の) プロパティ(キー)名の文字列リテラルかつユニオン型 を取得
type targetObjectKeysType = "en" | "fr" | "it" | "es";
keyof typeofの対象要素が、プリミティブな文字列や数値、文字列配列などではない場合
各種型が持つプロパティが返ってくる。
しかし、リテラル型の場合は(プロパティがないので)型エラーとなる(※ typeof を通さない場合限定)
/* 文字列型 */
const str = "hello";
type StrKeys = keyof typeof str;
// "length" | "toUpperCase" | "charAt" | ... など
// メソッド名の文字列リテラルかつユニオン型となる
/* 数値型 */
const num = 123;
type NumKeys = keyof typeof num;
// "toFixed" | "toString" | ...
/* 真偽値 */
const flag = true;
type FlagKeys = keyof typeof flag;
// "valueOf" | "toString" | ...
/* イテラブル */
const arr = ["a", "b", "c"];
type ArrKeys = keyof typeof arr;
// "length" | "toString" | "push" | "pop" | ...
Mapped Types
対象要素が持つプロパティ(キー)に即したオブジェクト型を生成する。
※補足:Record<Keys, Type>といったユーティリティ型はこの仕組みを応用して定義されています。
type SystemSupportLanguage = "en" | "fr" | "it" | "es";
type Butterfly = {
[key in SystemSupportLanguage]: string;
};
/**
type Butterfly = {
en: string;
fr: string;
it: string;
es: string;
}
*/
Mapped Typesを使ったユーティリティ型の一つ:Readonly<T>
Readonly<T>は、プロパティを読み取り専用にするreadonlyをそのオブジェクトのすべてのプロパティに適用するというユーティリティ型です。
Mapped Typesの機能を使って、次のように実装されています。
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
-
readonlyで、プロパティ名を編集不可に -
keyofで、ジェネリクス(※ここではオブジェクトを想定)のプロパティ名を文字列リテラルかつユニオン型で取得 -
T[P]はジェネリクスのオブジェクトの各種キーにブラケット記法でアクセスしていて、その値の型を取得している(targetObject[en]の値は文字列なので型レベルでstringとなる)- TypeSciptでは、型レベルのインデックスアクセスとなるので
stringとなる - JavaScriptにおけるブラケット記法では、値レベル(実体のオブジェクトのプロパティ)へのインデックスアクセスとなり、結果として "Butterfly" のような実際の値が取得される
- TypeSciptでは、型レベルのインデックスアクセスとなるので
-
P in keyof T:
ここのinはJavaScript のin 演算子ではなく、Mapped Types固有の記述。対象要素の各キーを取得するシンプルなループ処理の働きをしている。
-
Mapped TypesではIndex Signature(インデックスアクセス)に注意- 症状:プロパティ(キー)に即したオブジェクト型を生成する性質から「存在しないキーにアクセスしてもキーが必ずあるかのように扱われるためランタイムエラーを引き起こす」可能性(プロトタイプチェーン)がある
-
対処:
tsconfig.jsonで、TypeScriptのコンパイラオプションnoUncheckedIndexedAccessを指定(有効化)する
-
tsconfig.json例:
{
"compilerOptions": {
"target": "ES2022", // 出力するJavaScriptのバージョン
"module": "ESNext", // モジュールシステム
"strict": true, // 厳格な型チェックを有効化
"noImplicitAny": true, // 暗黙のany型を禁止
"noUnusedLocals": true, // 未使用ローカル変数を警告
"noUnusedParameters": true, // 未使用パラメータを警告
"noUncheckedIndexedAccess": true, // インデックスアクセスの安全性を型で保証
"forceConsistentCasingInFileNames": true, // ファイル名の大文字小文字を厳密に扱う
"esModuleInterop": true, // CommonJSとの互換性
"skipLibCheck": true, // ライブラリ型定義のチェックをスキップ
"outDir": "./dist", // 出力先ディレクトリ
"rootDir": "./src", // ソースコードのルート
"resolveJsonModule": true, // JSONインポートを許可
"moduleResolution": "node" // Node.js互換のモジュール解決
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}
TypeScriptにおけるextendsの意味と使われ方
TypeScriptのextendsには、文脈によって3つの意味があります。
左辺が右辺の部分型である(T ⊆ U) という考え方は、型制約や条件型の場面で特に重要となる印象です。
| 用途 | 例 | 意味 |
|---|---|---|
| ① クラス継承(値レベル) | class Dog extends Animal {} |
値レベルでの継承。プロトタイプチェーンを形成し、AnimalのメソッドやプロパティをDogが引き継ぐ。 |
| ② 型制約(ジェネリクス) | T extends Animal |
型レベルの制約。TはAnimalの部分型であり、Animalとして扱える(構造的部分型:T ⊆ Animal)。 |
③ 条件型(conditional types) |
T extends U ? X : Y |
条件分岐型。TがUの部分型ならX、そうでなければYを返す。 |
補足
- TypeScriptは
構造的部分型(structural subtyping) を採用
つまり、TがUのメンバー構造を満たしていれば「部分型」として扱われる。 - 「
extends=継承」というよりも、「互換性」や「代入可能性(assignability)」のチェックを意味する場合が多い。
ReturnType
型注釈するには面倒な量のプロパティを持つオブジェクトがある場合に役立つ機能です。
対象オブジェクトに準じた型定義をしてくれるという便利な働きをしてくれます。
以下の記事が大変分かりやすく参考になりました。
記事から拝借
const getInitialUser = () => {
return {
id: 12345,
name: "Guest",
preferences: {
theme: "dark",
notifications: {
email: true,
push: false,
sms: false
},
layout: {
sidebar: "collapsed",
density: "compact"
}
},
lastLoginAt: new Date(),
tags: ["new", "trial", "mobile"]
};
};
上記オブジェクトを型定義した場合
type User = {
id: number;
name: string;
preferences: {
theme: string;
notifications: {
email: boolean;
push: boolean;
sms: boolean;
};
layout: {
sidebar: string;
density: string;
};
};
lastLoginAt: Date;
tags: string[];
};
上記のような型定義を書いていくのが面倒(かつ記述漏れのリスクもあるという場合)な時にReturnTypeを使えばたった一行で型定義できます。
type User = ReturnType<typeof getInitialUser>
さいごに
本記事は、今後も筆者がTypeScriptを使っていて書き残しておきたいもの(書いておかないと忘れてしまいそうなこと)を随時更新していく予定です。
筆者の知識不足もあるかと思いますので、何かお気づきの方はご教授いただけますと嬉しく思います。
ここまで読んでいただき、ありがとうございました。
参照