サイトトップ

Director Flash 書籍 業務内容 プロフィール

HTML5テクニカルノート

TypeScriptハンドブック 05: 型アサーションとリテラル型


TypeScript公式「Handbook」の「Everyday Types」から今回ご紹介するのは、つぎのふたつの項目です(かっこ内は原文の項目タイトル)。型アサーションは、TypeScriptの分析に指示を与えて、データ型を拡げたり、絞り込んだりします。そして、リテラル型というのは、文字列や数値などのリテラルを型として用いる構文です。

01 型アサーション

型アサーションasは、TypeScriptがデータをどういう型として解析すべきか指定する機能です。

式 as 型

たとえば、document.getElementById()メソッドでは、TypeScriptが戻り値をHTMLElementと推論します。けれど、それでは要素が独自にもっているプロパティやメソッドは参照できません。


const myCanvas = document.getElementById("main_canvas");
// オブジェクトは 'null' である可能性があります。
// プロパティ 'getContext' は型 'HTMLElement' に存在しません。
const ctx = myCanvas.getContext('2d');  // エラー

TypeScriptではわからない要素の型が予め決まっているのであれば、型アサーションで具体的な要素に絞り込めます。


// const myCanvas = document.getElementById("main_canvas");
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
const ctx = myCanvas.getContext('2d');  // エラーなし

型アサーションのもうひとつの構文は、データの前に山かっこ(<>)で型を添えることです。

<型>式

const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");

ただし、JSXを用いるファイル(.tsx)では、山かっこ(<>)がJSX要素とみなされて、つぎのようなエラーになることがあります。したがって、型アサーションはasに統一した方がわかりやすいでしょう。

JSX要素HTMLCanvasElementには対応する終了タグがありません。
HTMLCanvasElementをJSXコンポーネントとして使用することはできません。
そのインスタンスの型HTMLCanvasElementは、有効なJSX要素ではありません。

ノート01■型アサーションとキャスト

データ型の変換は他の言語で「キャスト」と呼ばれることがあります。その場合、ランタイムにおけるサポートを含むのが一般的です。それに対して、TypeScriptの型アサーションは、コンパイルされたら除かれます。コンパイルするとき、TypeScriptが型を解析する手助けとなるに過ぎません。ランタイムで、型に合わないデータをnullに変えたり、例外エラーを出すといったことはないのです。

型アサーションは、TypeScriptが型を絞ったり拡げて解析するための仕組みです。相容れないデータ型に変換することはできません。


// 型 'string' から型 'number' への変換は、互いに十分に重複できないため間違っている可能性があります。意図的にそうする場合は、まず式を 'unknown' に変換してください。
const x = "1" as number;  // エラー

もっとも、anyに型アサーションすれば、そのあとどのようにでも型づけできます(エラーメッセージのとおり、unknownも同じように使えます)。けれど、それはTypeScriptが型チェックを諦めたに過ぎません。データは変換されませんので、意図した結果が得られるかは自ら確かめなければならないのです。


const x = "1" as any as number;  // エラーなし
console.log(x + "0");  // 10 <- stringとして処理

02 リテラル型

基本となる型のstringnumberに加えて、特定の文字列や数値をリテラル型として定めることができます。

ご説明に先立って見ておきたいのは、変数宣言verletに対して、TypeScriptのconstの捉え方の違いです。verletで宣言された変数の値は書き替えられます。たとえば、文字列を代入すれば、エディタで変数にポインタを合わせたとき、つぎのようにstring型と説明が表示されるでしょう。TypeScriptが、初期値から代入しうるデータの型を推論するのです。


let changingString = "Hello World";
changingString = "Olá Mundo";
// 説明表示: let changingString: string
changingString;

他方で、const宣言に与えられた値は書き替えできません。このとき、TypeScriptは変数を、初期値のリテラル型とみなすのです。説明表示には、初期値のリテラル("Hello World")が型として示されます。


const constantString = "Hello World";
// 説明表示: const constantString: "Hello World"
constantString;

もっとも、リテラル型を単体で定めても、あまり使い途はありません。constを用いれば、済んでしまうからです。


let greeting: "hello" = "hello";
// 型 '"howdy"' を型 '"hello"' に割り当てることはできません。
// greeting = "howdy";  // エラー
greeting = "hello";  // OK

けれど、ユニオンで複数のリテラル型を結びつけると、予め決めたリテラルのみを受け入れる型がつくれるのです。


type Calc = "sum" | "average" | "hypot";
function calcArray(calc: Calc, array: number[]) {
	switch (calc) {
		case "sum":
			return array.reduce((result, num) => result + num);
		case "average":
			const sum = array.reduce((result, num) => result + num);
			return sum / array.length;
		case "hypot":
			return Math.hypot(...array);
	}
}
const array = [-1, 0, 1, 2, 3, 2, 2, 1, -1];
console.log(calcArray("sum", array));  // 9
console.log(calcArray("average", array));  // 1
console.log(calcArray("hypot", array));  // 5

数値リテラルも同じように定められます。たとえば、つぎの関数(compare)の戻り値は、3つの数値にかぎられるのです。


function compare(a: number, b: number): -1 | 0 | 1 {
	return a === b ? 0 : a > b ? 1 : -1;
}
const array = [1, 2000, 2, -1, 101, 21];
console.log([...array].sort());  // [-1, 1, 101, 2, 2000, 21]
console.log([...array].sort(compare));  // [-1, 1, 2, 21, 101, 2000]

ノート02■ブールリテラル型

ブール(論理)値もリテラル型に用いることができます。もっとも、booleanはそもそもが二値です(つまり、リテラルtruefalseのユニオン型ともみなせます)。あえてリテラル型として使うとすれば、他の型を加える場合でしょう。


type BoolOrNull = true | false | null;

もちろん、リテラル型を通常のオブジェクトの型とユニオンで組み合わせることもできます。


interface Options {
	width: number;
}
type Config = Options | "default";

リテラル推論

変数をオブジェクトで初期化したとき、TypeScriptはプロパティの値が変わるものとみなして解析します。たとえば、つぎの変数(counter)に与えたオブジェクトのプロパティ(count)は、リテラル型の0でなく、書き替えができるnumber型と推論するのです。したがって、プロパティに別の数値を収めてもエラーにはなりません。読み取りと書き替えを考えたうえで、型は決められるのです。


const counter = { count: 0 };
function countUp() {
	++counter.count
}
countUp();
console.log(counter);  // {count: 1}

プロパティが文字列でも同じです。初期値のオブジェクト(request)のプロパティは、値が任意の文字列で書き替えられるstring型と推論されます。すると、文字列("GET")としては一致しても、決まった文字列のリテラル型で定められた引数に渡せば、互換性がないというエラーになってしまうのです。


interface Request {
	url: string;
	method: "GET" | "POST";  // リテラル型
}
function handleRequest({ url, method }: Request) {
	console.log(url, method);
}
const request = { url: "https://example.com", method: "GET" };
// 型 '{ url: string; method: string; }' の引数を型 'Request' のパラメーターに割り当てることはできません。
// プロパティ 'method' の型に互換性がありません。
// 型 'string' を型 '"GET" | "POST"' に割り当てることはできません。
handleRequest(request);  // エラー

この場合、引数に渡す文字列の型アサーションは、ふたつ考えられます。ひとつは、引数の側でリテラル型とされているプロパティ(method)を、リテラル("GET")で型アサーションすることです。


// const request = { url: "https://example.com", method: "GET" };
const request = { url: "https://example.com", method: "GET" as "GET" };
handleRequest(request);  // https://example.com GET

もうひとつ、渡すオブジェクト(request)そのものを、constで型アサーションすることもできます。すると、const宣言した初期値がリテラル型とみなされたように、オブジェクトのプロパティがその値のリテラル型とみなされるのです。もちろん、文字列リテラル型の値を、より縛りの緩いstring型に与えることは問題ありません。


const request = { url: "https://example.com", method: "GET"} as const;


作成者: 野中文雄
作成日: 2021年05月22日


Copyright © 2001-2020 Fumio Nonaka.  All rights reserved.