TypeScript1.8の目玉機能と言えば、 ストリングリテラル(String literal)型 だ。
ストリングリテラル型は文字列型(string
型)にさらに制約を加えて、 特定の文字列のみ許可する ものだ1。JavaScript的にはただの文字列で、コンパイルタイムに型チェックされる。
ストリングリテラル型のいい所
何と言ってもJSに多い、引数に文字列キーを渡して処理を分ける関数をちゃんとチェックできるようになるという点。他言語ならenumで処理する所だが、なぜかTypeScriptには数値enumはあるのに文字列enumがなく、有効な手段がとれているとは言いがたかった。
一応これまでも関数オーバーロードの形で、部分的にストリングリテラル型っぽいことは出来ていた。だが、関数を文字列キーの数だけ宣言する必要があり、決して使いやすくはなかった。
ストリングリテラル型の登場で、関数の引数以外にも使える箇所が広がり、文字列enumの扱いはよりやりやすくなった2。
type Enum = "a" | "b" | "c";
namespace Enum{
export const A:Enum = "a"
export const B:Enum = "b"
export const C:Enum = "c"
}
function enumOnly(arg:Enum){}
enumOnly(Enum.A); //補完も効くし、typoもしない!
ストリングリテラル型の問題
さて、文字列enum問題に福音をもたらしたストリングリテラル型だが、意外と問題を抱えている。普通の型と同じように扱おうとすると、結構制限があるのだ。
そこでざっくり調べて表にしてみた。
ストリングリテラルできるできない表
項目 | できるできない | 補足 |
---|---|---|
ダブルクオートで宣言"
|
○ | |
シングルクオートで宣言'
|
○ | |
テンプレートストリングスで宣言 | × | * |
変数・定数(var,let,const )型宣言 |
○ | |
プロパティ型宣言 | ○ | |
関数/メソッド返り値の型宣言 | ○ | |
関数/メソッドの引数の型宣言 | △ | * |
デフォルト/オプショナル引数 | △ | * |
型アサーション<"a"> /as 演算子 |
○ | |
配列 "a"[]
|
○ | |
ジェネリックス Class<"a">
|
○ | |
型パラメータの制約 <T extends "a">
|
○ | |
string 型への暗黙のキャスト |
○ | * |
string 型からの暗黙のキャスト |
× | * |
インデックスシグネチャ | × | * |
型クエリ let a:typeof "a"
|
× | |
継承 class C extends "a"{}
|
× | |
タイプエイリアス | ○ | |
Union型 | ○ | |
Intersection型 "a"&"b"
|
○ | * |
タプル型 let a:["a","b"]
|
○ | |
Type Guard | × | * |
User Defined Type Guard | ○ | * |
IDEの補完サポート | × | * |
IDEのシンボルリネームサポート | × | * |
const 宣言時の型推論 |
× | * |
TypeScript ver. 1.8.7
できること
string型への暗黙のキャスト
let A:"a" = "a";
let a:string = A; //ok
string型へは特に何もせずキャスト可能(型アサーション不要)。逆は不可。
User Defined Type Guard
function isA(a:any): a is "a"{
return true;
}
function fnUserAorB(arg:"a"|"b"){
if(isA(arg)){
let a:"a" = arg;
}else{
let b:"b" = arg;
}
}
普通のタイプガードはできないが、ユーザー定義タイプガード関数を使えばいける。is
演算子も問題ない。
できるけど意味ないこと
交差型
type A = "a" & "b"; //定義可能
let a:A;
a = "a"; //error
a = "b"; //error
a = "ab"; //error
交差型は定義できるが、これに当てはまる値が作れない。
まあ"a"&"a"
みたいなのであればいけるが、意味が無い。
できないこと
テンプレートストリングスで宣言
type DQ = "a\nb"; //ok
type SQ = 'a\nb'; //ok
type TS = `a\nb`; //error
テンプレートストリングスの書き方ではストリングリテラル型として宣言できない。
string型からの暗黙のキャスト
const s:string = "s";
let s2:"s" = s; //error!
let s3:"s" = <"s">s; //ok
let s4:"s" = s as "s"; //ok
逆はできるが、これは例え同じ文字列でもできない(要・型アサーション)。
Type Guard
function fnAorB(arg: "a"|"b"){
if(typeof arg === "a" ){ //typeof arg === "string"
let a:"a" = arg; //error!
}else{
let b:"b" = arg; //error!
}
}
function fnAorB2(arg: "a"|"b"){
if(arg instanceof "a"){ //error!
let a:"a" = arg; //error!
}else{
let b:"b" = arg; //error!
}
}
function fnAorB3(arg: "a"|"b"){
switch(arg){
case "a":{
let x:"a" = arg; //error!
break;
}
case "b":{
let x:"b" = arg; //error!
break;
}
}
}
タイプガードには使えない。
fnAorB()
,fnAorB2()
のように、typeof演算子やinstanceof演算子で扱えるものではない。
関連Issue: enable type guards over string literal types #6028
関数の引数の型指定に一つだけ指定
function f(a:"a"){} //error!関数オーバーロード扱い
関数/メソッドの引数の型指定にそのまま一つのストリングリテラル型を指定してもエラーになる(関数/メソッドオーバーロードと被る為)。ただしくは以下の方法を使う。
function f(a:("a")){}
type A = "a";
function f(a:A){};
function f1(a:"a"|"a"){};
function f2(a:"a"&"a"){};
実際の利用シーンでは一つだけのストリングリテラル型というのも考えにくいのであまり問題ではないが、覚えておくべき。
function defaultParamFunc(a:number, b:("b") = "b"){}
function optionalParamFunc(a:number, b?:("b")){}
上記ルールに従えば、デフォルト引数、オプショナル引数両方とも型指定可能。
インデックスシグネチャ
type A = "a";
type O = {
[s:A]:number; //error!
};
インデックスシグネチャはstring|number
と定義されておりストリングリテラル型は指定できない。
関連Issue: String literal types as index signature parameter types? #5683
IDEでの補完
VSCodeなどで、選択肢が絞り込めても補完できない。型として扱うメリットの半分くらい無い感じである。
関連Issue: Provide completion lists for string literals #606
IDEでのシンボルリネーム
VSCodeだと、変更文字列を入力まではできるがその先が機能しない。
関連Issue: String Literal refactoring support #5602
const宣言時の型推論
const A = "A"; //Aはstring型
const B:"B" = "B"; //Bは"B"型
let a:"A";
a = A; //error!
let b:"B";
b = B; //ok
let c:string;
c = B; //ok
const
で宣言した文字列変数・定数は不変。にもかかわらず、現時点では型指定をしっかり書かないとstring型として扱われ、型エラー。なかなか不便である。
関連Issue: Mutability widening for literal types #6554
上記の pull request が実装されると冒頭の文字列enumは以下のように書けるようになるはず。
type Enum = "a" | "b" | "c";
namespace Enum{
export const A = "a"
export const B = "b"
export const C = "c"
}
まとめ
- ストリングリテラル型は便利
- でも、既存の型と全く同じに扱える訳じゃない(表)
※追記
- TS2.0.3でString以外のリテラル型が追加された
- http://qiita.com/vvakame/items/826bf193dd301862014f#literal-types%E3%81%AE%E6%8B%A1%E5%A4%A7