TypeScript
StringLiteralType

今すぐ知るべきTypeScriptのストリングリテラル型

More than 1 year has passed since last update.

TypeScript1.8の目玉機能と言えば、 ストリングリテラル(String literal)型 だ。

ストリングリテラル型は文字列型(string型)にさらに制約を加えて、 特定の文字列のみ許可する ものだ1。JavaScript的にはただの文字列で、コンパイルタイムに型チェックされる。

ストリングリテラル型のいい所

何と言ってもJSに多い、引数に文字列キーを渡して処理を分ける関数をちゃんとチェックできるようになるという点。他言語ならenumで処理する所だが、なぜかTypeScriptには数値enumはあるのに文字列enumがなく、有効な手段がとれているとは言いがたかった。

一応これまでも関数オーバーロードの形で、部分的にストリングリテラル型っぽいことは出来ていた。だが、関数を文字列キーの数だけ宣言する必要があり、決して使いやすくはなかった。

ストリングリテラル型の登場で、関数の引数以外にも使える箇所が広がり、文字列enumの扱いはよりやりやすくなった2

現在のベターな文字列enum
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")){}
TypeAliasでもOK
type A = "a";
function f(a:A){};
Union型や交差型でならOK
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は以下のように書けるようになるはず。

#6554が取り込まれたらこう書ける
type Enum = "a" | "b" | "c";
namespace Enum{
    export const A = "a"
    export const B = "b"
    export const C = "c"
}

まとめ

  • ストリングリテラル型は便利
  • でも、既存の型と全く同じに扱える訳じゃない(

※追記


  1. C++やDで「文字列リテラル型」と言われているものとは別。flowtypeがほぼ同じものを既に実装している。 

  2. 参考:TypeScriptでのイベント名を管理・指定するもう一つの方法(+ストリングリテラル型にも対応)