カテゴリー
個人的Typescriptの型入門② 〜 オブジェクト型の応用
※ 当ページには【広告/PR】を含む場合があります。
2024/09/09

目次
- 1. 型の学習には「Typescript Playground」を使おう
- 2. オブジェクト型の応用
- 2-1. 「?」プロパティ修飾子
- 2-2. オプショナルなプロパティへのアクセス
- 2-3. 極力「?」修飾子に頼らない
- 2-4. 「exactOptionalPropertyTypes」コンパイラオプション
- 2-5. 「readonly」プロパティ修飾子
- 2-6. readonlyの注意点
- 2-7. インデックスシグネチャ
- 2-8. インデックスシグネチャの注意点
- 2-9. 関数シグネチャ
- 2-10. 複数の関数シグネチャによるオーバーローディング
- 2-11. newシグネチャ
- 2-12. asによるダウンキャスト
- 2-13. asの注意点
- 2-14. readonlyな配列型
- 2-15. readonlyなタプル型
- 2-16. Variadic Tuple Types
- 2-17. Variadic Tuple Typesの応用
- 2-18. 「[...T]」と「T」の違い
- 2-19. テンプレートリテラル型
- 2-20. テンプレートリテラルの利用法
- 2-21. as const
- 2-22. as constの使いどころ
- 2-23. as constとテンプレート文字列リテラル
- 3. まとめ
前回はTypescriptの型の初歩を中心にまとめてみました。
今回は基礎のレベルから少し進んで、「オブジェクト型」について掘り下げていきます。
前回の記事同様、以下の詳しい解説されているサイトをベースに、著者が防備録として抑えておきたい利用法をピックアップしています。
詳しい説明はそちらのほうをご参照ください。
型の学習には「Typescript Playground」を使おう
復習も兼ねて、型の基礎のほうから徐々にTypescriptの型の使い方を慣らして行きましょう。
なおTypescriptの型チェックは、VSCode等のエディタで用意されているものを扱うことを想定していますが、サッとTypescriptを試したい際には定番の
オブジェクト型の応用
ここからよりオブジェクト型に関連した応用的なテーマに進みます。
「?」プロパティ修飾子
オブジェクト型のプロパティに対しては、
まず
interface MyObj {
foo: string;
bar?: number;
}
let obj: MyObj = { foo: 'string' };
obj = { foo: 'foo', bar: 100 };
この例ではbarが省略可能なプロパティになっています。
オプショナルなプロパティへのアクセス
JavaScriptでは存在しないプロパティにアクセスすると
undefined
このことを反映して、先程のMyObjのbarプロパティにアクセスした場合に得られる型は、
number | undefined
つまり、
?
よってオプショナルなプロパティをもつUnion型では、undefinedチェックを行う必要があります。
interface MyObj {
foo: string;
bar?: number;
}
function func(obj: MyObj): number {
return obj.bar !== undefined ? obj.bar * 100 : 0;
}
極力「?」修飾子に頼らない
Typescriptコードの実装指針として、なるべく
?
number | undefined
?
interface MyObj1 {
foo: string;
bar?: number;
}
interface MyObj2 {
foo: string;
bar: number | undefined;
}
//オプショナルなプロパティはなくてもエラーは出ない
let obj: MyObj1 = {
foo: 'string',
};
//✘ Type '{ foo: string; }' is not assignable to type 'MyObj'.
// Property 'bar' is missing in type '{ foo: string; }'.
let obj: MyObj2 = {
foo: 'string',
};
「exactOptionalPropertyTypes」コンパイラオプション
オプショナルなプロパティの挙動は、
exactOptionalPropertyTypes
このオプションはデフォルトで無効です。
このオプションが無効の場合、オプショナルなプロパティには明示的に
undefined
interface MyObj {
foo: string;
bar?: number;
}
//exactOptionalPropertyTypesが無効の場合、以下は全部OK
const obj1: MyObj = { foo: "pichu" };
const obj2: MyObj = { foo: "pikachu", bar: 25 };
const obj3: MyObj = { foo: "raichu", bar: undefined };
逆に有効の場合、オプショナルなプロパティに
undefined
interface MyObj {
foo: string;
bar?: number;
}
//exactOptionalPropertyTypesが有効の場合:
//✘ Type 'undefined' is not assignable to type 'number'.
const obj3: MyObj = { foo: "raichu", bar: undefined };
このオプションが有効な状態であれば、
bar?: number;
つまり、
bar?: number;
こうすることで、in演算子を用いて型の絞り込みが安心して行えるメリットがあります。
//exactOptionalPropertyTypesが有効の状態
interface MyObj {
foo: string;
bar?: number;
}
function func(obj: MyObj) {
if ("bar" in obj) {
//obj.barはnumber型
console.log(obj.bar.toFixed(1));
}
}
注意が必要なのは、自身のプロジェクトで
exactOptionalPropertyTypes
こちらの設定では有効だとしても、別のライブラリから提供された型定義にオプショナルなプロパティには「undefined型の値が入っている」という状態になる可能性もあります。
「readonly」プロパティ修飾子
もうひとつの
その意味で、Typescript独自の「constのプロパティ版」と言えます。
interface MyObj {
readonly foo: string;
}
const obj: MyObj = {
foo: 'Hey!',
};
//✘ Cannot assign to 'foo' because it is a constant or a read-only property.
obj.foo = 'Hi';
readonlyの注意点
readonly演算子でプロパティ書き換え不可が保証されるかといえばそうでもなく、次の例に示すように、readonlyでない型の値からの参照を経由して書き換えできます。
interface MyObj {
readonly foo: string;
}
interface MyObj2 {
foo: string;
}
const obj: MyObj = { foo: 'Hey!', }
const obj2: MyObj2 = obj;
obj2.foo = 'Hi';
console.log(obj.foo); // 'Hi'
インデックスシグネチャ
オブジェクト型のプロパティの特殊な記法に、
インデックスシグネチャは、以下のように使います。
interface MyObj {
[key: string]: number;
}
const obj: MyObj = {};
const num: number = obj.foo;
const num2: number = obj.bar;
ここでの、オブジェクト型の宣言の中で、
[key: string]: number;
このように書くと、名前がstring型である任意のプロパティに対して、全てnumber型を持つ、という意味になります。
例では、obj.fooやobj.barなどは全てnumber型です。
インデックスシグネチャの注意点
インデックスシグネチャを使うと、そもそも型安全な設計ができません。
インデックスシグネチャにはリスクがあるものの、配列型の定義には欠かせない構文なのもまた事実です。
実際、配列型の定義は概ね下のような感じです。
interface Array<T> {
[idx: number]: T;
length: number;
// メソッドの定義が続く
// ...
}
ちなみに、この例のようにインデックスシグネチャ以外のプロパティ宣言があった場合、そちらの定義が優先されます。
オブジェクト型を辞書として独自に実装する場合は、あえてインデックスシグネチャのテクニックは避けて、組込みユーティリティの
関数シグネチャ
以下の例を見てください。
interface Func {
(arg: number): void;
}
const f: Func = (arg: number) => { console.log(arg); };
ここでの関数シグネチャは
(arg: number): void;
この記法は通常のプロパティの宣言と併用ができるので、関数型だけど同時に特定のプロパティを持っているようなオブジェクト型、つまり、関数型とオブジェクト型のIntersection型をまとめて表すことができます。
複数の関数シグネチャによるオーバーローディング
複数の関数シグネチャを書くと、
interface Func {
foo: string;
(arg: number): void;
(arg: string): string;
}
この型が表す値は、string型のfooプロパティを持つオブジェクト型、かつ、関数としてnumber型を引数で呼び出す場合は何も返さない関数型、かつ、string型を引数で呼び出す場合はstring型の値を返すような関数型、となります。
関数のシグネチャの違いだけで、関数のオーバロードを簡潔に表現できるのは便利です。
newシグネチャ
interface Ctor<T> {
new(): T;
}
class Foo {
public bar: number | undefined;
}
const f: Ctor<Foo> = Foo;
const obj = new f();
ここでの
Ctor<T>
new
クラス定義したFooはコンストラクタ関数(new)を持っているので、
Ctor<Foo>
ちなみに、関数型を
(foo: string)=>number
new()=>Foo
asによるダウンキャスト
評価式 as 型
ダウンキャストの操作は、一般に型安全ではありませんが、Union型などをその部分型に当てはめるときに必要です。
function rand(): string | number {
if (Math.random() < 0.5) {
return 'hello';
} else {
return 123;
}
}
const value = rand();
const str = value as number;
console.log(str * 10);
この例では、valueは
string | number
value as number
当然、valueはstring型かもしれないので、asでダウンキャストするのは安全な操作ではありません。
asの注意点
asを使っても型キャストに全く関係ない2つの型の間で変換することはできません。
const value = 'foo';
//✘ Type 'string' cannot be converted to type 'number'.
const str = value as number;
ただ、any型かunknown型を経由すれば半ば強制的に型変換できます。
その場合、asはダウンキャストではなくアップキャストになります。
const value = 'foo';
const str = value as unknown as number;
この例では、最初のas unknownで行われているのはアップキャストで、その後as numberでダウンキャストしています。
他のアップキャストの例として、文字列リテラル型・'foo'型をstring型へアップキャストしている例です。
//const foo: string = 'foo'; と同じ結果になる
const foo = 'foo' as string;
例のように、アップキャスト自体はasを使わなくてもできる安全な操作ですが、アップキャストにまでasを使うのは、危険なダウンキャストと見分けがつかないので、利用は控えたいところです。
readonlyな配列型
配列型やタプル型においても
ただし、配列型やタプル型全体をreadonlyに設定できるだけで、オブジェクト型の場合でプロパティごとにreadonlyを制御できるのに対して、配列型やタプル型は要素ごとに細かい制御はできません。
const arr: readonly number[] = [1, 2, 3];
//✘ Index signature in type 'readonly number[]' only permits reading.
arr[0] = 100;
//✘ Property 'push' does not exist on type 'readonly number[]'
arr.push(4);
例のように、readonlyな配列型は
readonly T[]
reaodnlyなプロパティと同じく、readonlyな配列の要素を書き換えようとするとエラーとなる他、pushなどの配列を破壊的メソッドは除去されており使えません。
なお、配列型を
T[]
Array<T>
readonly T[]
ReadonlyArray<T>
readonly Array<T>
readonlyなタプル型
readonlyなタプル型は、タプル型の前にreadonlyを付けて表現します。
const tuple: readonly [string, number] = ['foo', 123];
//✘ Cannot assign to '0' because it is a read-only property.
tuple[0] = 'bar';
この例では、tupleは
readonly [string, number]
Variadic Tuple Types
さらに進んだ機能として後付で実装されたものが、
この機能は、タプル型に、別のタプル型を付け加えた別のタプル型を作ることができます。
type SNS = [string, number, string];
//[string, string, number, string, number]型
type SSNSN = [string, ...SNS, number];
Variadic Tuple Typesの応用
...T
function removeFirst<T, Rest extends readonly unknown[]>(
arr: [T, ...Rest]
): Rest {
const [, ...rest] = arr;
return rest;
}
//arrは[number, number, string]型
const arr = removeFirst([1, 2, 3, "foo"]);
この例では、関数removeFirstの型引数TおよびRestが、それぞれnumberおよび[number, number, string]であると判別され、変数arrが
[number, number, string]
特に、引数
[1, 2, 3, "foo"]
[T, ...Rest]
上の例で言えば、removeFirst内の変数restの型が自動的にRestと推論されている点も注目に値し
なお、型引数を
...T
T
上の例でいうと、
Rest extends readonly unknown[]
「[...T]」と「T」の違い
型変数で、
[...T]
実際には、
[...T]
T
function func1<T extends readonly unknown[]>(arr: T): T {
return arr;
}
function func2<T extends readonly unknown[]>(arr: [...T]): T {
return arr;
}
//arr1はnumber[]型
const arr1 = func1([1, 2, 3]);
//arr2は[number, number, number]型
const arr2 = func2([1, 2, 3]);
この例のように、型引数の推論時に、
T
[...T]
関数に渡された配列の各要素の型を得たい場合、配列型にしたくない場面で活用できるかもしれません。
テンプレートリテラル型
一般的なJavascriptのテンプレートリテラルについては以下のサイトをご覧ください。
type HelloStr = `Hello, ${string}`;
const str1: HelloStr = "Hello, world!";
const str2: HelloStr = "Hello, uhyo";
//✘ Type '"Hell, world!"' is not assignable to type '`Hello, ${string}`'.
const str3: HelloStr = "Hell, world!";
この例では、
HelloStr
Hello,
これで
Hello,
テンプレートリテラルの利用法
テンプレートリテラル型では、テンプレートリテラルからのほとんどの文字列をリテラル型として宣言できるのですが、数値型(number型)を文字列へ変換する際に、文字列フォーマットによっては使えないものもあります。
type PriceStr = `${number}円`;
const str1: PriceStr = "100円";
const str2: PriceStr = "-50円";
const str3: PriceStr = "3.14円"
const str4: PriceStr = "1e100円";
// ここから下は全部エラー
const str5: PriceStr = "1_000_000円";
const str6: PriceStr = "円";
const str7: PriceStr = "1,234円";
const str8: PriceStr = "NaN円";
const str9: PriceStr = "Infinity円";
例のように、使えるフォーマットとそうでないものが微妙に区別される
${number型}
テンプレートリテラル型の実用性を考えると、
${string型}
${文字列リテラル型のUnion型}
as const
つまり、as constは、各種リテラル(文字列/数値/ブール値/オブジェクト/配列)に作用し、その値が書き換えを意図していないことを表します。
//fooはstring型
let foo = '123';
この
'123'
as const
"123"
//foo2は"123"型
let foo2 = '123' as const;
as constの使いどころ
「as const」
以下の例で説明します。
const obj = {
foo: "123",
bar: [1, 2, 3]
};
//👇objの型は
// {foo: string; bar: number[]}
const obj2 = {
foo: "123",
bar: [1, 2, 3]
} as const;
//👇obj2は型
// {
// readonly foo: "123";
// readonly bar: readonly [1, 2, 3];
// }
まず
as const
{ foo: string; bar: number[] }
オブジェクトリテラルのreadonlyでないプロパティは、結局
obj.foo = "456";
対して、オブジェクトリテラルに
as const
なお、obj2の変数barについて、配列リテラルへas constが適用され、
readonlyタプル型
readonly [1, 2, 3]
as constとテンプレート文字列リテラル
テンプレート文字列リテラルに対して、
as const
const world: string = "world";
// string型
const str1 = `Hello, ${world}!`;
// `Hello, ${string}!` 型
const str2 = `Hello, ${world}!` as const;
上の例で、テンプレートリテラルは通常、string型となります。
これが、
as const
以上、
as const
1. 文字列・数値・ブール値リテラルに作用させると、それ自体のリテラル型として推論される
2. テンプレート文字列リテラルに作用させると、テンプレートリテラル型に推論される
3. オブジェクトリテラルに作用させると、各プロパティがreadonlyを持つ
4. 配列リテラルに作用させると、readonlyタプル型になる
まとめ
以上、今回はTypescriptのオブジェクト型にまつわる応用的・実践的なテクニックを中心にまとめていきました。
ここまで一通り読んでもらうと、普段なんとなく使っているTypescriptの型システムの背景をじっくりと考えなおすことができたかもしれません。
次回からは、Conditional typeや、Mapped typeなど、Typescript界隈では「型パズル」や「型チャレンジ」などと言われて、競技性(?)が高くなってきた比較的新しい構文の内容を特集していく予定です。
記事を書いた人
ナンデモ系エンジニア
主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。
カテゴリー
- 1. 型の学習には「Typescript Playground」を使おう
- 2. オブジェクト型の応用
- 2-1. 「?」プロパティ修飾子
- 2-2. オプショナルなプロパティへのアクセス
- 2-3. 極力「?」修飾子に頼らない
- 2-4. 「exactOptionalPropertyTypes」コンパイラオプション
- 2-5. 「readonly」プロパティ修飾子
- 2-6. readonlyの注意点
- 2-7. インデックスシグネチャ
- 2-8. インデックスシグネチャの注意点
- 2-9. 関数シグネチャ
- 2-10. 複数の関数シグネチャによるオーバーローディング
- 2-11. newシグネチャ
- 2-12. asによるダウンキャスト
- 2-13. asの注意点
- 2-14. readonlyな配列型
- 2-15. readonlyなタプル型
- 2-16. Variadic Tuple Types
- 2-17. Variadic Tuple Typesの応用
- 2-18. 「[...T]」と「T」の違い
- 2-19. テンプレートリテラル型
- 2-20. テンプレートリテラルの利用法
- 2-21. as const
- 2-22. as constの使いどころ
- 2-23. as constとテンプレート文字列リテラル
- 3. まとめ