TypeScript 高度な型を使ってみる
ユニオン型やリテラル型、インデックスアクセス型など高度な型の使い方や、typeof や keyof 型演算子の使い方、型の絞り込みや型アサーション、ユーザー定義型ガード、Mapped Types、Conditional Types、ユーティリティ型などについて。
作成日:2023年11月12日
関連ページ
- TypeScript 環境の構築
- TypeScript 基本型や型注釈
- TypeScript 関数
- TypeScript クラス
- TypeScript モジュール
- TypeScript 非同期処理 Promise と Fetch
ユニオン型
ユニオン型(Union Type)は2つ以上の型から構成される型で、それらの型のいずれかになる可能性を表現します。ユニオン型は、2つ以上の型をパイプ記号 |
で繋げて書きます。
例えば、number 型もしくは string 型を表す場合は、number | string
のように書きます。
以下は変数の型注釈と関数の引数の型注釈にユニオン型を使用する例です。
let value: string | number; // value は文字列または数値を格納することができる value = 'hello'; value = 42; value = true; //コンパイルエラー(文字列または数値ではない) // Type 'boolean' is not assignable to type 'string | number'. // ユニオン型(string 型または number 型)の型エイリアス(型の別名)を定義 type StringOrNumber = string | number; // 引数に文字列または数値を受け取ることができる関数 function printId(id: StringOrNumber) { console.log("Your ID is: " + id); } printId(1); // Your ID is: 1 printId("007"); // Your ID is: 007 printId({ myID: 1234 }); // コンパイルエラー(文字列または数値ではない) // Argument of type '{ myID: number; }' is not assignable to parameter of type 'StringOrNumber'.
パイプ記号|
は型のリストの冒頭に置くこともでき、type 文で型ごとに改行するときに使われます。
type StrNumBool = | string | number | boolean;
重複する型のユニオン型
以下の StrNumBool 型は StrNum型(string | number)と BoolNum型(boolean | number)のユニオン型です。この時、StrNumBool 型は string | number | boolean | number とはならず、重複した number は1つにまとめられて string | number | boolean となります。
type StrNum = string | number; type BoolNum = boolean | number; // 以下は string | number | boolean type StrNumBool = StrNum | BoolNum;
配列型のユニオン型
string または number からなる配列の型は以下のように記述できます。
string | number[]
とすると、string 型または number[] 型を表すことになってしまいます。
type List = (string | number)[]; // または type List = string[] | number[]; const list1: List = ['a','b','c']; const list2: List = [1,2,3];
オブジェクトのユニオン型
ユニオン型を使うと、複数の型のうちのいずれかを受け入れる型を定義することができます。
以下の例では User 型と Admin 型を定義し、それらをユニオン型として使っています。UserOrAdmin 型の変数は、User 型または Admin 型のいずれかを受け入れます。これにより、User オブジェクトや Admin オブジェクトを同じ変数に代入できます。
// User 型 type User = { id: number; username: string; }; // Admin 型 type Admin = { id: number; role: string; }; // ユニオン型(User または Admin) type UserOrAdmin = User | Admin; // User 型のインスタンス const user: User = { id: 1, username: 'John Doe', }; // Admin 型のインスタンス const admin: Admin = { id: 2, role: 'administrator', }; // UserOrAdmin 型(ユニオン型)の変数 let userOrAdmin: UserOrAdmin; // User を代入可能 userOrAdmin = user; console.log(userOrAdmin); // {id: 1, username: 'John Doe'} // Admin を代入可能 userOrAdmin = admin; console.log(userOrAdmin); // {id: 2, role: 'administrator'}
また、ユニオン型を使って、異なる型のいずれかを受け入れる変数やパラメータを定義できます。
以下の例では、UserInfo 型は id と username のプロパティを持つオブジェクトまたは email と displayName のプロパティを持つオブジェクトを表しています。
printUserInfo 関数では、受け取ったオブジェクトの型によって処理を分岐しています。この例では in 演算子を使って、引数のオブジェクトが id プロパティを持っているかどうかで判定しています。
// ユーザー情報が含まれる異なる型のオブジェクトのユニオン型 type UserInfo = { id: number; username: string; } | { email: string; displayName: string; }; // 関数の引数として UserInfo 型を使用 function printUserInfo(user: UserInfo): void { // id と username の場合 if ('id' in user) { console.log(`User ID: ${user.id}, Username: ${user.username}`); } // email と displayName の場合 else { console.log(`Email: ${user.email}, Display Name: ${user.displayName}`); } } // ユーザー情報が含まれるオブジェクト const user1: UserInfo = { id: 1, username: 'john_doe', }; const user2: UserInfo = { email: 'john@example.com', displayName: 'John Doe', }; // 関数呼び出し printUserInfo(user1); //User ID: 1, Username: john_doe printUserInfo(user2); //Email: john@example.com, Display Name: John Doe
ユニオン型の伝播
以下の Person 型は User または Admin 型のユニオン型ですが、User 型の id プロパティは string 型で、Admin 型の id プロパティは number 型です。
この場合、Person 型のインスタンスを引数に受け取る関数 getId() での変数 id の型は string | number のユニオン型になります。このようにユニオン型を構成するオブジェクトに同じ名前のプロパティがある場合、それらのプロパティの型のユニオン型になります。
// User 型 type User = { id: string; // string 型 username: string; }; // Admin 型 type Admin = { id: number; // number 型 role: string; }; // ユニオン型(User または Admin) type Person = User | Admin; function getId(person: Person) { // id は string | number のユニオン型 const id = person.id; return id; }
以下は関数のユニオン型の例です。以下の MyFunc 型の関数は「string を受け取って string を返す関数」と「string を受け取って number を返す関数」のユニオン型です。
この場合、以下の関数 callMyFunc() の第2引数に受け取る MyFunc 型の関数 func() の戻り値は string | number のユニオン型になります。
type MyFunc = | ((str: string) => string) | ((str: string) => number); function callMyFunc(str: string, func: MyFunc) { const result = func(str); console.log(result); } function strFunc(str: string) { return str + '!'; } function numFunc(str: string) { return str.length; } callMyFunc('foo', strFunc); // foo! callMyFunc('foo', numFunc); // 3
ユニオン型と絞り込み
例えば、以下の関数は string | number 型の引数 id を受け取ります。
引数 id は、文字列または数値の可能性があるため、以下のように文字列でのみ使用できるメソッド toUpperCase() を呼び出すとコンパイルエラーになってしまいます。
function printId(id: string | number) { console.log(id.toUpperCase()); // コンパイルエラー /* Property 'toUpperCase' does not exist on type 'string | number'. Property 'toUpperCase' does not exist on type 'number'. */ }
コンパイルエラーにならないようにするには、TypeScript がコードに基づいて値の具体的な型を推測できるように型の絞り込み(Type Narrowing)を行います。
以下は if 文と typeof を使った条件分岐で引数 id が string 型か number 型かを判定して TypeScript が型を推測できるように絞り込む例です。
typeof id === "string"
が true であれば、id は string 型なので、文字列のメソッド toUpperCase() を使うことができます。
function printId(id: number | string) { // typeof を使って型を判定 if (typeof id === "string") { // ここでは id は string 型に絞り込まれている console.log(id.toUpperCase()); } else { // ここでは id は number 型になる console.log(id); } }
インターセクション型
インターセクション型(Intersection Type)を使うと、複数のオブジェクト型を結合して新たな型を作ることができます。インターセクション型は 複数の型を &
で繋いで表現します。
例えば、T & U
は「T 型でありかつ U 型である」型を意味します。
以下は Name 型と Age 型を結合して新たな Person 型を作成する例です。Person 型のオブジェクトは string 型の name プロパティと number 型の age プロパティを持つ必要があります。
type Name = { name: string; }; type Age = { age: number; }; type Person = Name & Age; // インターセクション型 const person: Person = { name: 'Alice', age: 5, };
これは Person 型を以下のように定義したのとほぼ同じ意味になります。
type Person = { name: string; age: number; };
また、以下のように記述してもほぼ同じ意味になります。
type Person = Name & { age: number; }; // または type Person = { name: string } & { age: number };
インターセクション型は、既存のオブジェクト型を結合(拡張)して新たな型を作る際に利用できる便利な機能です。インターセクション型を使うことで、型の再利用が可能になります。
type Name = { name: string; }; type Age = { age: number; }; type Person = Name & Age; type School = { school: string; id: number; } type Student = Person & School; // または type Student = Name & Age & School; const student: Student = { name: 'Joe', age: 19, school: 'LA Academy', id: 7256 }
以下はインターフェースを結合して新たな型を作成する例です。
interface Color { color: string; } interface Circle { radius: number; } // インターフェースを結合して新たな型を作成 type ColorCircle = Color & Circle; function draw(circle: ColorCircle) { console.log(`Color:${circle.color} Radius:${circle.radius}`); } const circle: ColorCircle = { color: "red", radius: 33 }; draw(circle); // Color:red Radius:33
インターフェースは、インターフェースや型エイリアスを継承できるので、ColorCircle 型は以下のように定義することもできます。
interface Color { color: string; } /* または type Color = { color: string; }; */ interface ColorCircle extends Color { radius: number; }
プリミティブ型のインターセクション型
プリミティブ型のインターセクション型を作った場合は、never 型が作成されます。
never 型は属する値がない型で、言い換えると、never 型にはいかなる値も代入できず、never 型の値を作ることはできません。
type Never = string & number; const neverEver: Never = "2"; // コンパイルエラー(never 型にはいかなる値も代入できない) // Type 'string' is not assignable to type 'never'.
以下は同じプロパティ名に異なるプリミティブ型を持つオブジェクトのインターセクション型を作成していますが、プロパティ foo は string & number となるので、never 型になります。
type Conflicting = { foo: number } & { foo: string }; const conflict: Conflicting = { foo: 3 // foo は never 型なので値を代入できず、コンパイルエラー // Type 'number' is not assignable to type 'never'. }
リテラル型
リテラル型を使うと、特定の決まった値のみを持つことができる型を表現できます。
リテラル型はプリミティブ型を細分化した型で、プリミティブ型の特定の値だけを代入可能にし、指定したリテラル以外を許可しない型です。
以下は "hello" という型を HelloString 型として定義しています。
"hello" という型は、"hello" という文字列のみが属するリテラル型で、それ以外の値を代入できません。
そのため、5行目ではコンパイルエラーになっています。
type HelloString = "hello"; // type 型名 = 型; const hello: HelloString = "hello"; const hello2: HelloString = "hello world"; // コンパイルエラー // error TS2322: Type '"hello world"' is not assignable to type '"hello"'.
リテラル型の定義は、type 文を使う必要はなく、以下のように "hello" というリテラル型として変数 hello に型注釈を書いて定義することもできます。
変数 hello は "hello" というリテラル型として定義されているので、"hello" 以外の値を代入できません。
let hello : "hello" = "hello"; hello = "foo"; // コンパイルエラー // error TS2322: Type '"foo"' is not assignable to type '"hello"'.
リテラル型の変数は通常 const を使って定義することが多いかと思います。
const 変数名: リテラル型 = 値;
4種類のリテラル型
リテラル型として表現できるプリミティブ型は以下の4種類になります。
- 文字列型(文字列リテラル型)
- 数値型(数値リテラル型)
- 論理型(真偽値リテラル型)
- BigInt 型(BigInt リテラル型)
const foo: "foo" = "foo"; // 文字列リテラル型 const two: 2 = 2; // 数値リテラル型 const isTrue: true = true; // 真偽値リテラル型 const three: 3n = 3n; // BigInt リテラル型
部分型関係
例えば、文字列リテラル型は文字列型の部分型であるため、文字列リテラル型を持つ値は文字列型として扱うことができます。数値リテラル型や真偽値リテラル型も同様です。
let greeting: 'Hello' = 'Hello';
let hello: string = greeting; // greeting(文字列リテラル型)は文字列型として扱える
greeting = hello; // エラー(文字列型はリテラル型"Hello"に割り当てることはできない)
//error TS2322: Type 'string' is not assignable to type '"Hello"'.
const を使って宣言した変数の型推論
const を使って宣言した変数の場合、型注釈を付けないとリテラル型と推論されます。
以下の場合、const を使って宣言した変数 foo は "foo" 型(リテラル型)と推論されます。let や var を使って宣言した変数は、値が文字列であれば文字列型と推論されます。
const foo = 'foo'; //foo は "foo" 型になる let bar = 'bar'; //bar は string 型(文字列型)になる
VS Code で各変数にマウスオーバーすると、以下のように推論されているのが確認できます。
参考
ユニオン型と組み合わせて使う
リテラル型はユニオン型と組み合わせて使うことが多く、リテラル型とユニオン型を組み合わせることで、特定の値のセットのみを受け入れる型を作成(表現)することができます。
例えば、以下の Size 型は 'small'、'medium'、'large' のいずれかの値のみを受け入れるユニオン型です。
そして setSize 関数は、Size 型の値のいずれかを引数として受け取ります。それ以外の値を渡そうとすると、TypeScript はコンパイルエラーを出してくれます。
type Size = 'small' | 'medium' | 'large'; const setSize = (size: Size) => { console.log(`The size is ${size}`) } setSize('small'); // OK setSize('M'); // // Argument of type '"M"' is not assignable to parameter of type 'Size'.
また、型定義で適切にリテラル型のユニオン型を指定することで、入力の補完候補が表示されるようになり、コーディングの支援になります。
以下は VS Code で関数 setSize() の引数に文字列を入力する際に表示される補完候補の例です。
以下の例では Color 型と Size 型をリテラル型のユニオン型として定義し、それらを組み合わせて Product 型を作成しています。そして、filterProducts 関数を使って商品のリストをフィルタリングしています。
filterProducts 関数は、Product 型の値の配列と Color 型と Size 型の値を引数に受け取り、マッチした Product 型の値の配列を返します。
// 色のリテラル型のユニオン型を定義 type Color = 'red' | 'blue' | 'green'; // サイズのリテラル型のユニオン型を定義 type Size = 'small' | 'medium' | 'large'; // 商品の型を定義(ユニオン型を使用) type Product = { name: string; color: Color; size: Size; }; // 商品の配列を作成 const products: Product[] = [ { name: 'Shirt', color: 'blue', size: 'medium' }, { name: 'Dress', color: 'red', size: 'large' }, { name: 'Shoes', color: 'green', size: 'small' }, ]; // 特定の条件に合致する商品をフィルタリングする関数 function filterProducts(products: Product[], color: Color, size: Size): Product[] { return products.filter(product => product.color === color && product.size === size); } // フィルタリングの例 const filteredProducts = filterProducts(products, 'blue', 'medium'); console.log(filteredProducts); //{ name: 'Shirt', color: 'blue', size: 'medium' }
Product 型のオブジェクトでは color プロパティには Color 型で定義した 'red' | 'blue' | 'green' のいずれかの値を、 size プロパティには Size 型で定義した 'small' | 'medium' | 'large' のいずれかの値を指定する必要があり、それ以外の値を指定するとコンパイルエラーになります。
また、filterProducts 関数を呼び出す際には、第2引数には Color 型のいずれかの値を、第3引数には Size 型のいずれかの値を渡す必要があります。
このように、ユニオン型とリテラル型を組み合わせることで、特定の値の組み合わせに制限をかけたり、特定の条件に合致するデータを簡単に扱えるようになります。
テンプレートリテラル型
テンプレートリテラル型は、テンプレート文字列(バッククオート ` で囲まれた文字列)を使用して新しい文字列の型を作成するための機能です
以下の Greeting 型は「Hello, 何かしらの文字列!」という形を持つことが期待されます。
${string}
の部分がテンプレートパラメータ(動的な値を受け入れる部分)で、これにより動的な部分が挿入されます。${}
の中には型が入ります。
type Greeting = `Hello, ${string}!`; const hello1: Greeting = 'Hello, world!'; // OK const hello2: Greeting = 'Hello, Foo!'; // OK const hello3: Greeting = 'Hello, Bar'; // コンパイルエラー(! がないので代入できない) // Type '"Hello, Bar"' is not assignable to type '`Hello, ${string}!`'.
テンプレートリテラル型を使うことで、文字列が特定の形であることを型としてチェックできます。
また、テンプレートリテラル型は、文字列だけでなく、他の型も組み合わせて利用することができます。
以下の RGBColor 型は red(255, 0, 0) や green(0, 128, 0) といった形式の文字列を表します。${Color}
は動的な Color 型(リテラル型のユニオン型)の値を、${number}
は動的な数値を受け入れる部分です。
type Color = 'red' | 'green' | 'blue'; type RGBColor = `${Color}(${number}, ${number}, ${number})`; const myColor1: RGBColor = 'red(255, 0, 0)'; const myColor2: RGBColor = 'green(0, 128, 0)';
以下のテンプレートリテラル型の GreetingToPerson はジェネリック型を使用して文字列リテラル型をパラメータ ${T}
として受け取り、${T} ${string}!
のような構文を使って、動的な文字列型を生成します。
この場合、Greeting 型に基づいて新しい型 GreetingToPerson が構築(生成)されます。
// 文字列リテラル型(ユニオン型) type Greeting = "Hello," | "Hi," | "Hey,"; // テンプレートリテラル型 type GreetingToPerson<T extends string> = `${T} ${string}!`; // 使用例 const greetingToFoo: GreetingToPerson<Greeting> = "Hello, Foo!"; const greetingToBar: GreetingToPerson<Greeting> = "Hi, Bar!";
上記の<T extends string>
は、テンプレートリテラル型が受け取る型パラメータ T に対しての制約を示しています。この例の場合、T は string 型または string 型のユニオン型である必要があります。
テンプレートリテラル型を使用すると、文字列の結合や変換を行うことができますが、これは基本的に文字列型に対しての操のため、T が string 型であることを保証することで安全性を高めています。
例えば、もし T が number 型や他の型でも許容されると、文字列型としての操作が保証されなくなり、実行時にエラーが発生する可能性があります。extends string
を省略すると以下のようなコンパイルエラーになります。
以下の関数 hello() の戻り値はテンプレートリテラル型になりますが、戻り値の型注釈を省略すると戻り値は string 型になります(その場合、7行目はエラーになりません)。
function hello(name: string): `Hello, ${string}!` { return `Hello, ${name}!`; } let helloFoo = hello('Foo'); helloFoo = "Hello, Bar!"; // OK helloFoo = "Hello!" // コンパイルエラー(Hello, ${string}! の形ではない) // Type '"Hello!"' is not assignable to type '`Hello, ${string}!`'.
以下のように as const を使えば戻り値はテンプレートリテラル型になります。
function hello(name: string) { return `Hello, ${name}!` as const; }
以下のようにジェネリクスを使うと、T は文字列リテラル型になるので、戻り値は文字列リテラル型になります。以下の場合、変数 helloFoo はリテラル型である Hello, Foo! 型になります。
function hello<T extends string>(name: T): `Hello, ${T}!` { return `Hello, ${name}!`; } let helloFoo = hello('Foo'); helloFoo = "Hello, Foo!"; // OK helloFoo = "Hello, Bar!" // コンパイルエラー(Hello, Foo! 型ではない) // Type '"Hello, Bar!"' is not assignable to type '"Hello, Foo!"'.
リテラル型の widening
リテラル型における widening は、リテラル型が変数に代入された際に、より広い型に拡大される現象を指します。具体的には、リテラル型がそのままの型として維持されるのではなく、より一般的な型に広げられることを指します。
この現象は式としてのリテラルが let で宣言された変数に代入される場合に発生します。
変数宣言の型注釈が省略されている場合、通常は以下の const で宣言された x のように変数の初期化子(= の右側の式)の型(この場合はリテラル型)に推論されますが、let で宣言された場合は、より一般的な型(この場合は string 型)に推論されます。
let で宣言された変数でも、以下の z のように型注釈をつければリテラル型になります。
const x = "hello"; // x は hello 型(リテラル型)となる let y = "hello"; // y は string 型となる let z: "hello" = "hello"; // z は hello 型(リテラル型)となる
また、オブジェクト型のプロパティの型でも、型注釈を省略すると、同様に widening が発生します。
以下の場合、obj はオブジェクト型 { prop: string } として推論され、そのプロパティ prop の型は初期化時に与えられた string 型として推論されます。その後、新しい値を obj.prop に代入しても問題ありません。これも widening です。
const obj = { prop: "hello" }; obj.prop = "world";
厳密に型を指定する場合は、型注釈を使用して以下のように書くことができます。
const obj: { prop: "hello" } = { prop: "hello" }; obj.prop = "world"; // コンパイルエラー("world" は "hello" 型に代入できない)
同様に、type 文や interface を使って型を定義して型注釈すれば、widening は発生しません。
type Obj = { prop: "hello"; }; const obj: Obj = { prop: "hello" }; obj.prop = "world"; // コンパイルエラー("world" は "hello" 型に代入できない)
プロパティに再代入させたくなければ readonly キーワードを使って読み取り専用にすることもできます。
widening されるリテラル型とされないリテラル型
リテラル型の中にも widening されるリテラル型と widening されないリテラル型があります。
型注釈を省略して、型推論により推論されたリテラル型は widening されるリテラル型となります。
以下の場合、widening されるリテラル型の変数 x を変数 hello1 に初期値として代入すると hello1 は widening されて string 型になります。
型注釈を付けた widening されないリテラル型の変数 y を変数 hello2 に初期値として代入すると hello2 は widening されず hello 型(リテラル型)となります。
const x = "hello"; // widening されるリテラル型 let hello1 = x; // hello1 は string 型 hello1 = "world"; // OK const y : "hello" = "hello"; // widening されないリテラル型 let hello2 = y; // hello1 は hello 型(リテラル型) hello2 = "world"; // コンパイルエラー("hello" 以外は代入できない)
型の絞り込み
TypeScript の絞り込み(Type Narrowing)は、型に基づいて値を絞り込むプロセスです。
変数やオブジェクトの型を絞り込むことにより、コードの安全性を向上させ、コンパイラがより正確な型情報を持つようになります。絞り込みにはいくつかの方法(型ガード)があります。
TypeScript では、特定の型であるかどうかを判定するために型ガードが使用されます。型ガードは型を確認し、それに基づいて絞り込みを行う方法です。
- 型の絞り込み
- 型の絞り込みは、TypeScript において、特定のスコープ内で変数やオブジェクトの型をより具体的なものに絞り込むプロセスを指します。
- これは、条件分岐や特定の操作によって、TypeScript が変数やオブジェクトの型を推論し、そのスコープ内でより具体的な型として扱うことです。
- 型ガード
- 型ガードは、型の絞り込みを実現するための手段や仕組みの一部です。具体的には、typeof、instanceof、in、カスタム型ガードなど、様々な方法が型ガードとして機能します。
- typeof ガード
- instanceof ガード
- in ガード
- nullish 判定ガード
- タグ付きユニオン
- ユーザー定義型ガード
- 型ガードを使用することで、TypeScript に特定の条件を教え、それに基づいて変数やオブジェクトの型を絞り込むことができます。
- 型ガードは、型の絞り込みを実現するための手段や仕組みの一部です。具体的には、typeof、instanceof、in、カスタム型ガードなど、様々な方法が型ガードとして機能します。
- 制御フロー分析と型ガードによる型の絞り込み(サバイバルTypeScript)
- Narrowing(TypeScript Handbook)
- 型ガード(TypeScript Deep Dive 日本語版)
typeof を使った絞り込み
値や変数の型を調べるために typeof 演算子を使用できます。
typeof は typeof 式
という構文で使い、式の評価結果(データの型名)を文字列で返します。
console.log(typeof "foo"); // "string" const num = 5; console.log(typeof num);; // "number" console.log(typeof (num === num)); // "boolean" console.log(typeof {foo: 'bar'});; // "object" console.log(typeof ["foo","bar"]); // "object"(配列もオブジェクト) function foo():string {return "foo"}; console.log(typeof foo); // "function" console.log(typeof undefined); // "undefined" console.log(typeof null); // "object"
プリミティブ値が typeof 演算子に与えられた場合はそのタイプに応じて異なる値が返されますが、オブジェクトが与えられた場合は関数(function)またはそれ以外のオブジェクト(object)の2種類になります。そのため配列や Map などもオブジェクトになります。
また、例外的に typeof null は "object" を返します。
式 | typeof 式 の戻り値 |
---|---|
文字列 | "string" |
数値 | "number" |
真偽値 | "boolean" |
オブジェクト(関数以外) | "object" |
関数(オブジェクト) | "function" |
シンボル | "symbol" |
BigInt | "bigint" |
undefined | "undefined" |
null | "object" (例外的) |
例えば、以下のようにユニオン型に対して絞り込みを行う場合に typeof 演算子を利用できます。
この例では、引数 value が number | string のユニオン型になっているので、typeof 演算子を使って value が number 型か string 型かをチェックしています。
function func(value: number | string) { if (typeof value === 'number') { // ここでは value は number 型に絞り込まれる console.log(value.toFixed(2)); } else { // ここでは value は string 型に絞り込まれる console.log(value.toUpperCase()); } }
typeof value === 'number' の判定が true であれば、そのスコープ内では value を number 型として使用できます。そして else 節では value は string 型に絞り込まれて string 型として使用できます。
上記は以下のように記述することもできます。
function func(value: number | string) { console.log( typeof value === 'number' ? value.toFixed(2): value.toUpperCase() ); }
配列の絞り込み
配列を typeof で調べるとオブジェクト("object")となってしまいます。配列かどうかを調べるには Array.isArray() メソッドや instanceof 演算子などを利用することができます。
以下は引数に文字列の配列と文字列のユニオン型(string[] | string)を受け取る関数の例です。Array.isArray() で引数が配列かどうかを判定し、配列の場合は配列の join()メソッドを使っています。
function hello(x: string[] | string) { if (Array.isArray(x)) { // ここでは x は string[] console.log(`Hello, ${ x.join(" and ")}!`); } else { // ここでは x は string console.log(`Hello, ${x}!`); } } hello(['Foo', 'Bar', 'Baz']); // Hello, Foo and Bar and Baz! hello('Foo'); // Hello, Foo!
上記は instanceof を使って以下のように書き換えても同じことです。
function hello(x: string[] | string) { if (x instanceof Array) { console.log(`Hello, ${ x.join(" and ")}!`); } else { console.log(`Hello, ${x}!`); } }
関連項目:配列型に絞り込む
instanceof を使った絞り込み
instanceof 演算子を使用すると、特定のクラスのインスタンスであること(そのクラスのコンストラクタで生成されたかどうか)を確認できます。
Bird クラスは Animal クラスを拡張したクラスなので(Bird 型は Animal 型の部分型になるので)、関数 moveAnimal() の引数に Bird 型のインスタンスも受け取ることができます。
以下の場合、fly() メソッドを使用するには Bird クラスのインスタンスである必要があるので、instanceof を使って Bird クラスのインスタンスであることを確認する必要があります。
class Animal { move() { console.log("Moving..."); } } class Bird extends Animal { fly() { console.log("Flying..."); } } function moveAnimal(animal: Animal): void { if (animal instanceof Bird) { // animal は Bird クラスのインスタンスとして絞り込まれる animal.fly(); } else { // animal は Animal クラスのインスタンスとして絞り込まれる animal.move(); } } const animal = new Animal(); // Animal クラスのインスタンス const bird = new Bird(); // Bird クラスのインスタンス moveAnimal(animal); // Moving... moveAnimal(bird); // Flying...
関連項目:instanceof 演算子(クラス)
in を使ったプロパティの存在チェック
オブジェクトのプロパティが存在するかどうかを in 演算子を使って確認して型を絞り込むこともできます。
以下の FamilyMember 型は Human 型または Pet 型のユニオン型 Human | Pet として定義しています。
関数 getAge() の引数の型 FamilyMember は Human か Pet の可能性があり、Human にしか age プロパティはないので FamilyMember は age プロパティを持たない可能性があります。
そのため、以下の関数 getAge() では in 演算子を使って引数のオブジェクトが age プロパティを持つかどうかを判定して絞り込みを行っています。
type Human = { name: string; age: number; } type Pet = { name: string; } type FamilyMember = Human | Pet; function getAge(member: FamilyMember): number | string { if('age' in member) { // age プロパティを持っているので member は Human return member.age; }else{ return 'age is unknown'; } } const foo: FamilyMember = { name: 'Foo', age: 25, } const tama: FamilyMember = { name: 'Tama' } console.log(getAge(foo)); // 25 console.log(getAge(tama)); // age is unknown
nullish 判定ガード
必要に応じて null または undefined でないことを確認してから、型を絞り込むことができます。
if (value !== null && value !== undefined)
は if (value != null)
でもほぼ同じことです。
function printLength(value: string | string[] | null | undefined) { if (value !== null && value !== undefined) { // value は null でも undefined でもない(value は 文字列または配列に絞り込まれる) console.log(value.length); // length は文字列にも配列にもある } else { console.log('値が存在しません'); } } printLength('foo'); // 3 printLength(['foo', 'bar']); // 2 printLength(null); // 値が存在しません printLength(undefined); // 値が存在しません
null 合体演算子 (??) を使用
null 合体演算子 ??
を使用して nullish(null または undefined)でない場合に絞り込むことができます。
?? は左辺が null または undefined の場合に右辺の値を返し、それ以外の場合は左辺の値を返します。OR 演算子 || とは異なり、null または undefined 以外の falsy な値(0や''など)の場合、左辺の値を返します。
function func(value: number | null | undefined) { // value が null または undefined でない場合に絞り込まれる const result = value ?? 0; // result は引数に与えられた数値または 0 console.log(result.toFixed(2)); } func(12.345); // 12.35 func(0); // 0.00 func(null); // 0.00 func(undefined); // 0.00
null 合体代入演算子(??=)を使用
null 合体代入演算子 ??=
は 左辺が nullish (null または undefined) である場合にのみ代入を行います。
左辺が null または undefined 以外の falsy な値(0や''など)の場合は、代入されません。
function func(value: number | null | undefined) { // value が null または undefined でない場合に絞り込まれる value ??= 0; // value は引数に与えられた数値または 0 console.log(value.toFixed(2)); } func(12.345); // 12.35 func(0); // 0.00 func(null); // 0.00 func(undefined); // 0.00
タグ付きユニオン(Discriminated Union)
タグ付きユニオンは、オブジェクトの型を判別するために「タグ」と呼ばれる特定のプロパティを使う方法です。このプロパティの値によって、TypeScript がどの型かを理解し、対応する型のメンバーにアクセスできるようにします。
以下の Shape 型は円と正方形を表す型(Circle 型と Square 型)を持つユニオン型です。
そして、Circle 型と Square 型はともに kind プロパティを持っていて、これがタグとしての役割を果たします。kind の値が 'circle' の場合、そのオブジェクトは Circle 型として扱われ、'square' の場合は Square 型として扱われます。
type Circle = { kind: 'circle'; // タグとしての役割を果たすプロパティ(文字列リテラル型) radius: number; } type Square = { kind: 'square'; // タグとしての役割を果たすプロパティ(文字列リテラル型) sideLength: number; } type Shape = Circle | Square;
Shape 型はタグ(kind プロパティ)を持つオブジェクト(Circle 型と Square 型)のユニオン型になっています(タグ付きユニオン)。これにより、タグ(kind プロパティ)の値を調べれば、オブジェクトがどの型なのかを判別することができます。
タグとなるプロパティに使える型は(数値、文字列、論理値の)リテラル型と null、undefined です。この例ではプロパティ名を kind としていますが、type などにすることも多いようです。
以下はタグ付きユニオンを使用してオブジェクトの型を絞り込む例です。
shape.kind の値に基づいて型を判別し、それに応じてオブジェクトのメンバーにアクセスしています。
function getArea(shape: Shape): number { if(shape.kind === 'circle') { // shape は Circle クラスのインスタンスとして絞り込まれる return Math.PI * shape.radius ** 2; } else { // shape は Square クラスのインスタンスとして絞り込まれる return shape.sideLength ** 2; } } const circle1: Shape = { kind: 'circle', radius: 10 } const square1: Shape = { kind: 'square', sideLength: 20 } console.log(getArea(circle1)); // 314.1592653589793 console.log(getArea(square1)); // 400
switch 文を使って絞り込む
以下は上記の関数 getArea() を switch 文を使って書き換えたものです。
ユニオン型の構成要素が多い場合などでは、switch 文を使った方が簡潔に記述できます。タグ付きユニオンでは switch 文がよく使われます。
以下の場合、case 節で return しているので、break 文はありません。
function getArea(shape: Shape): number { switch (shape.kind) { case 'circle': return Math.PI * shape.radius ** 2; case 'square': return shape.sideLength ** 2; } }
タグ付きユニオン(tagged union)は判別可能なユニオン型(discriminated union)や直和型と呼ぶこともあります。
typeof 型演算子
typeof 演算子は、JavaScript の演算子ですが、TypeScript でも異なる機能として実装されています。
JavaScript の typeof 演算子はデータの型名(文字列)を返します。例えば以下のような型の絞り込みなどで、変数の型をチェックする場合などに使用します。
function func(value: number | string) { if (typeof value === 'number') { console.log(value.toFixed(2)); } else { console.log(value.toUpperCase()); } }
型注釈や型エイリアス(type 文)などの型のコンテキストで typeof を使うと、TypeScript の typeof 型演算子として、変数やプロパティの型を返します。
let foo = 'Foo'; // JavaScript の typeof(通常の変数に結果を代入) const fooType = typeof foo; console.log(fooType); // string // TypeScript の typeof(型のコンテキストで使用) type FooType = typeof foo; // type FooType = string console.log(FooType); // 通常の値としては使えないのでエラーになる
変数やプロパティ以外の値を直接指定すると、コンパイルエラーになります。
type Type123 = typeof 123; //コンパイルエラー // Identifier expected.
変数の型を取得
TypeScript の typeof 型演算子は、変数やオブジェクトの型を取得するために使われます。
以下では type 文を使って T を変数 hello に代入された値の型の別名としています。変数 hello は型注釈 : string により string 型なので typeof hello は string 型になります。
// 型注釈により hello は string 型 const hello: string = 'Hello'; // 型 T は変数 hello の型(string 型) type T = typeof hello; // foo は T 型(string 型)の変数になる const foo: T = 'Foo';
typeof 型演算子で取得した型は type 文で別名(型エイリアス)を付けずに、以下のように直接型注釈に指定することもできます。
// hello は string 型 const hello: string = 'Hello'; // typeof hello は string 型なので foo は string 型 const foo: typeof hello = 'Foo';
型注釈がない場合は、型推論の結果(の型)を typeof を使って取得して利用することができます。
以下では typeof obj
により、{ name: string; value: number; }
という型を取得して型エイリアス T に代入して、変数 obj2 に適用しています。
変数 obj3 には、直接 typeof obj
を型注釈として書いています。
const obj = { name: 'foo', value: 123 }; type T = typeof obj; // T は { name: string; value: number; } const obj2: T = { name: 'bar', value: 456 }; const obj3: typeof obj = { name: 'baz', value: 789 };
オブジェクトのプロパティの型を取得
typeof を使ってオブジェクトのプロパティの(値の)型を取得することもできます。
const obj = { name: 'foo', // name プロパティは string 型と推論される value: 123 // value プロパティは number 型と推論される }; type ObjectName = typeof obj.name; // typeof obj.name は string 型 const propName: ObjectName = 'abc'; // string 型 const propValue: typeof obj.value = 777; // number 型
後述の lookup 型を使ってもオブジェクトのプロパティの型を取得できますが、直接オブジェクトから取得することはできず、オブジェクトの型から取得する必要があります。
const obj = { name: 'foo', value: 123 }; // obj の型を取得 type Obj = typeof obj; // { name: string; value: number; } // lookup 型(Obj は obj の型) type ObjectName = Obj['name']; // string const propName: ObjectName = 'abc'; // string 型 const propValue: Obj['value'] = 777; // number 型
配列形式でプロパティの型を取得
オブジェクトのプロパティにアクセスするにはドット演算子.
または、配列演算子[]
を使うことができるので、以下のように配列形式でプロパティの型を取得することもできます。
const obj = { name: 'foo', value: 123 }; type ObjectName = typeof obj['name']; const propName: ObjectName = 'abc'; // string 型 const propValue: typeof obj['value'] = 777; // number 型
プロパティの型をユニオン型で取得
typeof を使ってオブジェクトのプロパティの型を取得してユニオン型にするには、以下のように記述することができます。
const obj = { name: 'foo', value: 123 }; type ObjUnion = typeof obj.name | typeof obj.value; // string | number const foo: ObjUnion = 'abc'; const bar: ObjUnion = 123;
配列演算子[]
を使って配列形式で添え字部分をユニオン型にすると、typeof の結果をユニオン型で返してくれるので、上記は以下のように記述できます。
type ObjUnion = typeof obj['name'|'value']; // string | number
但し、ユニオン型では同じ型は一つにまとめられるため、以下の UserUnion 型は string | number になります。
const user = { name: 'Foo', id: 1, age: 22, email: 'foo@example.com' }; type UserUnion = typeof user['name'|'id'|'age'|'email']; // string | number
as const を使って widening しないようにすると、以下のようにリテラル型のユニオン型が取得できます。
const user = { name: 'Foo', id: 1, age: 22, email: 'foo@example.com' } as const; type UserUnion = typeof user['name'|'id'|'age'|'email']; // type UserUnion = "Foo" | 1 | 22 | "foo@example.com"
プロパティの数が多いと全てのプロパティを記述するのは大変ですが、keyof と組み合わせると全てのプロパティの型をリテラル型のユニオン型で取得できます。
type UserUnion = typeof user[keyof typeof user]; // UserUnion は "Foo" | 1 | 22 | "foo@example.com"
keyof typeof user
ではプロパティ名のリテラル型のユニオン型を取得しています。
type UserKeys = keyof typeof user // UserKeys は "name" | "id" | "age" | "email" type UserUnion = typeof user[UserKeys]; // UserUnion は "Foo" | 1 | 22 | "foo@example.com"
配列要素の型からユニオン型を作成
配列要素の型をユニオン型で取得する場合、添え字に数値型を表す number を指定することができます。
配列は number 型のインデックスに要素を代入しているオブジェクトなのでインデックス番号の代わりに number を使えます(インデックスアクセス型)。
以下は、[number]
を使用して配列 arr の要素の型(ユニオン型)を取得しています。
const arr =['abc', 123, true]; type ArrUnion = typeof arr[number]; // または (typeof arr)[number] // type ArrUnion = string | number | boolean type Arr = typeof arr[1]; // これでも同じユニオン型が取得される // type Arr = string | number | boolean
この場合も、as const を使って widening しないようにすると、以下のようにリテラル型のユニオン型が取得できます。
const directions = ['north', 'south', 'east', 'west'] as const; type Direction = typeof directions[number]; // または (typeof directions)[number] // type Direction = "north" | "south" | "east" | "west"
以下はオブジェクトの配列からリテラル型のユニオン型を取得する例です。
const animals = [ { species: 'cat', name: 'Tama' }, { species: 'dog', name: 'Pochi' }, { species: 'mouse', name: 'Chutaro' } ] as const; type Animal = typeof animals[number]['species']; // type Animal = "cat" | "dog" | "mouse" type AnimalName = typeof animals[number]['name']; // type AnimalName = "Tama" | "Pochi" | "Chutaro"
keyof 型演算子
keyof 型演算子は指定されたオブジェクトの型のプロパティ名を文字列リテラルのユニオン型として返します。型を返すので keyof 型とも呼びます。
keyof は TypeScript 独自の型の演算子で、JavaScript にはありません。
以下の場合、User というオブジェクト型のプロパティは name と id なので、keyof User
は、'name' | 'id'
という文字列リテラル型のユニオン型になります。
type User = { name: string; id: number; } // 以下の UserKeys は 'name' | 'id'(文字列リテラル型のユニオン型) type UserKeys = keyof User; let key: UserKeys = 'name'; key = 'id'; key = 'age'; //コンパイルエラー(name と id 以外は受け入れない) // Type '"age"' is not assignable to type 'keyof User'.
上記の keyof User
(UserKeys 型)は、User 型のオブジェクトの全てのプロパティ名を受け入れる型になります。
そのため UserKeys 型として宣言した変数 key には 'name' と 'id' は代入できますが、User 型のプロパティ名以外の文字列を代入することはできません。
keyof には直接オブジェクトを指定することもできます。
type UserKeys = keyof { name: string; id: number; }
型がネストされた構造を持っている場合
keyof 型演算子はすべてのネストされたプロパティ名をフラット(平ら)にして取得します。そのため、以下の場合、UserKeys は 'name' | 'age' | 'address' のようなユニオン型になります。
type User = { name: string; age: number; address: { street: string; city: string; } }; type UserKeys = keyof User; //'name' | 'age' | 'address' let prop : UserKeys = 'name'; // OK prop = 'age'; // OK prop = 'address'; // OK prop = 'city'; // コンパイルエラー
このとき、address プロパティはオブジェクト型なので keyof を使用できます。
オブジェクト型 User の address プロパティの型は User['address'] で参照できます(lookup 型)。
type AddressKeys = keyof User['address']; // AddressKeys は 'street'|'city' let addressKey: AddressKeys = 'street'; // OK addressKey = 'city'; // OK addressKey = 'zip'; // コンパイルエラー(AddressKeys は 'street'|'city')
ユニオン型の場合
keyof 演算子は、ユニオン型に対して使用された場合、それらの構成要素の共通のプロパティを取得します。ユニオン型の各要素が異なるプロパティを持っている場合、keyof は never 型になります。
type A = { name: string; age: number; } type B = { name: string; id: number; } type C = { id: number; } type AorB = A | B; type AorBKeys = keyof AorB; // "name"(AとBに共通のプロパティ) type AorC = A | C; type AorCKeys = keyof AorC; // never(AとCに共通のプロパティはない)
具体的には、ユニオン型に含まれる各型のプロパティ名の共通部分が抽出され、それが結果となります。共通のプロパティがない場合は never 型になります。
インデックスシグネチャの場合
keyof 型演算子はインデックスシグネチャでも機能します(Keyof Type Operator)。
インデックスシグネチャのキーが string 型の場合は、string | number になり、インデックスシグネチャのキーが number 型の場合は number になります。
type Foo = { [key: string]: string; // キーの型は string } type FooKey = keyof Foo; // string | number type Bar = { [key: number]: string; // キーの型は number } type BarKey = keyof Bar; // number
keyof typeof
keyof にはオブジェクトの型を指定します。オブジェクト(変数)を指定するとエラーになります。
const user = { name: 'foo', id: 1 } type UserKeys = keyof user; // コンパイルエラー // 'user' refers to a value, but is being used as a type here. Did you mean 'typeof user'?
オブジェクトからプロパティ名のユニオン型を取得するには、以下のように typeof 型演算子と組み合わせて使うことができます(typeof は型を返します)。
type UserKeys = keyof typeof user; // "name" | "id"
上記は以下と同じことです。
type User = typeof user; // {name: string; id: number;} type UserKeys = keyof User; // "name" | "id"
以下の関数 convertUnit() は数値とその単位と変換後の単位の文字列を引数に受け取り、変換後の値をコンソールに出力します。from と to として引数に受け取る単位の型は、単位を表すオブジェクト unit のプロパティ名のリテラル型のユニオン型として keyof typeof unit で取得しています。
具体的には単位の型 UnitKey は "mm" | "cm" | "m" | "km" となり、引数の from と to にこれら以外の文字列を指定するとコンパイルエラーになるようになっています。
11行目の unit[from] と unit[to] はオブジェクト unit へのプロパティアクセスですが、from と to は "mm" | "cm" | "m" | "km" のユニオン型なのでコンパイルエラーにならずにアクセスができます。
const unit = { mm: 1, cm: 10, m: 1000, km: 1000000 } type UnitKey = keyof typeof unit; // 単位の型 "mm" | "cm" | "m" | "km" function convertUnit(value: number, from: UnitKey, to: UnitKey) { const converted = value * unit[from] / unit[to]; console.log( `${value}${from} は ${converted}${to} です。`); } convertUnit(5000, 'cm', 'km'); // 5000cm は 0.05km です。 convertUnit(1, 'km', 'mm'); // 1km は 1000000mm です。
上記のように keyof と typeof を使った実装では、オブジェクト unit のプロパティを追加・削除しても、関数の型定義が自動的に追随します。
例えば、以下のように unit にプロパティを追加しても、関数側は変更する必要がありません。
const unit = { mm: 1, cm: 10, feet: 304.8, // 追加 m: 1000, km: 1000000 } type UnitKey = keyof typeof unit; // 自動的に "mm" | "cm" | "feet" | "m" | "km" になる function convertUnit(value: number, from: UnitKey, to: UnitKey) { const converted = value * unit[from] / unit[to]; console.log( `${value}${from} は ${converted}${to} です。`); } convertUnit(300, 'feet', 'm'); // 300feet は 91.44m です。
Indexed Access 型(Lookup 型)
Indexed Access 型はオブジェクト型や配列型のメンバーにアクセスするための構文で、オブジェクトの特定のプロパティの型を動的に取り出したり、新しい型を作成する際に利用できます。
Indexed Access 型(インデックスアクセス型)は Lookup 型とも呼びます。以降では表記の短い Lookup を主に使っています。
具体的には、オブジェクト型 T とキーの型 K が与えられたとき、T[K]
という形式で、オブジェクト型 T のプロパティ K の型を取得できます。
これは、オブジェクト型 T のプロパティの名前が K(多くの場合は文字列のリテラル型)によって指定され、そのプロパティの型が取得されるという動作です。
以下の場合、MyObj 型の中の age プロパティの型が number として取得されます。
type MyObj = { name: string; age: number; hasJob: boolean; }; type AgeType = MyObj['age']; // number('age' は文字列のリテラル型)
以下は User 型から Lookup 型を使って特定のプロパティの型を取り出す例です。
type User = { name: string; age: number; address: { street: string; city: string; } }; // User 型から Lookup 型を使って特定のプロパティの「型」を取り出す type Name = User['name']; // string type Age = User['age']; // number type Address = User['address']; // {street: string; city: string} type City = User['address']['city'] // string
構文的には通常のオブジェクトや配列のブラケット記法[]
と同じですが、「型」に対して適用しています。
また、Lookup 型の構文 T[K]
の K の部分にはユニオン型や keyof を使って指定することもできます。
type Member = { name: string; age: number; isActive: boolean; }; // 文字列リテラル型のユニオン型を指定 type NameOrAge = Member['name'|'age']; // string | number type NameOrAgeUnion = 'name'|'age'; // リテラル型のユニオン型 type NameOrAge2 = Member[NameOrAgeUnion] // string | number(7行目と同じこと) // keyof でオブジェクトの各プロパティの値の型のユニオン型を指定 type MemberProps = Member[keyof Member]; // string | number | boolean
typeof 型演算子と組み合わせるとオブジェクトのインスタンスから型を取得することもできます。
const foo = { name: 'Foo', age: 23 } type Foo = typeof foo; // {name: string; age: number;} type FooName = Foo['name']; // string // または type FooName = typeof foo['name']; type FooAge = Foo['age']; // number type FooProp = Foo[keyof Foo]; // string | number // または type FooProp = typeof foo[keyof typeof foo]
keyof と使う
keyof と Lookup 型を使って、オブジェクトの各プロパティの値の型をユニオン型で取得できます。
以下のように keyof でオブジェクト(型)の全てのプロパティを文字列リテラル型のユニオン型で取得して、T[K]
の K の部分に指定して、そのオブジェクト型のプロパティの値の型のユニオン型を表せます。
type User = { name: string; age: number; address: { street: string; city: string; } }; // User 型のプロパティ名(の文字列リテラル型)のユニオン型 type UserKeys = keyof User; // "name" | "age" | "address" // User 型の各プロパティの値の型のユニオン型 type UserPropValue = User[UserKeys]; // 以下と同じこと // type UserPropValue = User[ "name" | "age" | "address" ]; // type UserPropValue = string | number | {street: string; city: string}; let userProp : UserPropValue = 'foo'; userProp = 123; userProp = { street: '1 Mercer st', city: 'New York' }; // true は User 型のプロパティの型と異なるので代入できない userProp = true; // コンパイルエラー
上記の11行目と13行目はまとめて以下のように記述できます。
type UserPropValue = User[keyof User];
以下は Lookup 型を使って特定の型のプロパティを取得するサンプルです。
関数 getAction は、引数として AnimalTypes 型の type を受け取り、それに基づいて AnimalAction 型の値を返します。関数内で AnimalAction[AnimalTypes] という型(Lookup 型)を使用して、引数で与えられた type に対応するアクションを取得しています。
これにより、関数を呼び出す際に誤った動物の種類を渡すことがなく、安全にアクションを取得できます。例えば、getAction('cat') のように存在しない動物の種類を渡すと、TypeScript はエラーを検出します。
また、AnimalTypes の定義に keyof AnimalAction を使用することで、AnimalAction に新しい動物が追加された場合に、型の変更が自動的に反映されます。
type AnimalAction = { dog: string; bird: string; fish: string; }; // AnimalAction のプロパティ名(リテラル型)のユニオン型 type AnimalTypes = keyof AnimalAction; // 'dog' | 'bird' | 'fish' // 引数としてAnimalTypes 型の type を受け取り、それに基づいて AnimalAction 型の値を返す function getAction(type: AnimalTypes): AnimalAction[AnimalTypes] { const animalAction: AnimalAction = { dog: 'bark and run', bird: 'fly and sing', fish: 'swim and dance', } // 戻り値の型 AnimalAction[AnimalTypes] は Lookup 型 return animalAction[type] } console.log(getAction('dog')); // bark and run console.log(getAction('bird')); // fly and sing console.log(getAction('fish')); // swim and dance
keyof とジェネリクスと使う
以下は任意のオブジェクトのインスタンスとプロパティ名を引数に受け取り、そのプロパティの値を返す関数 getProperty() とプロパティの値を設定する関数 setProperty() です。
いずれの関数も任意の型のオブジェクトを受け取るため(引数に受け取るオブジェクトによって戻り値や処理する値の型が異なるため)、ジェネリクスで表現しています。
この2つの関数は T と K という型引数を持ち、T は第1引数 obj、K は第2引数 key の型を表しています。
また、K extends keyof T
として、extends キーワードを使って K
は keyof T
の部分型でなければならないという制約を設けています。
keyof T
は T 型のオブジェクトの全てのプロパティ名の文字列リテラル型のユニオン型になるので、K は T 型のオブジェクトのプロパティ名である必要があります(keyof 型演算子)。
これにより、第2引数には、第1引数に指定したオブジェクトのプロパティ名以外を指定するとコンパイルエラーとなります。
また、関数 setProperty() の第3引数には第2引数に指定したプロパティと異なる型の値を指定するとコンパイルエラーとなります。
関数 getProperty() の戻り値は Lookup 型の T[K] 型と推論され、setProperty() は第3引数 value に Lookup 型の T[K] 型の値を受け取ります。
function getProperty<T, K extends keyof T>(obj: T, key: K) { return obj[key]; // 戻り値の型は T[K] と推論される } function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]) { obj[key] = value; // value は T[K] 型 } type User = { name: string; age: number; address: { street: string; city: string; } }; const foo: User = { name: 'Foo', age: 22,const catAction = address: { street: '1 Broadway', city: 'New York' } } const fooName = getProperty(foo, 'name'); // "Foo" (string) const fooAge = getProperty(foo, 'age'); // 22 (number) const fooAddress = getProperty(foo, 'address'); // {street: '1 Broadway', city: 'New York'} const fooCity = getProperty(foo.address, 'city'); // "New York" (string) setProperty(foo,'name', 'FOO'); // OK setProperty(foo,'age', '25'); // コンパイルエラー ('25'は文字列なのでエラー) // Argument of type 'string' is not assignable to parameter of type 'number'.
以下は型注釈のないオブジェクトで上記の関数を使う例です。animalAction の型は推論され、以下のコードは問題なく実行され、存在しないプロパティ名を指定するとコンパイルエラーになります。
const animalAction = { dog: 'bark and run', bird: 'fly and sing', fish: 'swim and dance', }; console.log(getProperty(animalAction, 'dog')); // bark and run setProperty(animalAction,'dog', 'sit and eat'); console.log(getProperty(animalAction, 'dog')); // sit and eat // 以下は存在しないプロパティ名を指定しているのでコンパイルエラーになる const catAction = getProperty(animalAction, 'cat'); // コンパイルエラー // Argument of type '"cat"' is not assignable to parameter of type '"dog" | "bird" | "fish"'.
keyof と Lookup を使ったジェネリック型
以下は keyof と型変数を使ってオブジェクトのプロパティの型を表すジェネリック型の例です。
type PropValueType<T, K extends keyof T> = T[K]; type User = { name: string; age: number; address: { street: string; city: string; } }; type UserName = PropValueType<User, "name">; // string type UserAge = PropValueType<User, "age">; // number type UserAddress = PropValueType<User,"address">; //{street:string; city:string} // ネストしたプロパティの値の型も取得可能 type UserAddressCity = PropValueType<UserAddress, "city">; //string(以下でも同じ) type UserAddressCity2 = PropValueType<PropValueType<User,"address">, "city">;
配列型と Indexed Access 型
配列型の要素の型を参照するのにも Indexed Access 型が使えます。配列型の場合、[number]
を使用して配列の要素の型を取得することができます。
以下は StringArr 型の配列の要素の型を StringArr[number] で取得しています。string[] という型は全ての要素が string 型なので、数値を添え字(インデックス番号)で指定しても同じ結果になります。
type StringArr = string[]; // string 型の要素を持つ配列の型 type SA = StringArr[number]; // string type SA2 = StringArr[0]; // string
以下は Colors[number]
を使用して、Colors 配列の各要素の型を取得し、その結果として ColorUnion に'red' | 'green' | 'blue'
という文字列リテラル型のユニオン型が得られます。
number を使うことで、配列の全体の型が取得できます。インデックス番号(数値リテラル型)を使用することもできます。
type Colors = ['red', 'green', 'blue']; type ColorUnion = Colors[number]; // "red" | "green" | "blue" type Color0 = Colors[0]; // "red" type Color1or2 = Colors[1 | 2]; // "green" | "blue"
要素がユニオン型の配列型に対しても使うことができ、ユニオン型が取得されます。
type StrNumArray = (string | number)[]; type SN = StrNumArray[number]; // string | number
オブジェクト同様、typeof 型演算子と組み合わせると、配列の値から要素の型を取得できます。
const arr = ['foo', 1, null]; type Arr = typeof arr[number]; // string | number | null
タプル型と Indexed Access 型
タプル型の要素の型を参照するのにも Indexed Access 型が使えます。配列型同様、ブラケット記法に number や数値リテラル型を使用してタプル型の要素の型を取得することができます。
以下では Point[number]
を使用して、タプル Point の各要素の型を取得し、その結果として PointUnion に number | string | boolean
というユニオン型が得られます。
number を使うことで、タプルの全体の型が取得できます。
また、数値リテラル型を使用してタプル型の個々の要素の型を取得できます。
type Point = [number, string, boolean]; type PointUnion = Point[number]; // string | number | boolean type PointFirst = Point[0] // number
型アサーション as
型アサーションは TypeScript コンパイラーの型推論を上書きする機能で、式 as 型
という構文でその式の型を指定した型に強制的に変更することができます。
型アサーションは、開発者がコンパイラに対して「私はこの値が特定の型であると確信しています」と宣言するものです。
しかし、これが実際にその型であるかどうかは実行時には検証されません。そのため、型アサーションを誤って行うと、実行時エラーが発生する可能性があります。
型アサーションを使うと推論された型や型定義済みの変数の型を任意の型に上書きができますが、誤って行うと型安全性が保証されなくなるため、使用する際は注意が必要です。
以下のコードは JavaScript では問題ありませんが、TypeScript では変数 obj の型が {}
(プロパティがないオブジェクト)と推論されるため、プロパティを追加するとコンパイルエラーになります。
const obj = {}; obj.foo = 'Foo'; // コンパイルエラー // Property 'foo' does not exist on type '{}'. obj.bar = 1; // コンパイルエラー // Property 'bar' does not exist on type '{}'.
上記のコンパイルエラーは以下のように型アサーションを使って回避することができます。
以下では as
を使って{}
が Obj 型であること、つまり、変数 obj が Obj 型であることを TypeScript コンパイラに伝えているので、string 型の foo プロパティと number 型の bar プロパティを追加してもコンパイルエラーは発生しません。
type Obj = { foo: string; bar: number }; // as による型アサーション const obj = {} as Obj; obj.foo = 'Foo'; // OK obj.bar = 1; // OK
7行目は以下のように記述することもできます。
const obj = {} as { foo: string; bar: number };
しかし、型アサーションを誤って行うと、実行時エラーが発生する可能性があります。
以下では変数 obj が Obj 型であるしているので、実際には存在しない string 型の foo プロパティの length プロパティにアクセスしてもコンパイルエラーは発生しませんが、実行時にエラーが発生します。
type Obj = { foo: string; bar: number }; const obj = {} as Obj; // コンパイルエラーは出ない console.log(obj.foo.length); // 実行時エラーが発生 // Uncaught TypeError: Cannot read properties of undefined (reading 'length')
型アサーションのもう1つの記法
型アサーションには 式 as 型
という構文の他に、<型>式
という構文もあります。但し、< >
という構文が JSX にもあるため、現在ではこの記法はあまり使われません。
前述の例はこの記法を使うと以下のように記述することもできます。
const obj = <Obj>{}; // const obj = {} as Obj; と同じこと
コンパイルエラーになる型アサーション
型アサーションは、型のより具体的なバージョン、またはより具体的でないバージョンに変換する場合にのみが許可されます(どんな型にでも上書きできるわけではありません)。
例えば、以下のような string 型を number 型にする型アサーションや number 型をオブジェクト型にするような型アサーションはコンパイルエラーになります。
const x = "hello" as number; // コンパイルエラー // Conversion of type 'string' to type 'number' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. type Obj = { foo: string; bar: number }; const obj = 123 as Obj; // コンパイルエラー //Conversion of type 'number' to type 'Obj' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
ダブルアサーション
場合によっては、上記の理由により有効である可能性のあるより複雑な強制が許可されなくなることがあります。そのような場合は、ダブルアサーションを使用することができます。
ダブルアサーションは unknown(または any)型を経由した型アサーションで、最初にすべての型と互換性のある unknown(または any)をアサートするので、コンパイラはエラーを出しません。
ダブルアサーションはどんな型にでも変換可能ですが、実行時のエラーを引き起こす可能性が非常に高まるためとても危険です。使うべきではないと考えたほうが良さそうです。
const x = "hello" as unknown as number; // OK(但し、安全ではない)
型アサーションが必要になる例
型アサーションが必要になる場合としては、データの形式が予測困難な API からのデータ取得やユーザーの入力など実行時に動的に変化するデータの型が明確でない場合、また、DOM 要素の値を取得する場合などがあります。以下は DOM 要素の値を取得する際に型アサーション使う例です。
DOM 要素から値を取得する際、通常は適切な型が付与されますが、TypeScript はそれを確実に解釈できないことがあります。
以下の例では、getElementById メソッドは HTMLElement 型または null を返すと推論されますが、HTMLElement 型には value プロパティが存在しないため、TypeScript はコンパイルエラーにします。
const element = document.getElementById("myElement"); // element が null でなければ if (element) { const value: string = element.value; // コンパイルエラー // Property 'value' does not exist on type 'HTMLElement'. console.log(value); }
このような場合、開発者が getElementById("myElement") で取得する要素が HTMLInputElement 型であることを知っている場合、型アサーションを使用してこれを TypeScript に教えることができます。
const element = document.getElementById("myElement"); if (element) { // 型アサーションを使用して element が HTMLInputElement 型であることを伝える const value: string = (element as HTMLInputElement).value; console.log(value); }
但し、この例の場合、以下のようにユーザー定義型ガードを使うほうが型アサーションなしで安全に型情報を利用できます。
// ユーザー定義型ガード(要素が HTMLInputElement であれば true を返す関数) function isInputElement(element: HTMLElement | null): element is HTMLInputElement { return element instanceof HTMLInputElement; } // DOM 要素の取得 const element = document.getElementById("myElement"); // ユーザー定義型ガードを使用して型を確認 if (isInputElement(element)) { // element が HTMLInputElement 型であることが確定 const value: string = element.value; console.log(value); }
以下の場合、関数 getRadius() は Shape 型の配列 sahpes を受け取り、配列の要素が全て Circle 型の場合はその要素の radius プロパティの値から面積を算出して、その値の配列を返します。
関数内では every() メソッドを使って、引数に受け取った配列のすべての要素(Shape 型のオブジェクト)の kind プロパティが circle かどうか、つまり、全ての要素が Circle 型であるかどうかを確認しています。
しかし、現時点では TypeScript コンパイラはこれを理解できず、Shape 型と判断するため、コンパイルエラーを出します(将来コンパイラが改良されて理解できるようになる可能性はあります)。
type Circle = { kind: 'circle'; radius: number; } type Square = { kind: 'square'; sideLength: number; } type Shape = Circle | Square; function getRadius(sahpes: Shape[]): number[] | undefined { if(sahpes.every(shape => shape.kind === 'circle')) { // sahpes.every により shape は Circle 型のはずなのに、それを理解してもらえない return sahpes.map(shape => Math.PI * shape.radius ** 2); // コンパイルエラー // Property 'radius' does not exist on type 'Shape'. } }
このような場合、以下のように型アサーションを使うことでエラーを回避できます。
function getRadius(sahpes: Shape[]): number[] | undefined { if(sahpes.every(shape => shape.kind === 'circle')) { // 型アサーションを使って shapes が Circle 型の配列であることを伝える return (sahpes as Circle[]).map(shape => Math.PI * shape.radius ** 2); } }
この例の場合も、型アサーションを使わずに、ユーザー定義型ガードを使って書き換えることもできます。
以下は every() を使った判定部分をユーザー定義型ガードとして別途定義する例で、上記とほぼ同じです。
// ユーザー定義型ガード function isAllCircleArray(arr: any[]): arr is Circle[] { return arr.every(elem => elem.kind === 'circle') === true; } function getRadius(sahpes: Shape[]): number[] | undefined { if(isAllCircleArray(sahpes)) { return sahpes.map(shape => Math.PI * shape.radius ** 2); } }
以下では、map() の代わりに forEach() でユーザー定義型ガードを使って配列の要素が Circle 型であることを確認していますが、もっと良い方法があるかも知れません。
// ユーザー定義型ガード(value が Circle 型であれば true を返す) function isCircle(value: any): value is Circle { if(value == null) return false; return ( value.kind === 'circle' && typeof value.radius === "number" ) } function getRadius(sahpes: Shape[]): number[] | undefined { if(sahpes.every(shape => shape.kind === 'circle')) { const result: number[] = []; sahpes.forEach((shape) => { // ユーザー定義型ガードで確認 if(isCircle(shape)) { // shape は Circle 型 result.push(Math.PI * shape.radius ** 2 ) } }); return result; } }
以下はユーザー定義型ガード isCircle の引数 value の型を unknown 型とする例です。
function isCircle(value: unknown): value is Circle { if (typeof value !== "object" || value == null) { return false; } const circle = value as Record<keyof Circle, unknown>; return ( circle.kind === 'circle' && typeof circle.radius === "number" ) }
非nullアサーション
非 null アサーション演算子の!
を使用して、そのオペランドが null でなく、かつ undefined でないことをコンパイラに伝えることができます。
但し、!
は、null や undefined である可能性を型から取り除いているだけで、実際の値としては null や undefined である可能性があり、型アサーション同様、使用する際は注意が必要です。
非 null アサーション演算子(Non-Null Assertion Operator)は 式!
のように書きます。真偽値を反転させる演算子 !式
と区別するため、!ポストフィックス式演算子(Postfix !)などとも呼びます。
例えば、以下の関数 myFunc() はオプショナルな number | null 型の引数を受け取るので、引数 x は null または undefined になる可能性があるため、コンパイルエラーになります。
function myFunc(x?: number | null) { console.log(x.toFixed()); // コンパイルエラー // 'x' is possibly 'null' or 'undefined'. }
以下のように関数内で x を x!
とすることで、強制的に null や undefined の可能性を取り除けばコンパイルエラーにはなりません。
但し、関数の呼び出し時に引数を省略したり、null が渡されると、コンパイルエラーにはなりませんが、実行時エラーとなってしまいます。
function myFunc(x?: number | null) { console.log(x!.toFixed()); // エラーにならない } myFunc(); // 実行時エラー(オプショナル引数を省略すると undefined になる) // Uncaught TypeError: Cannot read properties of undefined
この場合、コンパイルエラーを発生させたくなければ、例えば以下のように記述することができます。
function myFunc(x?: number | null) { if(x != null) { // null でも undefined でもなければ console.log(x.toFixed()); }else{ console.warn('x is null or undefined') } }
以下の場合、getElementById メソッドは HTMLElement 型または null を返すと推論されるため(null の可能性があるので)、コンパイルエラーになります。
const text = document.getElementById('target').innerText; // Object is possibly 'null'.
以下のように getElementById('target') の後に !
を使えば、コンパイルエラーになりません。
const text = document.getElementById('target')!.innerText;
これは以下のように型アサーションを使うのと同じことです。
const text = (document.getElementById('target') as HTMLElement).innerText;
また、オプショナルチェイニング演算子 (?.
) を使って記述してもコンパイルエラーになりません。
const text = document.getElementById('target')?.innerText
オプショナルチェイニング演算子を使う場合は、もしその要素が存在しない場合は undefined が返されますが、非 null アサーション演算子や型アサーションを使う場合は、実行時エラーとなります。
どちらを使うかはプログラムの内容によって決めることになります。
明確な割り当てアサーション
後置の!
には、変数やプロパティの初期化が確実に行われていることをコンパイラに伝える「明確な割り当てアサーション」という機能もあります。
TypeScript は初期化されていない変数を参照した際にコンパイルエラーを出します(コンパイラオプションの strict や strictNullChecks が true の場合)。
let str: string; console.log(str); // コンパイルエラー // Variable 'str' is used before being assigned.
この場合、変数を参照する前に初期化すればエラーにはなりません(当然ですが)。
let str: string; str = 'foo'; // 変数を参照する前に初期化 console.log(str); // OK
また、以下のように変数を参照する前に変数を初期化する関数を定義して変数を初期化している場合でも、コンパイルエラーを出します。
let str: string; // 変数 str を初期化する関数 function initStr() { str = 'foo'; } initStr(); // 関数で変数を初期化(値を代入) console.log(str); // それでもコンパイルエラーになる
以下は、unknown 型の値を具体的な型へ代入する際に、型を確認してから代入する場合ですが、この場合もコンパイルエラーになります。
const value: unknown = 10; let int: number; // 変数 int を初期化する処理 if(typeof value === 'number'){ // この場合、value は number 型なので初期化されるはず int = value; } console.log(int); // それでもコンパイルエラーになる
このような場合は、以下のように明確な割り当てアサーション(definite assignment assertion)を使って、変数やプロパティの初期化が確実に行われていることをコンパイラに伝えることができます。
let str!: string; // 明確な割り当てアサーション function initStr() { str = 'foo'; } initStr(); console.log(str); // OK(コンパイルエラーにならない)
const value: unknown = 10; let int!: number; // 明確な割り当てアサーション if(typeof value === 'number'){ int = value; } console.log(int); // OK
この場合、変数は初期化されていることが確実なので、明確な割り当てアサーションを使う代わりに、変数を参照するコードに非 null アサーションを指定することでもコンパイルエラーを回避できます。
let str: string; function initStr() { str = 'foo'; } initStr(); // 非 null アサーション console.log(str!); // OK
const value: unknown = 10; let int: number; if(typeof value === 'number'){ int = value; } // 非 null アサーション console.log(int!); // OK
const アサーション as const
const アサーション(as const)は TypeScript の型アサーションの一つで変数やリテラルに、より具体的で制約の強い厳密な型を指定するための(安全な)機能です。
as const を使用することで、変数やリテラルが持つ具体的な値に基づいて型が推論され、変数やプロパティが変更されない(固定される)ことを TypeScript に伝えることができます。
const アサーションの構文は式 as const
で、式の部分の型推論に対して以下の効果を及ぼします。
- 文字列・数値・BigInt・真偽値リテラル型を widening しない
- オブジェクトリテラルから推論されるオブジェクトの型はすべてのプロパティが readonly になる
- 配列リテラルの推論結果が配列型ではなくタプル型になり、推論されるタプル型も readonly になる
- テンプレート文字列リテラルの推論結果が string 型ではなくテンプレートリテラル型になる
as const を使用することで、その式の各種リテラルを変更不可なものとして表現する(TypeScript に伝える)ことができます。
以下の場合、TypeScript は color の型を string として推論します。もし color が常に同じ値であることがわかっている場合、as const を使ってその値を固定することができます。
let color = "red"; // string 型
as const により、color の型は "red" という具体的な文字列型(リテラル型)になり、将来的に color に別の値を代入することができないことを示しています。もし color = "blue" としようとすると、TypeScript はエラーを出します。
let color = "red" as const; // "red" 型(widening されないリテラル型) color = 'blue'; // コンパイルエラー // Type '"blue"' is not assignable to type '"red"'
また、as const により、color の型は widening されないリテラル型となるため、color を代入した変数 color2 もリテラル型となり、color2 にも "red" 以外の別の値を代入することができなくなります。
let color = "red" as const; // widening されない "red" 型 let color2 = color; // "red" 型 color2 = 'blue'; // コンパイルエラー // Type '"blue"' is not assignable to type '"red"'
let color = "red" as const; は以下のように const と型注釈を使って定義したのと同じことになります。
const color: "red" = "red";
以下はどちらも3つの文字列の要素の配列ですが、fruits は型推論により string[] 型と推論されますが、as const を指定した fruitsConst は読み取り専用の3要素のタプル型となり、3つの要素は widening されない文字列リテラル型と推論されます。
const fruits = ["apple", "banana", "orange"]; // fruits は string[] 型 const fruitsConst = ["apple", "banana", "orange"] as const; // fruitsConst は readonly ["apple", "banana", "orange"] 型(読み取り専用のタプル型)
このため、as const を指定した配列 fruitsConst の要素を書き換えたり、追加・削除するとコンパイルエラーとなります(変更ができなくなります)。
const fruits = ["apple", "banana", "orange"]; fruits[0] = 'melon'; // OK fruits.push("grape"); // OK fruits.shift(); // OK const fruitsConst = ["apple", "banana", "orange"] as const; fruitsConst[0] = 'melon'; // コンパイルエラー fruitsConst.push("grape"); // コンパイルエラー fruitsConst.shift(); // コンパイルエラー
以下はオブジェクトに as const を指定する場合の例です。
as const を指定しない person の型は { name: string; age: number; } として推論されますが、as const を使うことで、プロパティの値が変更できない型を持たせることができます。
const person = { name: "John", age: 30, }; // { name: string; age: number; } const personConst = { name: "John", age: 30, } as const; // { readonly name: "John"; readonly age: 30;}
as const により personConst の型は { readonly name: "John"; readonly age: 30; } として推論され、personConst.name = "Jane";のような変更ができなくなります。
const person = { name: "John", age: 30, }; person.name = "Jane"; // OK const person2 = person; // OK person2.name = 'Foo'; // OK const personConst = { name: "John", age: 30, } as const; personConst.name = "Jane"; // コンパイルエラー(読み取り専用) const personConst2 = personConst; // OK personConst2.name = 'Foo'; // コンパイルエラー(widening されない)
以下はテンプレート文字列リテラルの例です。hello は as const を指定しているので、テンプレートリテラル型と推論されます(as const がなければ string 型になります)。
let to = ""; const hello = `Hello, ${to}!` as const; // hello は `Hello, ${string}!`(テンプレートリテラル型) let hello2 = hello; hello2 = 'Hello, world!'; // OK hello2 = 'Hello!';; // コンパイルエラー
値から型を作成
Indexed Access 型(Lookup 型)と typeof キーワード、as const を組み合わせると、配列の要素を表す型(リテラル型のユニオン型)を作成することができます。
以下の Fruit 型は配列 fruits の要素を表す "apple" | "banana" | "orange"(リテラル型のユニオン型)になります(配列 fruits の要素の値から型を作成しています)。
添え字には数値型を表す number を指定することで、各要素の型を取得しています。
const fruits = ['apple', 'banana', 'orange'] as const; type Fruit = (typeof fruits)[number]; // Fruit は "apple" | "banana" | "orange" let myFruit: Fruit = 'apple'; // OK myFruit = 'banana'; // OK myFruit = 'orange'; // OK myFruit = 'melon'; // コンパイルエラー
以下はオブジェクトのプロパティの値から型を作成する例です。
添字部分の keyof typeof foo は "name" | "age" になり、FooProps は "Foo" | 23 になります。
const foo = { name: 'Foo', age: 23 } as const; type FooProps = typeof foo[keyof typeof foo]; // FooProps は "Foo" | 23 let fooProp: FooProps = 'Foo'; // OK fooProp = 23; // OK fooProp = 'Bar'; // コンパイルエラー
any 型
any 型は特別な型で、どのような型でも代入を許す型で、何を代入してもエコンパイルラーになりません。
これは any 型に対して TypeScript が型チェックを全く行わないことを意味し、実行するとエラーになるようなコードでも、コンパイラーはその問題を指摘しないためランタイムエラーの危険性が大きくなります。
例えば、型注釈で any にした値を使うと、以下のような型に矛盾のあるコードでもコンパイラーはその問題を指摘せず、コンパイルエラーを出しません。
また、any 型の引数を持つ関数では、関数内で引数に対する型チェックが一切行われず、存在しない可能性のあるプロパティやメソッドへのアクセスなどもコンパイルエラーになりません。
let value: any; value = 1; // OK value = "abc"; // OK value = { name: "foo" }; // OK console.log(value.age); // undefined(コンパイルエラーにならない) console.log(value.age.toFixed()); // 実行時エラー(コンパイルエラーにならない) function doAny(obj: any) { // 存在しない可能性のあるプロパティへのアクセス console.log(obj.name.length); // 存在しない可能性のあるメソッドの呼び出し obj.myMethod(); // 数値でない可能性のある値での数値演算 return obj * 100; }
このように any 型を使用すると、TypeScript の型システムから一時的に逃れることができますが、同時に型安全性が犠牲になります。any 型は型情報を失うことになり、型に関するコンパイラの利点が薄れます。
このため、any 型を使う場合はそのコードの安全性の責任をプログラマーが負うことになります。
可能な限り、any 型を使わずに、より具体的な型を使用することが望ましいです。
unknown 型
unknown 型は、型が何かわからないときに使う型で、unknown 型にはどのような値も代入できます。
任意の値や型を代入することができるという点では any 型と似ていますが、any 型の値は直接使用できるのに対し、unknown 型の値は使用する前にその型を確認する必要があります。
any 型と同様、unknown 型にもどのような値も代入できます。
let value: unknown; value = 1; // OK value = "abc"; // OK value = { name: "foo" }; // OK
しかし、any 型はどのような型の変数にも代入できますが、unknown 型の値はどのような型かわならないため、具体的な型へ代入できません。
const value1: any = 10; const num1: number = value1; // OK const value2: unknown = 10; const num2: number = value2; // コンパイルエラー // Type 'unknown' is not assignable to type 'number'.
unknown 型の値を具体的な型へ代入する(unknown 型の値を使用する)には、その前に型を確認する必要があります(num2! の ! は明確な割り当てアサーションです)。
const value2: unknown = 10; let num2! : number; if(typeof value2 === 'number'){ num2 = value2; // OK } console.log(num2); // 10
また、unknown 型の値はどのようなプロパティやメソッドを持っているわからないため、unknown 型の値へのプロパティアクセスやメソッドの呼び出しもコンパイルエラーになります。
const value: unknown = 10; console.log(value.toFixed()); // コンパイルエラー // 'value' is of type 'unknown'. const obj: unknown = { name: "foo" }; console.log(obj.name); // コンパイルエラー // 'obj' is of type 'unknown'.
この場合も、unknown 型の値のプロパティアクセスやメソッドの呼び出し(unknown 型の値を使用する)前に型を確認する必要があります。
const value: unknown = 10; // value が number 型であることを確認 if (typeof value === "number") { // このブロックでは value は number 型として扱える console.log(value.toFixed()); // OK } const obj: unknown = { name: "foo" }; // obj がオブジェクトで nullish ではなく name プロパティを持っていることを確認 if(typeof obj === "object" && obj != null && 'name' in obj) { console.log(obj.name); // OK }
unknown 型と絞り込み
前述の例のように、unknown 型の値には何が入っているのかわからないため、そのままでは使えず、その値の型を確認する必要があります。
そのため、unknown 型の値を使用するには基本的にはユニオン型同様、型の絞り込みを行います。
typeof を使った絞り込み
以下は typeof を使って unknown 型の値が string 型の場合と number 型の場合に絞り込む例です。
let value: unknown; value = "hello"; if (typeof value === "string") { // ここでは value は string 型に絞り込まれる console.log(value.toUpperCase()); //"HELLO" } else { // ここでは value は string 型以外 console.log("value is not a string"); } value = 123; if (typeof value === "number") { // ここでは value は number 型に絞り込まれる console.log(value * 10); // 1230 } else { // ここでは value は number 型以外 console.log("value is not a number"); }
以下は unknown 型の値を引数に受け取る関数の例で、typeof による絞り込みを switch 文で使っています。この場合、break 文を省略すると値はユニオン型になっていくためコンパイルエラーになります。
function useUnknownValue(value: unknown) { switch (typeof value) { case 'string': // ここでは value は string 型に絞り込まれる console.log(value.toUpperCase()); break; case 'number': // ここでは value は number 型に絞り込まれる console.log(value * 10); break; case 'boolean': // ここでは value は boolean 型に絞り込まれる console.log(value ? 'yes': 'no'); break; default: // 上記以外 console.log("value is not a string nor number nor boolean"); } } let value: unknown; value = "hello"; useUnknownValue(value); // HELLO value = 123; useUnknownValue(value); // 1230 value = true; useUnknownValue(value); // yes useUnknownValue(null); //value is not a string nor number nor boolean
instanceof を使った絞り込み
以下では instanceof を使って unknown 型の値 data が User クラスのインスタンスかどうかを調べてから使用しています。
class User { name: string; age?: number; constructor(name: string, age?: number) { this.name = name; this.age = age; } } let data: unknown; data = new User("John Doe"); if (data instanceof User) { // ここでは data は User のインスタンス data.age = 23; // age プロパティにアクセスできる console.log(data.name, data.age); // John Doe 23 } else { console.log("Data is not an instance of User"); }
配列型に絞り込む
unknown 型を配列型に絞り込む場合は Array.isArray() や instanceof を使って配列かどうかを調べることができます。また、配列の全ての要素の型は every() メソッドと typeof を使ってチェックできます。
以下では unknown 型の data が配列かどうかを調べてから使用しています。以下の配列かどうかの判定は Array.isArray(data) または data instanceof Array とすることができます。
every() メソッドは配列のすべての要素を指定したコールバック関数でテストして、すべて true を返した場合は true を、そうでなければ false を返します。以下ではコールバック関数で全ての要素が number 型かどうかをテストしています。
但し、every() メソッドでの確認をしても、TypeScript コンパイラは要素を any 型と判断するようなので、念のため、各要素を使用する際に typeof val === "number" で更に絞り込んでいます。この場合、この絞り込みをしなくてもコンパイルエラーにはなりませんが、オブジェクトの配列を絞り込む場合などでは型アサーションが必要になるかもしれません。
let data: unknown; data = [1, 2, 3, 4, 5]; // data が配列型かどうかを確認 if(Array.isArray(data)) { // ここでは data は any 型の配列(any[]) if(data.every((elem) => typeof elem === "number")){ // ここでは data は number 型の配列(但しコンパイラは any[] として扱っている) let sum = 0; data.forEach((val) => { if(typeof val === "number") { // ここでは val は number 型 sum += val; } }) console.log(`sum is ${sum}`); // sum is 15 }else{ // ここでは data は number 型以外のの配列 console.log('data is not number array.') } }else{ // ここでは data は配列ではない console.log('data is not array.') }
以下は unknown 型の値が number 型の配列かどうかを調べるユーザー定義型ガード isNumberArray を定義して使う例です。
//ユーザー定義型ガード function isNumberArray(value: unknown): value is number[] { if (!Array.isArray(value)) { return false; } return value.every((elem) => typeof elem === "number"); } if (isNumberArray(data)) { // ここでは data は number 型の配列 let sum = 0; data.forEach((val) => { if (typeof val === "number") { sum += val; } }); console.log(`sum is ${sum}`); // sum is 15 }else { // ここでは data は number 型以外のの配列 console.log("data is not number array."); }
オブジェクトの型に絞り込む
unknown 型をオブジェクトの型に絞り込むには、ユーザー定義型ガードを使います。
以下は unknown 型の値が User 型のオブジェクトかどうかを調べるユーザー定義型ガード isUser を定義して、unknown 型の値が User 型のオブジェクトであることを確認して値を使用しています。
type User = { name: string; id: number; age?: number; // オプショナルなプロパティ } // ユーザー定義型ガード function isUser(value: unknown): value is User { // value が object でない場合や null または undefined である場合は User ではない if (typeof value !== "object" || value == null) { return false; } // value の型を User 型と同じプロパティを持つ型(値は unknown)に変換 const user = value as Record<keyof User, unknown>; // 各プロパティの型をチェック if ( typeof user.name !== "string" || typeof user.id !== "number" || user.age && typeof user.age !== "number" ) { return false; } return true; } let data: unknown; data = { name: 'Jane Doe', id: 1, age: 22, } // ユーザー定義型ガードで unknown 型の data が User 型であることを確認 if(isUser(data)) { // ここでは data は User 型 console.log(data.name); // Jane Doe data.age = 34; console.log(data); // {name: 'Jane Doe', id: 1, age: 34} }
関連項目:引数の型を unknown にする
上記のユーザー定義型ガード isUser() は以下のように記述することもできます。
各プロパティの型のチェックではそれぞれのプロパティの型を判定して && で繋げているので、全てを満たせば true が返ります。また、age はオプショナルなので number または undefined になります。
function isUser(value: unknown): value is User { if (typeof value !== "object" || value == null) { return false; } const user = value as Record<keyof User, unknown>; return ( // 各プロパティの型をチェック typeof user.name === "string" && typeof user.id === "number" && (typeof user.age === "number" || typeof user.age === "undefined") ); }
object 型
object 型はオブジェクトだけが代入できる型です。プリミティブ以外の全てを表す型(プリミティブ型が代入できない型)とも言えます。
let obj: object; // object 型 obj = { name: 'foo' }; // OK obj = [1, 2, 3]; // OK(配列はオブジェクト) obj = /a-z/; // OK(正規表現はオブジェクト) obj = 7; // コンパイルエラー(プリミティブは NG) // Type 'number' is not assignable to type 'object' obj = false; // コンパイルエラー(プリミティブは NG) // Type 'boolean' is not assignable to type 'objec obj = "string"; // コンパイルエラー(プリミティブは NG) // Type 'string' is not assignable to type 'object obj = null; // コンパイルエラー(プリミティブは NG) // Type 'null' is not assignable to type 'object'. obj = undefined; // コンパイルエラー(プリミティブは NG) // Type 'undefined' is not assignable to type 'object'.
{} 型
空のオブジェクト型(プロパティを持たないオブジェクトを表す型)の {} 型には null と undefined 以外の全ての値が代入可能です。
let obj: {}; // {} 型 obj = { name: 'foo' }; // OK obj = [1, 2, 3]; // OK obj = /a-z/; // OK obj = 7; // OK obj = false; // OK obj = "string"; // OK obj = null; // コンパイルエラー(null は NG) // Type 'null' is not assignable to type '{}' obj = undefined; // コンパイルエラー(undefined は NG) // Type 'undefined' is not assignable to type '{}'
never 型
never 型は「値が絶対に発生しない」ことを表す、または「値を持たない」ことを意味する TypeScript の特別な型です。
そのため never 型には何も代入できません。
const foo: never = 'foo'; // コンパイルエラー //Type 'string' is not assignable to type 'never'. const bar: never = 1; // コンパイルエラー // Type 'number' is not assignable to type 'never'.
型アサーション as を使って騙せば、唯一 never 型は代入できます。
const foo: never = 'Foo' as never;
しかし、never 型はどんな型にも代入可能です(never 型は全ての型の部分型です)。
never 型の値を取得した場合、never 型は任意の型に代入することができます。
以下の関数 printNever() は引数に never 型の値を受け取ります。never 型の値は通常、存在しませんが、以下では as を使って never 型の値を作成して呼び出しています。
function printNever(val: never) { // val(never 型)はどんな型にも代入可能 const num: number = val; const str: string = val; const bool: boolean = val; const obj: object = val; console.log(num, str, bool, obj); } const foo = 'Foo' as never; // as を使って作成した never 型の値を引数に渡して実行 printNever(foo);
never 型はユニオン型では削除される
never 型は値を持たない型を表すため、型には値が含まれません。そのため、ユニオン型では never 型は削除されます。
type Never1 = string | number | never; // string | number type Never2 = any | never; // any type StrNum = string | number; type BoolNumNever = boolean | number | never; // 以下は never が削除され、 string | number | boolean type StrNumBool = StrNum | BoolNumNever;
以下の場合、MyObj3 は number 型のみになります。
MyObj2[keyof MyObj]
は keyof を使った Lookup 型です。
Lookup 型で keyof を使用すると、keyof が返すプロパティの値の型をユニオン型で取得できます。
この場合、keyof MyObj
は 'name' | 'age' | 'isActive'
という文字列リテラル型のユニオン型で、MyObj2[keyof MyObj]
は 'never' | 'number' | 'never'
となりますが、ユニオン型では never 型は削除されるため、最終的に MyObj3 は number 型のみを持つことになります。
type MyObj = { name: string; age: number; isActive: boolean; }; type MyObj2 = { name: never; age: number; isActive: never; }; type MyObj3 = MyObj2[keyof MyObj]; // number のみを持つ // MyObj2[keyof MyObj] は MyObj2['name'|'age'|'isActive'] と同じ
関連項目:Exclude<T, U>
インターセクション型では他の型をオーバーライド
逆にインターセクション型では、never 型は他の型をオーバーライドし、最終的に never 型を返します。
type Never3 = number & never; // never type Never4 = any & never; // never
関数の戻り値の型として使う
通常 never 型は関数の戻り値の型として使われます。
never 型は、ある関数が例外を投げたり無限ループに入ったりして、決して正常に終了しないことを示す戻り値の型として使われます。
例えば、never 型は関数が例外を投げる場合に使用されます。これにより、その関数が絶対に正常に終了しないことが示されます。
以下の関数 throwError は常に例外を投げ、決して正常に終了しないため、つまり、戻り値を得ることが不可能なため、戻り値の型は never になります。
function throwError(message: string): never { throw new Error(message); } try { throwError("something wrong"); } catch (e) { if (e instanceof Error) { console.error(e.message); } }
never 型は型ガードで使用されることがあります。
以下は typeof ガードを使った絞り込みの例ですが、関数 processValue 内ではユニオン型の引数の型の string 型と number 型に対する処理を行っています。
function assertNever(value: never): never { throw new Error("Unexpected value: " + value); } function processValue(val: string | number ) { if (typeof val === "string") { // val は string 型 console.log(val.toUpperCase()); } else if (typeof val === "number") { // val は number 型 console.log(val.toFixed(2)); } else { // val は never 型 (ここに来るべきではない) assertNever(val); } }
上記の関数 assertNever は val が never 型になる可能性がある場合には、それが起きないように(コンパイラに対してある条件の網羅性を確認する手段として)使用しています。
もし、関数の引数のユニオン型を string | number | boolean に変更した場合は、boolean 型に対する処理が記述されていないので、14行目の assertNever(val) の val でコンパイルエラーが発生します。
never を使った網羅性チェック
網羅性チェック(Exhaustiveness Checking)は、条件分岐や型のパターンを使った処理において、全ての可能なケースが網羅されているかを確認する仕組みです。これにより、予期せぬケースの漏れを防ぐことができます。
TypeScript においては、特に never 型と組み合わせて使われることがあります。
以下の関数 getFruitColor は Fruit 型の引数を受け取り、それに対応する果物の色を返します。
default ブロックの13行目で exhaustiveCheck: never = fruit; という行で fruit が予期しない値の場合、never 型の変数にその値を割り当てています。
もし将来 Fruit 型の引数が追加された場合にこの部分でエラーを検出し、開発者に通知します。
例えば、Fruit 型を "Apple" | "Orange" | "Banana"| "Melon" に変更すると、13行目の変数 で「Type 'string' is not assignable to type 'never'.」というコンパイルエラーが発生します。
これにより、将来的な変更や新しい値が追加された際に、コードの網羅性が担保され、バグを防ぐことができます。
type Fruit = "Apple" | "Orange" | "Banana"; function getFruitColor(fruit: Fruit): string { switch (fruit) { case "Apple": return "Red"; case "Orange": return "Orange"; case "Banana": return "Yellow"; default: // TypeScriptはここで網羅性がないと検知してくれる const exhaustiveCheck: never = fruit; return exhaustiveCheck; } }
または、以下のように網羅性チェック用の例外クラスを定義して、default ブロックで投げるようにすると網羅性が満たされていない場合、ExhaustiveError() の引数部分でコンパイルエラーが発生します。
網羅性チェック用の例外クラスでは、コンストラクタの引数に never 型を取るようにします(これにより、網羅性が満たされていない場合、引数部分でコンパイルエラーが発生します)。
class ExhaustiveError extends Error { // コンストラクタ引数に never 型を取るようにします constructor(value: never, message = `Unsupported type: ${value}`) { super(message); } } type Fruit = "Apple" | "Orange" | "Banana"; function getFruitColor(fruit: Fruit): string { switch (fruit) { case "Apple": return "Red"; case "Orange": return "Orange"; case "Banana": return "Yellow"; default: // TypeScriptはここで網羅性がないと検知してくれる throw new ExhaustiveError(fruit); } }
例えば、Fruit 型を "Apple" | "Orange" | "Banana"| "Melon" に変更すると、20行目の ExhaustiveError(fruit) の引数の部分で「Argument of type 'string' is not assignable to parameter of type 'never'.」というコンパイルエラーが発生します。
ユーザー定義型ガード
TypeScript のユーザー定義型ガードは、特定の型や条件を満たすかどうかを確認するために、開発者が独自の関数を定義する仕組みです。
ユーザー定義型ガードとして定義する関数は真偽値(boolean)を返し、引数名 is 型
という構文の型述語(type predicate)を戻り値の型として指定する必要があります。
これにより、関数が true を返したら、引数名
に与えられた値がis
の後に記述された型
であるということを TypeScript コンパイラに伝えることができます。
以下の関数 isString は引数として受け取った unknown 型の value が string 型であるかどうかを判定するためのユーザー定義型ガードです。
関数の戻り値の型に指定した value is string
の型述語は、この関数が TypeScript コンパイラに対して「この条件が真(true)である場合、value は string 型である」と教えています。
作成したユーザー定義型ガードは、if 文などの条件部分で呼び出すことで、型述語で示したとおりの型の絞り込みが行われます。
// ユーザー定義型ガード function isString(value: unknown): value is string { // value が string 型の場合は true を、そうでなければ false を返す return typeof value === "string"; } const foo: unknown = 'Hello Foo!'; if(isString(foo)) { // ここでは foo は string 型 console.log(foo.length); // 10 }
ユーザー定義型ガードは関数が true を返した場合に、型述語に記述された型の絞り込みが行われる仕組みのため、型述語を省略すると、型ガードとして機能しません。
例えば、以下のように型述語の部分を省略すると、JavaScript としては foo は string なので9行目の出力を実行しますが、TypeScript としては型述語により型を判断しているので、以下の場合、型述語がないため、foo は unknown 型と判定され、コンパイルエラーになります。
// 型述語を省略すると、型ガードとして機能しない function isString(value: unknown) { return typeof value === "string"; } const foo: unknown = 'Hello Foo!'; if(isString(foo)) { console.log(foo.length); // コンパイルエラー // 'foo' is of type 'unknown'. }
また、TypeScript はユーザー定義型ガードの関数の実装が、型述語に記述されている絞り込みの内容にあっているかどうかを確認しません(保証しません)。
TypeScript はユーザー定義型ガードの関数の定義を見て、型の絞り込みを行うわけではありません。
例えば、以下のように型述語で「value は string である」として、実装部分では number の場合に true を返してもエラーにはなりません。
function isString(value: unknown): value is string { // 型述語で示した内容と異なる実装でもエラーにならない return typeof value === "number"; }
つまり、ユーザー定義型ガードは any や as 同様、書いた人が責任を持たなければならない機能で、間違った実装は型の安全性の崩壊につながる可能性が高まります。
オブジェクトの判定
受け取った値が特定のオブジェクトかどうかを判定する方法は色々あります。
以下の isCar という関数は引数として受け取った Car | Bike
のユニオン型の vehicle が Car 型であるかどうかを判定するための型ガードです。
vehicle is Car
の型述語により、TypeScript に対して「この関数の戻り値が true である場合、引数に受け取った vehicle は Car 型である」と教えています。
この例の場合、Car 型は model プロパティを持っていて、Bike 型は持っていないのでその有無を in を使って判定しています。
type Car = { brand: string; model: string; } type Bike = { brand: string; type: string; } // ユーザー定義型ガード function isCar(vehicle: Car | Bike): vehicle is Car { // model プロパティを持っていれば Car 型 return "model" in vehicle; }
以下のような方法でも vehicle が Car 型であるかどうかを判定することができます。型アサーション as を使っているのとわかりにくいので前述の方が良いですが、as のこのような使い方が必要になる場合もあります(必ずしも必要になるわけではありませんが)。
return (vehicle as Car).model !== undefined;
の部分は、型アサーション as を使って vehicle を Car 型に変換し、その後 model プロパティが undefined でないかどうかを確認しています。
これは vehicle は Car | Bike なので、model プロパティを持っていない可能性があるため、Car 型に変換しないと model プロパティにアクセスできないためです。
// ユーザー定義型ガード function isCar(vehicle: Car | Bike): vehicle is Car { return (vehicle as Car).model !== undefined; }
上記の型ガードを使って、次のように型を絞り込むことができます。displayVehicleInfo 関数内では isCar(vehicle) により vehicle が Car 型または Bike 型として、適切なプロパティにアクセスできます。
function displayVehicleInfo(vehicle: Car | Bike) { if (isCar(vehicle)) { // ここでは vehicle が Car 型として扱われる console.log(`Car - Brand: ${vehicle.brand}, Model: ${vehicle.model}`); } else { // ここでは vehicle が Bike 型として扱われる console.log(`Bike - Brand: ${vehicle.brand}, Type: ${vehicle.type}`); } }
以下の関数 isUser は、与えられた値が User 型であるかどうか(正確には User 型の条件を満たすかどうか)を判定するユーザー定義型ガードです。
まず、value がプロパティにアクセスできる値であることをチェックしています。
if(value == null) としているのは value が nul と undefined 以外であればプロパティへのアクセスがランタイムエラーになることはないので、最初にその可能性を排除しています。
そして各プロパティの型を判定し、その結果の真偽値を返しています。もし全ての判定が true であれば、結果として true が返され、引数の値は User 型であると TypeScript に対して教えることになります。
このユーザー定義型ガードでは、引数 value の型をどんな値でも受け入れる any 型としているので、どのようなプロパティにアクセスしてもコンパイルエラーにらずに、各プロパティの型を判定できます。
但し、これは関数の中で引数 value に対する型チェックが全く行われないことを意味するので、ランタイムエラーにならないように気をつける必要があります(関連項目:引数の型を unknown にする)。
type User = { type: "User"; name: string; id: number; age?: number; } // ユーザー定義型ガード function isUser(value: any): value is User { // null や undefined の場合はプロパティアクセスできないので false を返して終了 if(value == null) return false; // 各プロパティの型を判定 return ( value.type === "User" && typeof value.name === "string" && typeof value.id === "number" && (typeof value.age === "number" || typeof value.age === "undefined") ); } function printUserInfo(user: unknown) { // ユーザー定義型ガードを使用 if(isUser(user)) { // ここでは user は User 型 console.log(user.type, user.name, user.id, user.age? user.age: 'no age'); }else{ console.log(`${user} is not User Type.`) } } const foo = { type: "User", name: 'Foo', id: 1, age: 22 } printUserInfo(foo); // User Foo 1 22 printUserInfo("hello"); // hello is not User Type
上記のユーザー定義型ガード isUser() は以下のように記述しても同じことです。
// ユーザー定義型ガード function isUser(value: any): value is User { if (value == null) return false; // 各プロパティの型をチェック if ( value.type !== "User" || typeof value.name !== "string" || typeof value.id !== "number" || value.age && typeof value.age !== "number" ) { return false; } return true; }
オブジェクトの配列かどうか
配列の要素のすべてが特定のオブジェクトかどうかを判定することもできます。
以下は配列が特定のオブジェクト(以下の場合は前述の User)の配列かどうかを判定する例です。
配列が User 型の配列かどうかを Array.isArray() と、User 型であるかどうかを判定するユーザー定義型ガード isUser を配列のメソッド every() に渡して判定しています。
type User = { type: "User"; name: string; id: number; age?: number; } // User 型であるかどうかを判定するユーザー定義型ガード function isUser(value: any): value is User { if(value == null) return false; return ( value.type === "User" && typeof value.name === "string" && typeof value.id === "number" && (typeof value.age === "number" || typeof value.age === "undefined") ); } // 受け取った値が User 型であればそのプロパティを出力する関数 function printUserInfo(user: unknown) { if(isUser(user)) { console.log(user.type, user.name, user.id, user.age? user.age: 'no age'); }else{ console.log(`${user} is not User Type.`) } } const foo = { type: "User", name: 'Foo', id: 1, age: 22 } const bar = { type: "User", name: "Bar", id: 2 } const foobar = [foo, bar]; // Array.isArray() と every() にユーザー定義型ガード isUser を渡して判定 if (Array.isArray(foobar) && foobar.every(isUser)) { // 配列の要素のすべてが User 型の場合にのみ、以下を実行 foobar.forEach((user) => printUserInfo(user)); }
asserts 型述語
引数名 is 型
という形式の型述語の他に、asserts 引数名 is 型
という型述語もあります。
この形式の型述語を関数の戻り値の型に指定した場合、関数が例外を投げずに終了すれば「引数名は型である」という意味になります。
以下の 関数 assertIsString は、「この関数が例外を発生させなければ引数 value は string 型である」という意味になります。
以下の関数は、引数 value の型をチェックし、それが string でなければ例外を発生させるので、この関数が正常終了するのは引数 value が文字列だった場合だけになります。
この例では unknown 型の変数 foo が assertIsString 関数呼び出し後は string 型に変化しています。
// asserts 型述語を使ったユーザー定義型ガード function assertIsString(value: unknown): asserts value is string { // 引数 value の型を typeof でチェックし、string でなければ例外を発生させる if (typeof value !== "string") { throw new Error("value is not string"); } } // foo は unknown 型 const foo: unknown = 'Hello Foo!'; assertIsString(foo); // assertIsString() が正常終了すれば(例外が発生しなければ)、ここでは foo は string 型 console.log(foo.length);
以下は先述の例を asserts 型述語で書き換えたものです。
引数に受け取った value が User 型の条件を満たさない場合は false を返す代わりに throw 文で例外を投げています。
このユーザー定義型ガード assertIsUser を使う関数 printUserInfo では、assertIsUser を呼び出して実行することで引数 value が User 型であることを確認しています。
assertIsUser の呼び出し以降の処理は、例外が発生しない場合にのみ実行されるので、value の型は unknown から User に変わっています。
// asserts 型述語を使ったユーザー定義型ガード function assertIsUser(value: any): asserts value is User { if(value == null){ throw new Error('value is null or undefined.'); } if( value.type !== "User" || typeof value.name !== "string" || typeof value.id !== "number" || value.age && typeof value.age !== "number" ) { throw new Error('value is not User.') } } function printUserInfo(value: unknown) { assertIsUser(value); // 以降は value は User 型 console.log(value.name, value.id); if(value.age) { console.log(value.age); } }
以下は assertIsUser で例外が発生した場合は、try catch 文で例外を捕捉する例です。
コンパイラオプションで strict が有効の場合、 catch (error) の error は unknown 型になるので型を確認してから message プロパティにアクセスしています(catch に渡される変数の型)。
function printUserInfo(value: unknown) { try { assertIsUser(value); // 以降は value は User 型 console.log(value.name, value.id); if (value.age) { console.log(value.age); } } catch (error) { // error は unknown 型なので型の絞り込みが必要 if (error instanceof Error) { console.error(error.message); } } }
引数の型を unknown にする
ユーザー定義型ガードでどんな値でも受け入れるには、引数の型を any または unknown にすることになります。先述の User 型判定のユーザー定義型ガードでは以下のように引数の型を any にしました。
type User = { type: "User"; name: string; id: number; age?: number; } // any 型の引数を受け取るユーザー定義型ガード function isUser(value: any): value is User { if(value == null) return false; return ( value.type === "User" && typeof value.name === "string" && typeof value.id === "number" && (typeof value.age === "number" || typeof value.age === "undefined") ); }
引数の型を any にした場合、関数の中で引数 value に対する型チェックが全く行われないので、どちらかと言えば、unknown にした方が型安全性の面では良いようです。
但し、引数の型を unknown にする場合、any と同様の危険性がある型アサーション as を使うことになります(as を使わない方法もあります)。
以下は、上記のユーザー定義型ガードを unknown 型の引数を受け取るように書き換えたものです。
この場合、引数 value の型を as を使って Record<keyof User, unknown>
型に変更しています。
これは value が unknown 型のままでは type や name などのプロパティアクセスは許可されず、コンパイルエラーになってしまうためです。Record<keyof User, unknown>
型は User 型と同じプロパティを持つ型なので、プロパティにアクセスできるようになります。
// unknown 型の引数を受け取るユーザー定義型ガード function isUser(value: unknown): value is User { if(value == null) return false; const user = value as Record<keyof User, unknown>; //型アサーション // または const user = value as User; // 各プロパティの型を判定 return ( user.type === "User" && typeof user.name === "string" && typeof user.id === "number" && (typeof user.age === "number" || typeof user.age === "undefined") ); }
Record<Keys, Type> はプロパティのキーと値からオブジェクト型を作るユーティリティ型で、プロパティのキーが Keys で、値の型が Type であるオブジェクトの型を作成します。
Record<keyof User, unknown>
という型はプロパティのキーが keyof User、プロパティの値の型が unknown である以下のような型になります。
type UserUnknown = Record<keyof User, unknown>; // 上記は以下のような型になります type UserUnknown = { type: unknown; name: unknown; id: unknown; age: unknown; }
as を使わない場合
上記のようにユーザー定義型ガードで unknown 型の引数に対してはプロパティアクセスができないため型アサーションを使用しますが、別途「引数が nullish でなければ、どんなプロパティ名にアクセスしても unknown 型が得られる型である」というようなユーザー定義型ガードを定義する方法もあります。
以下は、値が null や undefined でない場合、その型はプロパティアクセスすると unknown 型の値が得られることを宣言するユーザー定義型ガード isNotNullish を別途定義して利用する例です。
isNotNullish は value が null や undefined でない場合、ユーティリティ型の Record<T,K> を使って value の型を Record<string, unknown>
型(文字列のプロパティ名と unknown 型の値を持つオブジェクト)に絞り込みます。
// nullish でなければ unknown 型の値のプロパティを持つと判定するユーザー定義型ガード function isNotNullish(value: unknown): value is Record<string, unknown> { return value != null; } // unknown 型の引数を受け取るユーザー定義型ガード function isUser(value: unknown): value is User { //上記で定義したユーザー定義型ガードで nullish でなく、プロパティアクセスできるかを判定 if (!isNotNullish(value)) { return false; } // ここでは value は Record<string, unknown>型(プロパティアクセスが可能) // 各プロパティの型を判定 return ( value.type === "User" && typeof value.name === "string" && typeof value.id === "number" && (typeof value.age === "number" || typeof value.age === "undefined") ); }
Record<string, unknown>
はインデックスシグネチャを使って { [x: string]: unknown; }
としても同じです。
type NotNullish = Record<string, unknown> // 上記は以下のような型になります type NotNullish = { [x: string]: unknown; }
以下は上記のユーザー定義型ガードをジェネリクスを使って、アクセスするプロパティを T 型のプロパティ名とするように書き換えたものです。
// nullishでなければT型のプロパティ名とunknown型の値を持つと判定するユーザー定義型ガード function isNotNullish<T>(value: unknown): value is Record<keyof T, unknown> { return value != null; } function isUser(value: unknown): value is User { if (!isNotNullish<User>(value)) { return false; } // value は Record<keyof User,unknown>型(User 型のプロパティ名にアクセスが可能) return ( value.type === "User" && typeof value.name === "string" && typeof value.id === "number" && (typeof value.age === "number" || typeof value.age === "undefined") ); }
可変長タプル型
可変長タプル型(variadic tuple type)はタプル型の中に ...T
(T は配列型)というスプレッド構文のような要素(残余要素 rest elemets)を含んだ型です。
可変長タプル型は、以下のように ...配列型
を含んだ型で、...配列型
はその部分にその配列型の要素が任意の数だけ(0個以上)入ることを表します。
type StrNums = [string, ...number[]]; // 可変長タプル型 // 以下は OK const arr1: StrNums = ['foo', 1, 2, 3]; const arr2: StrNums = ['foo']; // 以下はコンパイルエラー const arr3: StrNums = [1, 'foo']; // Type 'number' is not assignable to type 'string'. const arr4: StrNums = ['foo', 'bar', 1]; // Type '[string, string, number]' is not assignable to type 'StrNums'. const arr5: StrNums = []; // Type '[]' is not assignable to type 'StrNums'.
残余要素 ...配列型
をタプル型の最初や途中に含めることもできます。
type StrNumsBool = [string, ...number[], boolean]; // 以下は OK const arr1: StrNumsBool = ['foo', 1, 2, 3, true]; const arr2: StrNumsBool = ['foo', false]; // 以下はコンパイルエラー const arr3: StrNumsBool = [ 'foo', true, 123]; // Type '[string, boolean, number]' is not assignable to type 'StrNumsBool'. const arr4: StrNumsBool = ['foo', 1, false, true]; // Type '[string, number, boolean, true]' is not assignable to type 'StrNumsBool'. const arr5: StrNumsBool = []; // Type '[]' is not assignable to type 'StrNumsBool'.
但し、残余要素 ...配列型
はタプル型の中で1回しか使えません。
// 以下は残余要素を2回使っているのでコンパイルエラー type StrsNumsBool = [...string[], ...number[], boolean]; // A rest element cannot follow another rest element.
また、オプショナルな要素を残余要素 ...配列型
よりも後ろで使うことはできません。
type StrNums = [string?, ...number[]]; // OK type NumsStr = [...number[], string?]; // コンパイルエラー // An optional element cannot follow a rest element.
タプル型の中に別のタプル型や配列を展開
スプレッド構文のように ...
を使って、タプル型や配列を別のタプル型の中に展開することができます。
既存のタプル型から新しいタプル型を作る際に利用することができます。また、...
は複数使うこともできます。
type StrNumStr = [string, number, string]; type NumStrNumStrBool = [number, ...StrNumStr, boolean] const nsnsb: NumStrNumStrBool = [1, 'a', 2, 'b', true]; type StrNumStr2 = [...StrNumStr, ...StrNumStr]; const sns2: StrNumStr2 = ['a',1,'b','c',2,'d'];
型変数を使う場合
可変長タプルの ...型
の構文で型の部分に型変数をを使う場合は、その型変数が extends readonly any[]
という制約を満たす必要があります。
以下の concat は2つのタプル型や配列を結合する関数です。T 型と U 型で表した型引数には extends Arr(extends readonly any[]
)を指定しています。
type Arr = readonly any[]; function concat<T extends Arr, U extends Arr>(arr1: T, arr2: U): [...T, ...U] { return [...arr1, ...arr2]; } const fooBar: [string, string] = ['foo','bar']; const numBool: [number, boolean] = [1, true]; const concatArr = concat(fooBar,numBool); // [string, string, number, boolean] console.log(concatArr); // ['foo', 'bar', 1, true]
Mapped Types
Mapped Types は、オブジェクトの型を変換する(プロパティ型を変更する)ための機能で、既存の型から新しい型を作成する場合などに利用できます。
Mapped Types や Conditional Types を使って型を柔軟に変換することができますが、TypeScript ではよく使われる変換などの操作の型を Utility Types として用意しているので、以下の例で扱っている読み取り専用に変換する型などは、実際は自分で定義する必要はありません。
以下が Mapped Types の基本的な構文です。
type MyMappedType = { [P in K]: T }
P はプロパティ名(キー)を表す型変数です。
K はキー(プロパティ名)の型を表すもので、文字列リテラル型やユニオン型など、プロパティ名になれる型(string | number | symbol の部分型)である必要があります。
多くの場合、K は文字列リテラルのユニオン型(keyof の戻り値など)が使われます。
[P in K]
の部分は K 型(ユニオン型)のプロパティを反復処理することを表しています。
T はプロパティの値の型です。
{[P in K]: T}
は型 K をキーとするプロパティ(P)が、型 T の値を持つオブジェクトを表現します。
また、P や T、K は任意の文字列を使えるので、例えば以下のように記述することができます。
type MyMappedType = { [Property in KeyTypes]: ValueType }
以下の場合、"name" | "id" を key とするプロパティが string 型の値をとるオブジェクトを表現します。
type User = { [key in "name" | "id"]: string; }
"name" | "id" が反復処理され、キーが name のプロパティとキーが id のプロパティの値の型が string 型に設定され、上記は以下のような型を表します(型推論されます)。
type User = { name: string; id: string; }
上記は以下のように、プロパティ名となる型を別途定義して記述しても同じことです。
type NameId = "name" | "id"; // プロパティ名となる型(K) type User = { [key in NameId]: string; };
既存の型から新しい型を作成
既存のオブジェクト型から、同じプロパティを持つ、値の型が異なるオブジェクト型を簡単に作成することができます。例えば、既存の型が以下のようなオブジェクト型の場合、
type ExistingType = { name: string; age: number; };
上記の型をもとに Mapped Types を使って、以下のようなプロパティの値の型が全て boolean 型の新しい型を作成することができます
type NewType = { [Property in keyof ExistingType]: boolean; };
keyof はオブジェクトのすべてのプロパティ名(キー)を文字列リテラル型のユニオン型として返すので、keyof ExistingType
は "name" | "age"
型になり、上記は以下のような型を表します。
ExistingType の各プロパティの値の型が boolean に変換されています。
type NewType = { name: boolean; age: boolean; };
Mapped Types の利点
Mapped Types の利点の1つは、元となる既存の型のプロパティに追加や削除などの変更があっても、Mapped Types で作成した型は、手動で変更しなくても自動的にプロパティの変更が反映されます。
例えば、前述の例の ExistingType にプロパティを追加しても、NewType(Mapped Types 側)の定義は変更する必要がありません。
type ExistingType = { name: string; age: number; id: number; // 追加 }; type NewType = { [Property in keyof ExistingType]: boolean; }; /* type NewType = { name: boolean; age: boolean; id: boolean; // 自動的に追加される } */
ジェネリック型を使う
特定の型を指定せずに、型をパラメータとして取るジェネリック型を使うと再利用しやすくなります。
以下の MyBoolType は前述の NewType をジェネリック型で書き換えたもので、T という型パラメータを受け取ります。
[Property in keyof T]
は、T 型のオブジェクトのプロパティを反復処理するための構文です。これにより、T に指定された既存のオブジェクトの各プロパティに対して型を変更することができます。
使用する際は、T の部分に既存の型(この例では ExistingType)を指定します。
type MyBoolType<T> = { [Property in keyof T]: boolean; }; type ExistingType = { name: string; age: number; }; // 既存の型から新しい型を作成(定義) type NewType = MyBoolType<ExistingType> ; const obj: NewType = { name: true, age: false } // 型を別途定義せずに型注釈に指定 const obj2: MyBoolType<ExistingType> = { name: false, age: true }
変換するプロパティの値の型を表す型引数を追加すれば、プロパティの値の型も指定することができます。
以下の場合、追加した型引数 U にはプロパティの値の型を指定します。
type MyMappedType<T, U> = { [P in keyof T]: U; }; type ExistingType = { name: string; age: number; }; // 既存の型から新しい型を作成(定義) type NewType = MyMappedType<ExistingType, boolean> ; const obj: NewType = { name: true, age: false } // 型を別途定義せずに型注釈に指定 const obj2: MyMappedType<ExistingType, boolean> = { name: false, age: true }
Conditional Types と組み合わせる
Conditional Types を使って条件を組み合わせることで、より柔軟な型を作成することができます。
以下は T 型のオブジェクトのプロパティのキーが "id" 型(文字列リテラル型)であれば、そのプロパティの値の型を number 型に変換したオブジェクト型を作成する例です。
P extends "id" ? number : T[P]
が Conditional Types の部分で、プロパティ(P)が "id" であれば number 型を、そうでなければそのままの型 T[P](Lookup 型)を返します。
type NumberIdType<T> = { [P in keyof T]: P extends "id" ? number : T[P]; }; type ExistingType = { name: string; id: string; age: number; }; type NewType = NumberIdType<ExistingType> ; /* 上記 NewType は以下のような型を表します type NewType = { name: string; id: number; age: number; } */
以下は比較するプロパティ(キー)の型や条件を満たした場合に変更する値の型を型引数として受け取れるように上記の Mapped Type を書き換えたものです。
以下の場合、T 型のオブジェクトのプロパティのキーが U 型の部分型(この場合は同じ文字列リテラル型)であれば、そのプロパティの値の型を V 型にした型を作成します。
P extends U ? V : T[P]
が Conditional Types で、プロパティが U で指定された型またはその部分型であれば V で指定された型を、そうでなければそのままの型 T[P] を返します。
11行目で作成される型 NewType は、既存の型 ExistingType の id プロパティの値の型を string | number に変更した型になります。
type MyMappedType<T, U, V> = { [P in keyof T]: P extends U ? V : T[P]; }; type ExistingType = { name: string; id: string; age: number; }; type NewType = MyMappedType<ExistingType, "id", string | number> ; /* 上記 NewType は以下のような型を表します type NewType = { name: string; id: string | number; age: number; } */
上記の例は、プロパティのキーの型(P)の条件で判定していますが、以下はプロパティの値の型(T[P])の条件により新しい型を作成しています。
以下では、プロパティの値の型が number であれば、string | number に変更した型を作成します。
type MyMappedType<T, U, V> = { [P in keyof T]: T[P] extends U ? V : T[P]; }; type ExistingType = { name: string; id: string; age: number; }; type NewType = MyMappedType<ExistingType, number, string | number> ; /* type NewType = { name: string; id: string; age: string | number; } */
以下は型パラメータ T に指定されたオブジェクトのプロパティで、値が U に指定された型(またはその部分型)のプロパティのキー(文字列リテラル型)を抽出するコードです。
T[P] extends U ? P : never
は、T[P](プロパティの値の型)が U 型の部分型である場合は P を、そうでなければ never を返します。
type FilterProps<T, U> = { [P in keyof T]: T[P] extends U ? P : never; }[keyof T]; type User = { name: string; id: number; age: number; isActive: boolean; }; type NumKeys = FilterProps<User, number>; // "id" | "age" type StrKeys = FilterProps<User, string>; // "name" type NumOrStrKeys = FilterProps<User, number|string>; // "name" | "id" | "age"
3行目の[keyof T]
は { [P in keyof T]: T[P] extends U ? P : never; }
の Lookup 型のプロパティの部分です。例えば、3行目の[keyof T]
を削除すると、NumKeys は以下になります。
type NumKeys = { name: never; id: "id"; age: "age"; isActive: never; }
例えば、上記の NumKeys で NumKeys[keyof User]
とすると、keyof User
は "name" | "id" | "age" | "isActive"
になるので、NumKeys[keyof User]
は "never" | "id" | "age" | "never"
になりますが、ユニオン型の never は削除されるので、 "id" | "age"
になります(never 型)。
- Mapped Types(サバイバルTypeScript)
- Mapped Types(TypeScript Handbook)
- Mastering TypeScript mapped types(LogRocket)
マッピング修飾子(readonly と ?)
Mapped Types では、readonly
(読み取り専用)と?
(オプショナル)の2つの修飾子(Mapping Modifiers)を利用することができます。
-
または +
をプレフィックス(接頭辞)として付けることで、これらの修飾子を削除または追加できます。 プレフィックスを省略した場合は、+
を指定したのと同じことになります。
プロパティが読み取り専用のオブジェクトの型を生成
Mapped Types と readonly
を組み合わせることで、既存の型の全てのプロパティを読み取り専用にしたオブジェクト型を作成できます。
以下は T 型のオブジェクトのプロパティを全て読み取り専用にする Mapped Types の例です。
T[P]
は型 T のプロパティの値の型を表します(Indexed Access 型)。
type CreateReadOnly<T> = { readonly [P in keyof T]: T[P]; // または +readonly [P in keyof T]: T[P]; }; type MyObj = { name: string; age: number; }; type ReadOnlyMyObj = CreateReadOnly<MyObj>;
※ CreateReadOnly<T> と同じ型は Utility Types で Readonly<T> として定義されているので、実際には自分で定義する必要はありません。
上記の ReadOnlyMyObj は各プロパティの前に readonly が追加され、以下のような型を表します。
type ReadOnlyMyObj = { readonly name: string; readonly age: number; }
プレフィックス -
を付けて-readonly
として、既存のプロパティの型から readonly を削除できます。
type CreateMutable<T> = { // - を readonly に付けて readonly を削除 -readonly [P in keyof T]: T[P]; }; type LockedAccount = { readonly id: string; readonly name: string; }; type UnlockedAccount = CreateMutable<LockedAccount>; /* 上記 UnlockedAccount は readonly が削除され、以下のような型を表します type UnlockedAccount = { id: string; name: string; } */
オプショナルなプロパティのオブジェクトの型を生成
Mapped Types と ?
を組み合わせることで、既存の型の全てのプロパティをオプショナルにしたオブジェクト型を作成できます。
type MakeOptional<T> = { [P in keyof T]?: T[P]; }; type MyObj = { name: string; id: string; }; type OtionalMyObj = MakeOptional<MyObj>;
上記の OtionalMyObj は各プロパティの後に ? が追加され、値の方は undefined とのユニオン型になり、以下のような型を表します
type OtionalMyObj = { name?: string | undefined; id?: string | undefined; }
-
を付けて-?
とすることで、既存の型から ? を削除して、全て必須のプロパティにすることができます。
type MakeRequired<T> = { [P in keyof T]-?: T[P]; }; type MaybeUser = { id: string; name?: string; age?: number; }; type User = MakeRequired<MaybeUser>; /* 上記 User は ? が削除され、以下のような型を表します type User = { id: string; name: string; age: number; } */
※ 上記で定義した MakeOptional<T> や MakeRequired<T> と同様の型は Utility Types でそれぞれ Partial<T>、Required<T> として定義されています。
as でキー名を変更
Mapped Types で as
句を使用して、マップされた型のキー(プロパティ名)を書き換えることができます(参考:Key Remapping via as)。
as
句では、テンプレートリテラルタイプなどの機能を利用して、もとのプロパティ名から新しいプロパティ名を作成できます。
以下は既存のオブジェクト型 OriginalType のプロパティ名の前に new_ を付けたプロパティ名のオブジェクト型 NewType を作成する例です。
type OriginalType = { key1: string; key2: number; key3: boolean; }; type NewType = { [P in keyof OriginalType as `new_${P}`]: OriginalType[P]; }; /* 以下のような型を生成します type NewType = { new_key1: string; new_key2: number; new_key3: boolean; } */ const newTypeObj: NewType = { new_key1: "value1", new_key2: 34, new_key3: true, };
ジェネリック型の場合
以下は前述の例を任意の型を受け取れるようにジェネリック型で書き換えたものです。
as
句の P & string
は P が文字列型として扱われることを保証するためのもので、文字列型(string)と型パラメータ(P)をインターセクション(&)で結合しています。
type NewType<T> = { [P in keyof T as `new_${P & string}`]: T[P]; }; type OriginalType = { key1: string; key2: number; key3: boolean; }; const newTypeObj: NewType<OriginalType> = { new_key1: "value1", new_key2: 34, new_key3: true, };
以下の GetterType 型は、オブジェクト型 T の各プロパティに対応する getter メソッドを生成します。これにより、例えば getName や getAge のようなメソッドを持つオブジェクト型が作られます。
Capitalize は、与えられた文字列の最初の文字を大文字に変換する TypeScript の組み込み型です。
Capitalize<string & P>
としているのは、Capitalize は文字列型を期待していますが、型パラメータ P が文字列ではない可能性があるためです。前述の例同様、インターセクション型を使用することで、P が文字列型であることを保証しています。
type GetterType<T> = { [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P] }; type Person = { name: string; age: number; location: string; } type GetterPerson = GetterType<Person>; /*以下のような型を生成します type GetterPerson = { getName: () => string; getAge: () => number; getLocation: () => string; } */
キーを除外
Utility Types の Exclude を使って、特定のプロパティを除外(削除)することができます。以下は型引数 U にプロパティの文字列リテラル型を指定して、そのプロパティを生成する型から除外する例です。
type RemoveField<T, U> = { [P in keyof T as Exclude<P, U>]: T[P] }; interface Circle { kind: "circle"; radius: number; color: string; } type KindlessCircle = RemoveField<Circle, "kind">; /* 以下のような プロパティ kind を除外した型を生成します type KindlessCircle = { radius: number; color: string; } */
Conditional Types
Conditional Types(条件付き型)は、型の条件に基づいて異なる型を選択するための機能です。
T extends U ? X : Y
という構文を持ち(T、U、X、Y は何らかの型)、型レベルで適用される条件演算子のような機能です。この構文は T が U の部分型であれば X を、そうでない場合は Y を返します。
実際の使用では、ジェネリック型と組み合わせて使われることが多く、以下が基本的な構文です。
以下の型 MyType は T が SomeCondition を満たす場合は TypeIfTrue が、そうでない場合は TypeIfFalse が使われる(返される)型です。
type MyType<T> = T extends SomeCondition ? TypeIfTrue : TypeIfFalse;
以下の型 IsString<T> は、渡された型 T が string 型(の部分型)であるかどうかを判定して、string 型であれば真偽値リテラル型の true になり、そうでなければ真偽値リテラル型の false になります。
type IsString<T> = T extends string ? true : false; type T1 = IsString<"Hello">; // true(真偽値リテラル型) type T2 = IsString<123> ; // false(真偽値リテラル型)
Conditional Types は再帰的に使用できます。
以下の Recursive<T> は T が string 型であれば string を、number 型であれば number を、boolean 型であれば boolean を返し返し、それ以外の型に対しては never を返します。
type Recursive<T> = T extends string ? string : (T extends number ? number :(T extends boolean ? boolean : never)); type A = Recursive<"abc">; // string type B = Recursive<123>; // number type C = Recursive<true>; // boolean type D = Recursive<{}>; // never
以下は Mapped Types と組み合わせて、値の型が null のプロパティの型を undefined に変換します。
type MyObj = { prop1: string; prop2: null; prop3: number; }; type RemappedType = { [P in keyof MyObj]: MyObj[P] extends null ? undefined : MyObj[P]; }; /* 以下のような型になります。 type RemappedType = { prop1: string; prop2: undefined; prop3: number; } */
上記はジェネリック型を使うと以下のように記述できます。
type NullToUndefined<T> = { [P in keyof T]: T[P] extends null ? undefined : T[P]; } type RemappedType = NullToUndefined<MyObj>
配列型を要素型にフラット化
Conditional Type を使うと、配列型からその要素の型を簡単に取得することもできます。
以下の Flatten<T> は、T が配列型の場合は配列型を要素型にフラット化して(要素の型を取り出して)返し、配列型以外の場合はそのままにする型です。
T[number]
は Indexed Access 型で、number を使うことで、配列の全体の型が取得できます。
type Flatten<T> = T extends unknown[] ? T[number] : T; // 配列型の場合は要素の型を抽出 type Str = Flatten<string[]>; // string const numArray = [1, 2, 3]; type Num = Flatten<typeof numArray>; // number type Tup = Flatten<[string, number, string]>; // string | number // 配列型でなければ、そのまま type NotArray = Flatten<number>; // number const obj = {foo: 'bar'} type NotArray2 = Flatten<typeof obj>; // {foo: string;} // 要素がオブジェクトの配列 const animals = [ { species: 'cat', name: 'Tama' }, { species: 'dog', name: 'Pochi' }, { species: 'mouse', name: 'Chutaro' } ]; // typeof 型演算子で配列の型を取得してフラット化 type FlattenAnimals = Flatten<typeof animals> /* 上記は以下のような型になります type FlattenAnimals = { species: string; name: string; } */
関連項目:配列要素の型からユニオン型を作成
Conditional Type による制約
Conditional Type の主な利点の1つは、ジェネリック型の実際の型を絞り込めることです。
例えば、型 T から id という名前のプロパティの型を抽出するために以下のようなジェネリック型 ExtractIdType<T> を定義すると、T に id というプロパティが存在するかどうかわからないため、TypeScript はコンパイルエラーを発生させます。
type ExtractIdType<T> = T["id"]; // コンパイルエラー // Type '"id"' cannot be used to index type 'T'.
この場合、実際のジェネリック型 T には id という名前のプロパティが必要なので、以下のように extends キーワードを使って T が{ id: unknown }
の部分型であるという制約をつければエラーになりません。
type ExtractIdType<T extends { id: unknown }> = T["id"]; type StrId = { id: string; } type NumId = { id: number; } type ObjId = { id: { uid: number; } } type StrIdType = ExtractIdType<StrId>; // string type NumIdType = ExtractIdType<NumId>; // number type ObjIdType = ExtractIdType<ObjId>; // {uid: number;}
但し、この場合、T に id プロパティを持たないオブジェクトの型を渡すとコンパイルエラーになります。
type ExtractIdType<T extends { id: unknown }> = T["id"]; type NoId = { name: string; } type NoIdType = ExtractIdType<NoId>; // コンパイルエラー // Type 'NoId' does not satisfy the constraint '{ id: unknown; }'.
Conditional Type を使うと ExtractIdType に任意の型を取り、id プロパティが存在しない場合はデフォルトで never などの任意の型を返すようなことが可能になります。
具体的には、以下のように制約をジェネリック型から Conditional Type に移動させます。
これにより、T extends { id: unknown } の条件を満たせば、TypeScript は T が id プロパティを持つことを認識し、T["id"] を返し、そうでなければ never を返します。
type ExtractIdType<T> = T extends { id: unknown } ? T["id"]: never; type StrId = { id: string; } type NoId = { name: string; } type StrIdType = ExtractIdType<StrId>; // string type NoIdType = ExtractIdType<NoId>; // never(OK)
また、以下のように制約を { id: unknown } から { id: string | number } に変更すれば、id の型が string または number 以外の場合(id プロパティがない場合も含む)に never を返すことができます。
type ExtractIdType<T> = T extends { id: string | number } ? T["id"]: never;
infer キーワード
Conditional Types 内で infer キーワードを使用すると、一時的な型変数を導入してその型を再利用することができるようになります。英語の infer には「推測する」や「推論する」などの意味があります。
infer キーワードは一般的に、次のような形で使われます。
type MyConditionalType<T> = T extends SomeType<infer U> ? U : DefaultType;
上記の例では、T extends SomeType の条件が満たされれば、その型を一時的な型変数 U に代入し、それを条件が満たされ場合の結果として使用し、そうでなければ DefaultType を使用します。
具体的には、先述の例は infer キーワードを使って以下のように書き換えることができます。
id プロパティの型を unknown 型や string | number 型とする代わりに、 infer キーワードを使用して新しい一時的な型変数 U を導入し、条件が満たされた場合、その型を返します。
type ExtractIdType<T> = T extends { id: infer U } ? U : never; type StrId = { id: string; } type NumberId = { id: number; name: string; } type NoId = { name: string; } type StrIdType = ExtractIdType<StrId>; // string type NumIdType = ExtractIdType<NumberId>; // number type NoIdType = ExtractIdType<NoId>; // never
上記の場合、T extends { id: infer U } が満たされれば id プロパティの値の型(U)が使われ、そうでなければ never が使われます。
infer キーワードを使用して一時的な型変数 U を導入することで、id プロパティに具体的な型を指定する必要がなく、また、条件を満たした場合の Lookup 型(T["id"])を指定する必要がなくなります。
また、先述の配列型を要素型にフラット化する例では以下のように記述していました。
type Flatten<T> = T extends unknown[] ? T[number] : T;
上記も infer キーワードを使って以下のように書き換えてもほぼ同じです。また、この例では型変数の名前を Item としていますが、任意の文字列を使用できます。
type Flatten<T> = T extends (infer Item)[] ? Item : T; // Array<型>の記法を使って以下でも同じ type Flatten<T> = T extends Array<infer Item> ? Item : T;
infer キーワードを使う利点は、型変数を一時的に導入し、その型変数を他の部分で再利用できることです。
関数型の戻り値の型を抽出
以下は infer キーワードを使って関数型の戻り値の型を抽出する例です。
type ExtractReturnType<T> = T extends (...args: never[]) => infer R ? R : never; // 関数型 type MyFunction = (x: number) => string; // ExtractReturnType<MyFunction> の型は string になります type ResultType = ExtractReturnType<MyFunction>; // string
(...args: never[]) => infer R
は関数型を表し、infer R
の部分の、R は戻り値の型を表します。
T が関数型である場合、T extends (...args: any[]) => infer R
の条件を満たすので、型変数 R にその戻り値の型が代入されます。つまり、T に渡された型が関数型の場合、その関数型の返り値の型が infer で R に代入されます。
※ ExtractReturnType<T> と同様の型は Utility Types で ReturnType として定義されています。
関数型の引数の型を抽出
infer キーワードを引数に記述すれば、関数型の引数の型を抽出することができます。
type ExtractParamType<T> = T extends (value:infer Arg) => number ? Arg : never; const func = (value: number) => { return value * 2; } type FuncParam = ExtractParamType<typeof func>; // number
引数が複数の場合は、Rest パラメータ(...args)に infer キーワードを記述すれば、配列型で引数の型を抽出できます。Rest パラメータ(残余引数)は可変長なので、引数がない場合などにも対応できます。
type ExtractParamTypes<T> = T extends (...args: infer Args) => unknown ? Args : never; type func1 = (x: number, y: boolean) => void; type func2 = (x: string) => string; type func3= ()=> void; type FuncParam1 = ExtractParamTypes<func1>; // [x: number, y: boolean] type FuncParam2 = ExtractParamTypes<func2>; // [x: string] type FuncParam3 = ExtractParamTypes<func3>; // []
※ ExtractParamTypes<T> とほぼ同じ型は Utility Types で Parameters として定義されています。
Union distribution
Conditional Types の T extends U ? X : Y
において、T が型変数かつユニオン型の場合、Union distribution(ユニオン型分配)という特殊な振る舞いをします。
具体的には、T extends U ? X : Y
の T が T1 | T2
のようなユニオン型の場合、ユニオン型のそれぞれの型に対し、Conditional Type が適用され、(T1 extends U ? X1 : Y1) | (T2 extends U ? X2 : Y2)
のような挙動になります。
例えば、次のようなオブジェクト型 T からプロパティの値の型を抽出する型 ValueTypeOf をユニオン型に対して使うと、string | number ではなく、never になります。
これは keyof が、ユニオン型に対して使用された場合、それらの構成要素の共通のプロパティを取得しますが、A と B のプロパティに共通のプロパティがないため、never を返すためです。
string | number を取得するには、個別に型を抽出してユニオン型を作成する必要があります。
type ValueTypeOf<T> = T[keyof T]; type A = { a: number }; type B = { b: string }; type AorB = A | B; type UnionValueType = ValueTypeOf<AorB>; // never type AorBType = ValueTypeOf<A> | ValueTypeOf<B> // string | number
上記の ValueTypeOf を Conditional Types を使って T extends unknown ? T[keyof T] : never
のように書き換えると、Union distribution によりユニオン型に対して各要素ごとに抽出が行われ、その結果 number | string が得られます。
type ValueTypeOf<T> = T extends unknown ? T[keyof T] : never; type A = { a: number }; type B = { b: string }; type AorB = A | B; type AorBType = ValueTypeOf<AorB>; // string | number // Union distribution により上記は以下と同じこと type AorBType2 = ValueTypeOf<A> | ValueTypeOf<B> // string | number // 以下と同じこと type AorBType3 = (A extends unknown ? number: never) | (B extends unknown ? string: never);
Union distribution の利点は、ユニオン型の各要素に対して同じ操作を一括で行うことができることです。
このように Union distribution は、ユニオン型内の各メンバーに対して同じ処理を行う際に便利で、通常、望ましい動作ですが、この動作を回避するには、extends キーワードの両側を角括弧 []
で囲みます。
// extends キーワードの両側を角括弧 [] で囲むと Union distribution されない type ValueTypeOf<T> = [T] extends [unknown] ? T[keyof T] : never; type A = { a: number }; type B = { b: string }; type AorB = A | B; type AorBType = ValueTypeOf<AorB>; // never
以下は型パラメータ T に受け取った型の配列型を作成する ToArray<T> の例です。
T がユニオン型の場合、Union distribution では配列型のユニオン型になりますが、extends キーワードの両側を角括弧[]
で囲むと、ユニオン型の配列になります。
type ToArray<T> = T extends unknown ? T[] : never; type StrArrOrNumArr = ToArray<string | number>; // string[] | number[] type ToArray2<T> = [T] extends [unknown] ? T[] : never; type StrOrNumArr = ToArray2<string | number>; // (string | number)[]
関連項目:Exclude<T, U>
Utility Types
Utility Types(ユーティリティ型)は、一般的な型の操作や変換を行うための標準で用意されているジェネリック型です。TypeScript を使用する際には、これらの型が自動的に利用可能になります。
どのような型があるかは以下で確認できます。また、新しいバージョンのリリースに伴い、新たなユーティリティ型が追加される可能性があります。
Utility Types(TypeScript Handbook)
これらの型は、TypeScript の型定義ファイル(.d.ts)に定義されており、TypeScript の標準ライブラリである lib.es5.d.ts に含まれています。
もし標準で用意されているユーティリティ型と同じ名前の型を定義するとコンパイルエラーになります。
以下はユーティリティ型と同じ名前の型を定義した場合に表示されるエラーの例です(VS Code の場合)。
エラーに表示されるファイル名のパス部分を command キーを押しながらクリックすると、定義が記述されているファイル(lib.es5.d.ts)が開きます。
ユーティリティ型は Mapped Types や Conditional Types などを使用して実装されています。
Partial<T>
Partial<T> は与えられた型 T の全てのプロパティをオプショナルな(任意の)プロパティにする型です。
この型の定義では Mapped Types を使って型 T のすべてのプロパティ [P in keyof T]
にオプショナルを表すマッピング修飾子 ?
を付けています。
T[P]
は型 T のプロパティの値の型を表す Lookup 型で、各プロパティの値の型は既存の型と同じです。
Partial<T>
の定義
/** * Make all properties in T optional */ type Partial<T> = { [P in keyof T]?: T[P]; };
以下の Partial<User>
は、既存の型 User のすべてのプロパティをオプショナルにした新しい型 PartialUser を作成します。
type User = { name: string; id: string; age?: number; } type PartialUser = Partial<User>;
PartialUser のすべてのプロパティには?
が付けられオプショナルなプロパティになります。
type PartialUser = { name?: string | undefined; // オプショナル id?: string | undefined; // オプショナル age?: number | undefined; // オプショナル }
Required<T>
Required<T> は与えられた型 T の全てのプロパティを必須にします。
この型の定義では Mapped Types を使って型 T のすべてのプロパティから-?
によりオプショナルを表す?
を削除しています。
Required<T>
の定義
/** * Make all properties in T required */ type Required<T> = { [P in keyof T]-?: T[P]; };
以下の Required<User>
は、既存の型 User のすべてのプロパティからオプショナル指定を削除して、必須にした新しい型 StrictUser を作成します。
type User = { name: string; id?: string; age?: number; } type StrictUser = Required<User>;
StrictUser はすべてのプロパティのオプショナル指定が削除され、以下のような型になります。
type StrictUser = { name: string; // 必須 id: string; // 必須 age: number; // 必須 }
Readonly<T>
Readonly<T> は与えられた型 T の全てのプロパティを読み取り専用(readonly)にします。
この型の定義では Mapped Types を使って型 T のすべてのプロパティに読み取り専用を表すマッピング修飾子 readonly
を付けています。
Readonly<T>
の定義
/** * Make all properties in T readonly */ type Readonly<T> = { readonly [P in keyof T]: T[P]; };
以下の Readonly<User>
は、既存の型 User のすべてのプロパティを読み取り専用にした新しい型 ReadonlyUser を作成します。
type User = { name: string; id: string; age: number; } type ReadonlyUser = Readonly<User>;
ReadonlyUser のすべてのプロパティにはreadonly
が付けられ読み取り専用のプロパティになり、以下のような型になります。
type ReadonlyUser = { readonly name: string; // 読み取り専用 readonly id: string; // 読み取り専用 readonly age: number; // 読み取り専用 }
Pick<T,K>
Pick<T,K> は与えられた型 T のプロパティのうち、K で与えられた名前(キー)のプロパティのみを残す型を返します。
この型の定義では、K extends keyof T として、extends キーワードを使って K は keyof T の部分型でなければならないという制約を設けています。
この場合、K は T のプロパティ名(キー)のリテラル型またはそのユニオン型になります。T に存在しないプロパティー名を K に指定するとコンパイルエラーになります。
Pick<T,K>
の定義
/** * 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]; };
第1パラメータには既存のオブジェクト型、第2パラメータには、残したいプロパティ名をリテラル型またはそのユニオン型で与えます。
type User = { name: string; id: string; age: number; } // User 型から name プロパティのみを持つ型を作成 type User1 = Pick<User, "name">; // User 型から name と age プロパティを持つ型を作成 type User2 = Pick<User, "name" | "age">;
User1 は User 型の name プロパティのみを持つ型になり、User2 は User 型の name と age プロパティを持つ以下のような型になります。
type User1 = { name: string; } type User2 = { name: string; age: number; }
Record<K, T>
Record<K, T> は、K 型のキーと T 型の値のセットを持つ型を作成します。
この型の定義の[P in K]: T
は Mapped Types の基本的な構文と同じで、型 K をキーとするプロパティが T 型の値を持つオブジェクトを作成します。
また、この型の定義では K extends keyof any
となっていますが、TypeScript では、オブジェクトのキー(プロパティ名)として使用できる型は string | number | symbol なので、keyof 型の型は string | number | symbol かその部分型でなければなりません。
Record<K, T>
の定義
/** * Construct a type with a set of properties K of type T */ type Record<K extends keyof any, T> = { [P in K]: T; };
例えば、以下の Record<string, boolean>
はキーが string 型で、値が boolean 型のオブジェクト型を作成します。
type StringBoolean = Record<string, boolean>; /* 上記は以下のような型を表します。 type StringBoolean = { [x: string]: boolean; // インデックスシグネチャ } */ // キーが string 型で値が boolean 型のオブジェクト const obj: StringBoolean = { a: true, b: false };
上記の場合は、インデックスシグネチャと同じですが、Record<K, T>
は K にユニオン型を受け取れるので、以下のような型を作成できます。
type Person = { name: string; age: number; } // 文字列リテラル型のユニオン型 type Names = "foo" | "bar" | "baz"; // キーが Names 型で値が User 型のオブジェクト型 type FooBarBaz = Record<Names, Person>; /* 上記は以下のような型を表します。 type FooBarBaz = { foo: Person; bar: Person; baz: Person; } */ const users: FooBarBaz = { foo: {name: "Foo", age: 18 }, bar: {name: "Bar", age: 22 }, baz: {name: "Baz", age: 36 }, }
また、keyof を使えば、既存のオブジェクトのプロパティを使った型を作成することができます。
type MyObj = { foo: string; bar: string; baz: string; } // キーが MyObj のプロパティで値が string | number 型のオブジェクト type StringOrNumberMyObj = Record<keyof MyObj, string | number>; /* 上記は以下のような型を表します。 type StringOrNumberMyObj = { foo: string | number; bar: string | number; baz: string | number; } */
Exclude<T, U>
Exclude<T, U> は、ユニオン型 T の構成要素のうち U の部分型であるものをすべて除外した新しい型を構築します。この型は Conditional Types の Union distribution という挙動を利用しています。
簡単に言うと、ユニオン型 T から U で指定した型(またはその部分型)を除外した型を作成します。
型の定義を見ると、T extends U ? never : T
とあり、T が U の部分型であれば、never を、そうでない場合は T を返します。T はユニオン型なので Union distribution が働き、T の構成要素それぞれについてこの操作が行われます。そして返されるユニオン型の中の never は属する値が無い型なので、ユニオン型の中では消えます。
Exclude<T, U>
の定義
/** * Exclude from T those types that are assignable to U */ type Exclude<T, U> = T extends U ? never : T;
以下の場合、MyUnion は string、number、boolean のリテラル型のユニオン型ですが、Exclude<MyUnion, number>
によりそれらの中の数値であるもの(第2パラメータ number の部分型であるもの)を除外したユニオン型になります。
type MyUnion = "foo" | 123 | true; type NumExcluded = Exclude<MyUnion, number>; // "foo" | true
これは Exclude<T, U>
の T
の MyUnion に Conditional Types における Union distribution が適用され、以下のような操作が行われるためです。
以下の操作の結果は "foo" | never | true となりますが、ユニオン型では never 型は削除されるので、結果として "foo" | true が作成されます。
type NumExcluded = ("foo" extends number ? never : "foo") | (123 extends number ? never : 123) | (true extends number ? never : true);
例えば、以下の最初の例は string | number | boolean の構成要素のうち、string を除外した型を作成するので、 number | boolean になります。
// 以下は number | boolean type Excluded1 = Exclude<string | number | boolean, string>; // 以下は boolean type Excluded2 = Exclude<string | number | boolean, string | number>; // 以下は never type Excluded3 = Exclude<string | number | boolean, string | number | boolean>; // 以下は string | number | boolean type Excluded4 = Exclude<string | number | boolean, object>;
以下の Exclude<keyof A, keyof B>
は、A 型のプロパティキーから B 型のプロパティキーを除外する操作を各プロパティに対して行います。具体的には、"id" が残ります。
type A = { name: string id: number } type B = { name: string age: number } type Excluded1 = Exclude<keyof A, keyof B>; // "id" // keyof A は "name"|"id"、keyof B は "name"|"age" なので以下と同じ type Excluded2 = Exclude<"name"|"id", "name"|"age">; // "id" // Union distribution が働き、以下と同じこと type Excludedy3 = ("name" extends "name"|"age" ? never : "name") | ("id" extends "name"|"age" ? never : "id"); // "id"
Extract<T, U>
Extract<T, U> は Exclude<T, U> の反対で、ユニオン型 T の構成要素のうち U の部分型であるものを抽出した新しい型を構築します。
簡単に言うと、ユニオン型 T から U で指定した型(またはその部分型)を抽出した型を作成します。
型の定義を見るとT extends U ?
の後の部分が、Exclude<T, U> の反対の T : never になっていて、T が U の部分型であれば、T を、そうでない場合は never を返します。
Extract<T, U>
の定義
/** * Extract from T those types that are assignable to U */ type Extract<T, U> = T extends U ? T : never;
以下の場合、NumStrExtracted は MyUnion の構成要素("foo" | 123 | true)の中の number | string の部分型である "foo" | 123 になります。
type MyUnion = "foo" | 123 | true; type NumStrExtracted = Extract<MyUnion, number | string>; // "foo" | 123
Exclude<T, U> とは逆に、第2パラメータで指定された型が抽出された型が作成されます。
// 以下は string type Extracted1 = Extract<string | number | boolean, string>; // 以下は string | number type Extracted2 = Extract<string | number | boolean, string | number>; // 以下は string | number | boolean type Extracted3 = Extract<string | number | boolean, string | number | boolean>; // 以下は never type Extracted4 = Extract<string | number | boolean, object>; type A = { name: string id: number } type B = { name: string age: number } type Extracted5 = Extract<keyof A, keyof B>; // "name"
Omit<T,K>
Omit<T,K> は既存の型 T から K で指定したプロパティを取り除いたオブジェクト型を作成します。
型の定義をみると、この型は Pick<T,K> と Exclude<T,K> を組み合わせて作成されています。
Omit<T,K>
の定義
/** * 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>>;
以下の例では、User から id と isActive を取り除いて、新たな型 UserNameAge を作成しています。
type User = { name: string; age: number; id: string; isActive: boolean; } type UserNameAge = Omit<User, 'id' | 'isActive'>; /* UserNameAge は以下のようになります type UserNameAge = { name: string; age: number; } */
NonNullable<T>
NonNullable<T> は型 T から null と undefined を取り除いた型を作成します。
型の定義を見ると、以下のように型 T と null と undefined 以外の全ての値が代入可能な空のオブジェクト型 {}のインターセクション型として定義されています。
NonNullable<T>
の定義
/** * Exclude null and undefined from T */ type NonNullable<T> = T & {};
以前の定義は、以下のように Exclude<T, U> の U を null | undefined で置き換えたような定義になっています。定義が変更された理由については、「TypeScript 4.8で入る型の絞り込みの改善とは」に詳しく書かれています。
// 以前の古い NonNullable<T> の定義 type NonNullable<T> = T extends null | undefined ? never : T;
type T1 = NonNullable<string | number | undefined>; // T1 は string | number type T2 = NonNullable<string[] | null | undefined>; // T2 は string[]
Parameters<T>
Parameters<T> は関数型 T の引数の型をタプル型として抽出した型を作成します。
以下の定義の型 T の制約 T extends (...args: any) => any
は T が関数の型であることを表しています。そして infer キーワードを使って 型 T が関数型であれば、その引数の型を返しています。
Parameters<T>
の定義
/** * Obtain the parameters of a function type in a tuple */ type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
type func1 = (x: number, y: boolean) => void; type func2 = (x: string) => string; type func3= ()=> void; type Param1 = Parameters<func1>; // [x: number, y: boolean] (ラベル付きタプル型) type Param2 = Parameters<func2>; // [x: string] type Param3 = Parameters<func3>; // []
ReturnType<T>
ReturnType<T> は関数型 T の戻り値の型を返します。
Parameters<T> 同様、定義の型 T の制約 T extends (...args: any) => any
は T が関数の型であることを表しています。そして infer キーワードを使って 型 T が関数型であれば、その戻り値の型を返しています。
/** * Obtain the return type of a function type */ type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
type func1 = (x: number, y: boolean) => void; type func2 = (x: string) => string; type func3= ()=> boolean; type Return1 = ReturnType<func1>; // void type Return2 = ReturnType<func2>; // string type Return3 = ReturnType<func3>; // boolean