こんにちは。野中やすおです。
今回の記事では、TypeScript 3.4で導入されたconst assertionのas constがとても便利なことに気がついたので記事にしてみます。
TypeScript 3.4 Release Notes…
as constとは
as constは、式の末尾に使用されます。as constを使用することでTypeScriptの型推論に以下の効果をもたらします。
- Wideningを防ぐ(no literal types in that expression should be widened)
- オブジェクトリテラルは、readonlyプロパティになる(object literals get readonly properties)
- 配列リテラルはreadonlyタプルになる(array literals become readonly tuples)
Wideningとは
Wideningは、よくよくみると英単語「Wide」のing形で、直訳すると拡張しているという意味になります。そこから、「リテラル型が自動的にプリミティブ型に広がって推論されること」を指します。
as constの使用例
まずは以下のようなオブジェクトがあるとします。そして末尾にas constを使用します。
1 2 3 4 5 |
const yasuo = { name: "yasuo", age: 30, gender: "man", } as const; |
これに
1 |
yasuo.name = "nonaka"; |
を代入しようとするとreadonlyのためもちろん以下のようにエラーになります。
それなら各プロパティにreadonlyをつければいいだけじゃない?ってなりますが、const assertionは再帰的にオブジェクトリテラルや配列リテラルをreadonlyにすることができます。
この性質を利用して、例えば以下のような関数TestAがあるとします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
type DataType = string | number | null; type ColumnType = 'string' | 'number' | 'datetime'; function TestA(data: DataType, type: ColumnType): string { if (data === null) return ''; switch (type) { case 'string': return data as string; case 'number': return data.toString(); case 'datetime': const date = new Date(data as string); date.setHours(date.getHours() + 9); const formattedDate = date.toISOString().split('T')[0].replace(/-/g, '/'); const formattedTime = date.toISOString().split('T')[1].slice(0, 5); return `${formattedDate} ${formattedTime}`; default: throw new Error(`Unsupported type: ${type}`); } } |
この関数に対して、自分は型エラー回避のために以下のように冗長に型をつけていました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
type DataType = string | number | null; type ColumnType = 'string' | 'number' | 'datetime'; describe('TestA', () => { test.each([ // string test case { data: 'Test String', type: 'string' as ColumnType, expected: 'Test String', }, // number test case { data: 123, type: 'number' as ColumnType, expected: '123' }, // datetime test case { data: '2021-12-31T00:00:00Z', type: 'datetime' as ColumnType, expected: '2021/12/31 09:00', }, // null data test case { data: null, type: 'string' <span class="crayon-st">as</span> <span class="crayon-v">ColumnType</span><span class="crayon-sy">,</span>, expected: '' }, { data: null, type: 'number' as ColumnType, expected: '' }, { data: null, type: 'datetime' as ColumnType, expected: '' }, ])('TestA($data, $type) should return $expected', ({ data, type, expected }) => { expect(TestA(data, type)).toEqual(expected) }) }) |
しかしas const
を使用してテストケースのデータを修正すると、ColumnType
へのキャスト (as ColumnType
) の必要がなくなります。
よって以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
describe('TestA', () => { test.each([ // string test case { data: 'Test String', type: 'string', expected: 'Test String', }, // number test case { data: 123, type: 'number', expected: '123' }, // datetime test case { data: '2021-12-31T00:00:00Z', type: 'datetime', expected: '2021/12/31 09:00', }, // null data test case { data: null, type: 'string', expected: '' }, { data: null, type: 'number', expected: '' }, { data: null, type: 'datetime', expected: '' }, ] as const)('TestA($data, $type) should return $expected', ({ data, type, expected }) => { expect(TestA(data, type)).toEqual(expected); }); }); |
かなりテストがすっきりしたかと思います。 as constはテストケースで使用するのにも非常に有用であることを学びました。
参考
オブジェクトリテラルの末尾にas constを記述すればプロパティがreadonlyでリテラルタイプで指定した物と同等の…