TypeScriptは型がついたJavaScriptです。プログラミングにおいて型があることの恩恵は大きく、近頃AltJSの代表格として人気を集めています。TypeScriptはもともと型のないJavaScriptで書かれるコードに型を付けることを使命としていることもあり、たまに変な型が追加されます。例えばTypeScript2.8で追加されたconditional typesはずいぶん注目を集めました。これによってTypeScriptの型システムの表現力が広がりましたが、一方でTypeScriptを書いている人の中には、よく分からない型が増えてついて行けない、一部の人たちが長くてよく分からない型定義を書いて喜んでいるだけと思っている方もいるのではないでしょうか。実際、健全にJavaScriptを書いていれば、自分でそのような変な型を書くことはあまり多くありません。
そこで、この記事ではTypeScriptの型について初歩から解説します。対象読者はTypeScriptを使っているけど型のことはよく分からない人やTypeScriptを使っていないけどTypeScriptの型システムに興味がある人などです。なお、型入門なのでTypeScriptの文法などは解説しません。具体的な文法などを知らなくても理解できるようにしていますが、文法を知りたいという方は先に他の入門記事を読むことをおすすめします。
※ 最終更新: 2021-09-12 (TypeScript 4.4対応)
プリミティブ型
TypeScriptにおいて一番基本となる型はプリミティブ型です。これはJavaScriptのプリミティブの型と対応しており、string
, number
, boolean
, symbol
, bigint
, null
, undefined
があります。これらは互いに異なる型であり、相互に代入不可能です。
const a: number = 3;
const b: string = a; // エラー: Type 'number' is not assignable to type 'string'.
ただし、コンパイラオプションで--strictNullChecks
をオンにしていない場合は、null
とundefined
は他の型の値として扱うことができます。
const a: null = null;
const b: string = a; // strictNullChecksがオンならエラー
逆に言うと、--strictNullChecks
をオンにしないとundefinedやnullが紛れ込む可能性があり危険です。このオプションはTypeScript2.0で追加されたもので、それまで適当だったundefinedやnullの扱いを改善する素晴らしいオプションです。このオプションは常にオンにするのがよいでしょう。
リテラル型
プリミティブ型を細分化したものとしてリテラル型があります。リテラル型には文字列のリテラル型と数値のリテラル型と真偽値のリテラル型があります。それぞれ、'foo'
や3
やtrue
というような名前の型です。
お分かりのように、'foo'
というのは'foo'
という文字列しか許されない型です。同様に、0
というのは0
という数値しか許されない型になります。
const a: 'foo' = 'foo';
const b: 'bar' = 'foo'; // エラー: Type '"foo"' is not assignable to type '"bar"'.
文字列のリテラル型はstring
の部分型であり、文字列のリテラル型を持つ値はstring型として扱うことができます。他の型も同様です。
const a: 'foo' = 'foo';
const b: string = a;
リテラル型と型推論
上の例では変数に全て型注釈を付けていましたが、これを省略してもちゃんと推論されます。
const a = 'foo'; // aは'foo'型を持つ
const b: 'bar' = a; // エラー: Type '"foo"' is not assignable to type '"bar"'.
これは、'foo'
というリテラルの型が'foo'
型であると推論されることによります。変数aは'foo'
が代入されているので、aの型も'foo'
型となります。
ただし、これはconstを使って変数を宣言した場合です。JavaScriptにおけるconstは変数に再代入されないことを保証するものですから、aにはずっと'foo'
が入っていることが保証され、aの型を'foo'
とできます。一方、constではなくletやvarを使って変数を宣言した場合は変数をのちのち書き換えることを意図していると考えられますから、最初に'foo'
が入っていたからといって変数の型を'foo'
型としてしまっては、他の文字列を代入することができなくて不便になってしまいます。そこで、letやvarで変数が宣言される場合、推論される型はリテラル型ではなく対応するプリミティブ型全体に広げられます。
let a = 'foo'; // aはstring型に推論される
const b: string = a;
const c: 'foo' = a; // エラー: Type 'string' is not assignable to type '"foo"'.
上の例では、aはletで宣言されているのでstring
型と推論されます。そのため、'foo'
型を持つcに代入することはできなくなります。
なお、letで宣言する場合も型注釈をつければリテラル型を持たせることができます。
let a: 'foo' = 'foo';
a = 'bar'; // エラー: Type '"bar"' is not assignable to type '"foo"'.
オブジェクト型
JavaScriptの基本的な概念としてオブジェクトがあります。オブジェクトは任意の数のプロパティがそれぞれ値を保持している構造です。もちろん、TypeScriptにはオブジェクトを表現するための型があります。これは、{ }
の中にプロパティ名とその型を列挙するものです。
例えば{foo: string; bar: number}
という型は、fooというプロパティがstring
型の値を持ちbarというプロパティがnumber
型の値を持つようなオブジェクトの型です。
interface MyObj {
foo: string;
bar: number;
}
const a: MyObj = {
foo: 'foo',
bar: 3,
};
なお、この例でinterfaceという構文が出てきましたが、これはTypeScript独自の(JavaScriptには存在しない)構文であり、オブジェクト型に名前を付けることができます。この例では、{foo: string; bar: number}
という型にMyObj
という名前を付けています。また、分かりやすくするためにconstに型注釈を付けていますが、型注釈をしなくてもオブジェクト型を推論してくれます。
もちろん、型が合わないオブジェクトを変数に代入したりしようとすると型エラーとなります。下記の例では、aに代入しようとしているオブジェクトはbarプロパティの型が違うためエラーになり、bに代入しようとしているオブジェクトはbarプロパティが無いためエラーとなります。
interface MyObj {
foo: string;
bar: number;
}
// エラー:
// Type '{ foo: string; bar: string; }' is not assignable to type 'MyObj'.
// Types of property 'bar' are incompatible.
// Type 'string' is not assignable to type 'number'.
const a: MyObj = {
foo: 'foo',
bar: 'BARBARBAR',
};
// エラー:
// Type '{ foo: string; }' is not assignable to type 'MyObj'.
// Property 'bar' is missing in type '{ foo: string; }'.
const b: MyObj = {
foo: 'foo',
};
JavaScriptではオブジェクトは自由に書き換えることができます。プロパティの書き換えはもちろん、プロパティを作ったり消したりすることもできます。しかし、TypeScriptではそのような操作は型によって制限されます。そうでないと型を導入した意味がありませんね。
一方、TypeScriptでは構造的部分型を採用しているため、次のようなことが可能です。
interface MyObj {
foo: string;
bar: number;
}
interface MyObj2 {
foo: string;
}
const a: MyObj = {foo: 'foo', bar: 3};
const b: MyObj2 = a;
MyObj2
というのはfooプロパティだけを持つオブジェクトの型ですが、MyObj2
型変数にMyObj
型の値a
を代入することができています。MyObj
型の値はstring
型のプロパティfooを持っているためMyObj2
型の値の要件を満たしていると考えられるからです。ちなみに、一般にこのような場合MyObj
はMyObj2
の部分型であると言います。
ただし、オブジェクトリテラルに対しては特殊な処理があります。次のような場合には注意してください。
interface MyObj {
foo: string;
bar: number;
}
interface MyObj2 {
foo: string;
}
// エラー:
// Type '{ foo: string; bar: number; }' is not assignable to type 'MyObj2'.
// Object literal may only specify known properties, and 'bar' does not exist in type 'MyObj2'.
const b: MyObj2 = {foo: 'foo', bar: 3};
変数bに代入しようとしている{foo: 'foo', bar: 3}
はfooプロパティがstring
型を持つため、先ほどの説明からすれば、barプロパティが余計であるもののMyObj2
型の変数に代入できるはずです。しかし、オブジェクトリテラルの場合は余計なプロパティを持つオブジェクトは弾かれてしまうのです。
これは、多くの場合余計なプロパティを持つオブジェクトリテラルを意図的に用いることが少なく、ミスである可能性が高いからでしょう。実際、TypeScriptの型システムを順守している限り、このような余計なプロパティは存在しないものと扱われるためアクセスする手段が無く、無駄です。どうしてもこのような操作をしたい場合はひとつ前の例のように別の変数に入れることになります。一度値を変数に入れるだけで挙動が変わるというのは直感的ではありませんが、TypeScriptでは入口だけ見ていてやるからあとは自己責任でということなのでしょう。
なお、上の例では変数bがMyObj2
型であることを明示していましたが、関数引数の場合でも同じ挙動となります。
interface MyObj2 {
foo: string;
}
// エラー:
// Argument of type '{ foo: string; bar: number; }' is not assignable to parameter of type 'MyObj2'.
// Object literal may only specify known properties, and 'bar' does not exist in type 'MyObj2'.
func({foo: 'foo', bar: 3});
function func(obj: MyObj2): void {
}
オブジェクト型についてはまだ紹介すべきことが色々とありますが、都合上いったん後回しにします。
配列型
JavaScriptでは配列はオブジェクトの一種ですが、配列の型を表すために特別な文法が用意されています。配列の型を表すためには[]
を用います。例えば、number[]
というのは数値の配列を表します。
const foo: number[] = [0, 1, 2, 3];
foo.push(4);
また、TypeScriptにジェネリクスが導入されて以降はArray<number>
と書くことも可能です。ジェネリクスについてはあとで述べます。
関数型
JavaScriptの、というより大抵のプログラミング言語において重要な概念として関数があります。TypeScriptにも当然ながら関数の型、すなわち関数型があります。関数型は例えば(foo: string, bar: number)=> boolean
のように表現されます。これは、第1引数としてstring
型の、第2引数としてnumber
型の引数をとり、返り値としてboolean
型の値を返す関数の型です。型に引数の名前が書いてありますが、これは型の一致等の判定には関係ありません。よって、(foo: number)=> string
型の値を(arg1: number)=> string
型の変数に代入するようなことは問題なく行えます。
function宣言などによって作られた関数にもこのような関数型が付きます。
const f: (foo: string)=> number = func;
function func(arg: string): number {
return Number(arg);
}
関数の部分型関係
関数型に対しては、普通の部分型関係があります。
interface MyObj {
foo: string;
bar: number;
}
interface MyObj2 {
foo: string;
}
const a: (obj: MyObj2)=>void = ()=>{};
const b: (obj: MyObj)=>void = a;
この例に見えるように、(obj: MyObj2)=>void
型の値を(obj: MyObj)=>void
型の値として扱うことができます。これは、MyObj
はMyObj2
の部分型なので、MyObj2
を受け取って処理できる関数はMyObj
を受け取っても当然処理できるだろうということです。aとbの型を逆にすると当然エラーになります。1
また、関数の場合、引数の数に関しても部分型関係が発生します。
const f1: (foo: string)=>void = ()=>{};
const f2: (foo: string, bar: number)=>void = f1;
このように、(foo: string)=>void
型の値を(foo: string, bar: number)=>void
型の値として使うことができます。すなわち、引数を1つだけ受け取る関数は、引数を2つ受け取る関数として使うことが可能であるということです。これは、関数の側で余計な引数を無視すればいいので自然ですね。
ただし、関数を呼び出す側で余計な引数を付けて呼び出すことはできないので注意してください。これは先のオブジェクトリテラルの例と同じくミスを防止するためでしょう。
const f1: (foo: string)=>void = ()=>{};
// エラー: Expected 1 arguments, but got 2.
f1('foo', 3);
可変長引数
JavaScriptには、可変長引数という機能があります。それは、(foo, ...bar)=> bar
のように、最後の引数を...bar
のようにすると、それ以降(この関数の場合は2番目以降)の引数が全部入った配列がbar
に渡されるというものです。
const func = (foo, ...bar)=> bar;
console.log(func(1, 2, 3)); // [2, 3]
TypeScriptでも可変長引数の関数を宣言できます。その場合、可変長引数の部分の型は配列にします。次の例では...bar
にnumber[]
型が付いているため、2番目以降の引数は全て数値でなければいけません。
const func = (foo: string, ...bar: number[]) => bar;
func('foo');
func('bar', 1, 2, 3);
// エラー: Argument of type '"hey"' is not assignable to parameter of type 'number'.
func('baz', 'hey', 2, 3);
void型
先ほどから何気なくvoid
という型が出てきていますので、これについて解説します。この型は主に関数の返り値の型として使われ、「何も返さない」ことを表します。
JavaScriptでは何も返さない関数(return文が無い、もしくは返り値の無いreturn文で返る)はundefinedを返すことになっていますので、void型というのはundefinedのみを値にとる型となります。実際、void型の変数にundefinedを入れることができます。ただし、その逆はできません。すなわち、void型の値をundefined型の変数に代入することはできません。
const a: void = undefined;
// エラー: Type 'void' is not assignable to type 'undefined'.
const b: undefined = a;
この挙動は、void型を返す関数というのはあくまで何も返さない関数なのだから、その値を利用することはできないという意図があると思われます。
void型の使いどころは、やはり関数の返り値としてです。何も返さない関数の返り値の型としてvoid型を使います。void型はある意味特殊な型であり、返り値がvoid型である関数は、値を返さなくてもよくなります。逆に、それ以外の型の場合(any型を除く)は必ず返り値を返さなければいけません。
function foo(): void {
console.log('hello');
}
// エラー: A function whose declared type is neither 'void' nor 'any' must return a value.
function bar(): undefined {
console.log('world');
}
なお、大して意味はありませんが、undefinedをvoid型の値として扱うことができるので、void型を返す関数にreturn undefined;
と書くことができます。
any型
ここで、any型という言葉が出てきましたので、これについても解説します。any型は何でもありな型であり、プログラマの敗北です。
any型の値はどんな型とも相互変換可能であり、実質TypeScriptの型システムを無視することに相当します。
const a: any = 3;
const b: string = a;
この例では、変数aはany型の変数ですので、どんな値でも代入可能です。また、any型の値はどんな型の値としても利用可能ですので、any型の値をもつaをstring型の変数bに代入することができます。
上のプログラムを見ると最終的に数値がstring型の値に入ってしまっています。このように、any型を使うと型システムを欺くことが可能であり、TypeScriptを使っている意味が薄れてしまいます。ですので、any型はやむを得ない場面でのみ使用するのがよいでしょう。
クラスの型
最近のJavaScriptにはクラスを定義する構文があります。TypeScriptでは、クラスを定義すると同時に同名の型も定義されます。
class Foo {
method(): void {
console.log('Hello, world!');
}
}
const obj: Foo = new Foo();
この例では、クラスFooを定義したことで、Foo
という型も同時に定義されました。Foo
というのは、クラスFooのインスタンスの型です。上の例の最後の文はFooが2種類あって分かりにくいですが、obj: Foo
のFooは型名のFooであり、new Foo()
のFooはクラス(コンストラクタ)の実体としてのFooです。
注意すべきは、TypeScriptはあくまで構造的型付けを採用しているということです。JavaScriptの実行時にはあるオブジェクトがあるクラスのインスタンスか否かということはプロトタイプチェーンによって特徴づけられますが、TypeScriptの型の世界においてはそうではありません2。具体的には、ここで定義された型Foo
というのは次のようなオブジェクト型で代替可能です。
interface MyFoo {
method: ()=> void;
}
class Foo {
method(): void {
console.log('Hello, world!');
}
}
const obj: MyFoo = new Foo();
const obj2: Foo = obj;
ここでMyFoo
という型を定義しました。これはmethodという関数型のプロパティ(すなわちメソッド)を持つオブジェクトの型です。実はFoo
型というのはこのMyFoo
型と同じです。クラスFooの定義から分かるように、Fooのインスタンス、すなわちFoo型の値の特徴はmethodというプロパティを持つことです。よって、その特徴をオブジェクト型として表現したMyFoo
型と同じと見なすことができるのです。
ジェネリクス
型がある言語にはいわゆるジェネリクスというものがよく存在します。いわゆる多相型に関連するものです。TypeScriptにもジェネリクスがあります。
型名をFoo<S, T>
のようにする、すなわち名前のあとに< >
で囲った名前の列を与えることで、型の定義の中でそれらの名前を型変数として使うことができます。
interface Foo<S, T> {
foo: S;
bar: T;
}
const obj: Foo<number, string> = {
foo: 3,
bar: 'hi',
};
この例ではFooは2つの型変数S, Tを持ちます。Fooを使う側ではFoo<number, string>
のように、SとTに当てはまる型を指定します。
他に、クラス定義や関数定義でも型変数を導入できます。
class Foo<T> {
constructor(obj: T) {
}
}
const obj1 = new Foo<string>('foo');
function func<T>(obj: T): void {
}
func<number>(3);
ところで、上の例でfuncの型はどうなるでしょうか。実は、<T>(obj: T)=> void
という型になります。
function func<T>(obj: T): void {
}
const f: <T>(obj: T)=> void = func;
このように、関数の場合は呼び出すまでどのような型引数で呼ばれるか分からないため、型にも型変数が残った状態になります。
余談ですが、型引数(func<number>(3)
の<number>
部分)は省略できます。
function identity<T>(value: T): T {
return value;
}
const value = identity(3);
// エラー: Type '3' is not assignable to type 'string'.
const str: string = value;
この例ではidentityは型変数Tを持ちますが、identityを呼び出す側ではTの指定を省略しています。この場合引数の情報からT
が推論されます。実際、今回引数に与えられている3は3
型の値なので、T
が3
に推論されます。identityの返り値の型はT
すなわち3
なので、変数valueの型は3
となります。3
型の値はstring
型の変数に入れることができないので最終行ではエラーになっています。この例からT
が正しく推論されていることが分かります。
ただし、複雑なことをする場合は型変数が推論できないこともあります。
タプル型
TypeScriptはタプル型という型も用意しています。ただし、JavaScriptにはタプルという概念はありません。そこで、TypeScriptでは配列をタプルの代わりとして用いることにしています3。これは、関数から複数の値を返したい場合に配列に入れてまとめて返すみたいなユースケースを想定していると思われます。
タプル型は[string, number]
のように書きます。これは実際のところ、長さが2の配列で、0番目に文字列が、1番目に数値が入ったようなものを表しています。
const foo: [string, number] = ['foo', 5];
const str: string = foo[0];
function makePair(x: string, y: number): [string, number] {
return [x, y];
}
ただし、タプル型の利用は注意する必要があります。TypeScriptがタプルと呼んでいるものはあくまで配列ですから、配列のメソッドで操作できます。
const tuple: [string, number] = ['foo', 3];
tuple.pop();
tuple.push('Hey!');
const num: number = tuple[1];
このコードはTypeScriptでエラー無くコンパイルできますが、実際に実行すると変数numに入るのは数値ではなく文字列です。このあたりはTypeScriptの型システムの限界なので、タプル型を使用するときは注意するか、あるいはそもそもこのようにタプル型を使うのは避けたほうがよいかもしれません。
ちなみに、0要素のタプル型なども作ることができます。
const unit: [] = [];
また、TypeScriptのタプル型は、可変長のタプル型の宣言が可能です。それはもはやタプルなのかという疑問が残りますが、これは実質的には最初のいくつかの要素の型が特別扱いされたような配列の型となります。
type NumAndStrings = [number, ...string[]];
const a1: NumAndStrings = [3, 'foo', 'bar'];
const a2: NumAndStrings = [5];
// エラー: Type 'string' is not assignable to type 'number'.
const a3: NumAndStrings = ['foo', 'bar'];
このように、可変長タプル型は最後に...(配列型)
という要素を書いたタプル型として表されます。ここで定義したNumAndStrings
型は、最初の要素が数値で、残りは文字列であるような配列の型となります。変数a3
は、最初の要素が数値でないのでエラーとなります。もちろん、[number, string, ...any[]]
のように型を指定された要素が複数あっても構いません。
...配列
はタプル型の他の場所にも書くことができます。例えば、最後の要素だけnumber
型で他はstring
型の配列は次のように書けます。
type StrsAndNumber = [...string[], number];
const b1: StrsAndNumber = ['foo', 'bar', 'baz', 0];
const b2: StrsAndNumber = [123];
// エラー:
// Type '[string, string]' is not assignable to type 'StrsAndNumber'.
// Type at position 1 in source is not compatible with type at position 1 in target.
// Type 'string' is not assignable to type 'number'.
const b3: StrsAndNumber = ['foo', 'bar'];
ただし、...
を使えるのはタプル型のどこかに1回だけです。例えば「まず数値が並んで次に文字列が並ぶ配列の型」として[...number[], ...string[]]
のような型を考えたくなるかもしれませんが、これは...
を2回使っているのでだめです。
さらに、オプショナルな要素を持つタプル型もあります。これは、[string, number?]
のように型に?
が付いた要素を持つタプル型です。この場合、2番目の要素はあってもいいし無くてもいいという意味になります。ある場合はnumber
型でなければなりません。
type T = [string, number?];
const t1: T = ['foo'];
const t2: T = ['foo', 3];
オプショナルな要素は複数あっても構いませんが、そうでない要素より後に来なければいけません。例えば[string?, number]
のような型はだめです。
タプル型と可変長引数
実は、最近(TypeScript 3.0)になってタプル型の面白い使い道が追加されました。それは、タプル型を関数の可変長引数の型を表すのに使えるというものです。
type Args = [string, number, boolean];
const func = (...args: Args) => args[1];
const v = func('foo', 3, true);
// vの型はnumber
ちょっと前に、可変長引数の型として配列を用いるということを紹介しましたが、実は配列の代わりにタプル型を用いることができます。上の例では、可変長引数args
の型はArgs
、すなわち[string, number, boolean]
です。これに合わせるために、すなわち引数の列args
が型Args
を持つようにするためには、関数funcの最初の引数の型はstring
、次はnumber
、その次はboolean
でなければいけません。もはや可変長という名前が嘘っぱちになっていますが、このようにタプル型を型の列として用いることによって、複数の引数の型をまとめて指定することができるのです。
ここで可変長タプルを用いた場合、引数の可変長性が保たれることになります。
type Args = [string, ...number[]];
const func = (f: string, ...args: Args) => args[0];
const v1 = func('foo', 'bar');
const v2 = func('foo', 'bar', 1, 2, 3);
また、同様に、オプショナルな要素を持つタプルの型を用いた場合はオプショナルな引数を持つ関数の型ができることになります。
関数呼び出しのspreadとタプル型
ところで、JavaScriptでは...
という記法は関数呼び出しのときにも使うことができます。
const func = (...args: string[]) => args[0];
const strings: string[] = ['foo', 'bar', 'baz'];
func(...strings);
func(...strings)
の意味は、配列strings
の中身をfunc
の引数に展開して呼び出すということです。つまり、func
の最初の引数はstrings
の最初の要素になり、2番目の引数は2番目の要素に……となります。
タプル型はここでも使うことができます。適切なタプル型の配列を...
で展開することで、型の合った関数を呼び出すことができるのです。
const func = (str: string, num: number, b: boolean) => args[0] + args[1];
const args: [string, number, boolean] = ['foo', 3, false];
func(...args);
タプル型と可変長引数とジェネリクス
さて、以上の知識とジェネリクスを組み合わせることによって、面白いことができるようになります。タプル型をとるような型変数を用いることで、関数の引数列をジェネリクスで扱うことができるのです。
例として、関数の最初の引数があらかじめ決まっているような新しい関数を作る関数bind
を書いてみます。
function bind<T, U extends any[], R>(
func: (arg1: T, ...rest: U) => R,
value: T,
): ((...args: U) => R) {
return (...args: U) => func(value, ...args);
}
const add = (x: number, y: number) => x + y;
const add1 = bind(add, 1);
console.log(add1(5)); // 6
// Argument of type '"foo"' is not assignable to parameter of type 'number'.
add1('foo');
関数bind
は2つの引数func
とvalue
を取り、新しい関数(...args: U) => func(value, ...args)
を返します。この関数は、受け取った引数列args
に加えて最初の引数としてvalue
をfunc
に渡して呼び出した返り値をそのまま返す関数です。
ポイントは、まずU extends any[]
の部分ですね。これは新しい記法ですが、型引数U
はany[]
の部分型でなければならないという意味です。string[]
などの配列型に加えてタプル型も全部any[]
の部分型です。この制限を加えることにより、...rest: U
のように可変長引数の型としてU
を使うことができます。
加えて、bind(add, 1)
の呼び出しでは型変数はそれぞれT = number, U = [number], R = number
と推論されます。返り値の型は(...args: U) => R
すなわち(arg: number) => number
となります。特に、U
がタプル型に推論されるのが偉いですね。これにより、add
の引数の情報が失われずにadd1
に引き継がれています。
union型(合併型)
さて、ジェネリクスなど、ここまで説明してきた要素の多くは型のある言語なら普通にあるものだと思います。しかし、ここで紹介するunion型を持っている言語はそこまで多くないのではないかと思います。TypeScriptはこのunion型のサポートに力を入れています。
union型は値が複数の型のどれかに当てはまるような型を表しています。記法としては、複数の型を|
でつなぎます。例えば、string | number
という型は「string
またはnumber
である値の型」、すなわち「文字列または数値の型」となります。
let value: string | number = 'foo';
value = 100;
value = 'bar';
// エラー: Type 'true' is not assignable to type 'string | number'.
value = true;
この例では変数valueがstring | number
型の変数となっていますので、文字列や数値を代入することができますが、真偽値は代入することができません。
もちろん、プリミティブ型だけでなくオブジェクトの型でもunion型を作ることができます。
interface Hoge {
foo: string;
bar: number;
}
interface Piyo {
foo: number;
baz: boolean;
}
type HogePiyo = Hoge | Piyo;
const obj: HogePiyo = {
foo: 'hello',
bar: 0,
};
ここでtype文というのが登場していますが、これはTypeScript独自の文であり、新しい型を定義して名前を付けることができる文です。この例ではHogePiyo
という型をHoge | Piyo
として定義しています。
union型の絞り込み
union型の値というのはそのままでは使いにくいものです。例えば、上で定義したHogePiyo
型のオブジェクトは、bar
プロパティを参照することができません。なぜなら、HogePiyo
型の値はHoge
かもしれないしPiyo
かもしれないところ、barプロパティはHoge
にはありますがPiyo
には無いからです。無い可能性があるプロパティを参照することはできません。同様に、baz
プロパティも参照できません。foo
プロパティは両方にあるので参照可能です(後述)。
普通は、Hoge | Piyo
のような型の値が与えられる場合、まずその値が実際にはどちらかのかを実行時に判定する必要があります。そこで、TypeScriptではそのような判定を検出して適切に型を絞り込んでくれる機能があります。
interface Hoge {
foo: string;
bar: number;
}
interface Piyo {
foo: number;
baz: boolean;
}
function useHogePiyo(obj: Hoge | Piyo): void {
// ここではobjはHoge | Piyo型
if ('bar' in obj) {
// barプロパティがあるのはHoge型なのでここではobjはHoge型
console.log('Hoge', obj.bar);
} else {
// barプロパティがないのでここではobjはPiyo型
console.log('Piyo', obj.baz);
}
}
この例ではin演算子を使った例です。'bar' in obj
というのはbarというプロパティがobjに存在するならtrueを返し、そうでないならfalseを返す式です。
in演算子を使ったif文を書くことで、if文のthen部分とelse部分でobjの型がそれぞれHoge
やPiyo
として扱われます。このようにして変数の型を絞り込むことができます。
ただし、この例は注意が必要です。なぜなら、次のようなコードを書くことができるからです。
const obj: Hoge | Piyo = {
foo: 123,
bar: 'bar',
baz: true,
};
useHogePiyo(obj);
objに代入されているのはPiyo
型のオブジェクトに余計なbarプロパティが付いたものです。ということはこれはPiyo
型のオブジェクトとみなせるので、Hoge | Piyo
型の変数にも代入可能です。これをuseHogePiyoに渡すと良くないことが起こりますね。objは実際Piyo
型なのに、これを実行すると'bar' in obj
が成立するのでobjがHoge
型と見なされているところに入ってしまいます。obj.bar
を参照していますが、これはHoge
型のプロパティなのでnumber
型が期待されているところ、実際は文字列が入っています。
このようにin演算子を用いた型の絞り込みは比較的最近 (TypeScript 2.7)入った機能ですが、ちょっと怖いので自分はあまり使いたくありません。
typeofを用いた絞り込み
もっと単純な例として、string | number
型を考えましょう。これに対する絞り込みはtypeof演算子でできます。typeof演算子は与えられた値の型を文字列で返す演算子です。
function func(value: string | number): number {
if ('string' === typeof value) {
// valueはstring型なのでlengthプロパティを見ることができる
return value.length;
} else {
// valueはnumber型
return value;
}
}
オブジェクトが絡まないこともあり、これなら安全ですね。
nullチェック
もうひとつunion型がよく使われる場面があります。それはnullableな値を扱いたい場合です。(JavaScriptなのでundefinedもありますが。)
例えば、文字列の値があるかもしれないしnullかもしれないという状況はstring | null
という型で表すことができます。string | null
型の値はnullかもしれないので、文字列として扱ったりプロパティを参照したりすることができません。これに対し、nullでなければ処理したいという場面はよくあります。JavaScriptにおける典型的な方法はvalue != null
のようにif文でnullチェックを行う方法ですが、TypeScriptはこれを適切に解釈して型を絞り込んでくれます。
function func(value: string | null): number {
if (value != null) {
// valueはnullではないのでstring型に絞り込まれる
return value.length;
} else {
return 0;
}
}
また、&&
や||
が短絡実行するという挙動を用いたテクニックもJavaScriptではよく使われますが、これもTypeScriptは適切に型検査してくれます。上の関数funcは次のようにも書くことができます。
function func(value: string | null): number {
return value != null && value.length || 0;
}
代数的データ型っぽいパターン
これまで見たように、プリミティブ型ならunion型の絞り込みはけっこういい感じに動いてくれます。しかし、やはりオブジェクトに対してもいい感じにunion型を使いたいという需要はあります。そのような場合に推奨されているパターンとして、リテラル型とunion型を組み合わせることでいわゆる代数的データ型(タグ付きunion)を再現する方法があります。
interface Some<T> {
type: 'Some';
value: T;
}
interface None {
type: 'None';
}
type Option<T> = Some<T> | None;
function map<T, U>(obj: Option<T>, f: (obj: T)=> U): Option<U> {
if (obj.type === 'Some') {
// ここではobjはSome<T>型
return {
type: 'Some',
value: f(obj.value),
};
} else {
return {
type: 'None',
};
}
}
これは値があるかもしれないし無いかもしれないことを表すいわゆるoption型をTypeScriptで表現した例です。Option<T>
型は、ある場合のオブジェクトの型であるSome<T>
型と無い場合の型であるNone
型のunionとして表現されています。ポイントは、これらに共通のプロパティであるtypeです。typeプロパティには、このオブジェクトの種類(SomeかNoneか)を表す文字列が入っています。ここでtypeプロパティの型としてリテラル型を使うことによって、Option<T>
型の値obj
に対して、obj.type
が'Some'
ならばobj
の型はSome<T>
で'None'
ならばobj
の型はNone
であるという状況を作っています。関数mapの中ではobj.type
の値によって分岐することにより型の絞り込みを行っています。この方法はTypeScriptで推奨されている方法らしく、コンパイラのサポートも厚いです。
次のようにswitch文でも同じことができます。大抵はどちらでも良いですが、こちらのほうが良い挙動を示す場合があります。
function map<T, U>(obj: Option<T>, f: (obj: T)=> U): Option<U> {
switch (obj.type) {
case 'Some':
return {
type: 'Some',
value: f(obj.value),
};
case 'None':
return {
type: 'None',
};
}
}
このmap関数の場合はこちらのほうが拡張に対して強くて安全です。例えばOption<T>
に第3の種類の値を追加した場合、if文のバージョンではその値に対してもNone
が返るのに対し、switch文のバージョンではそのままではコンパイルエラーが出ます(第3の値に対する処理が定義されておらずmap関数が値を返さない可能性が生じてしまうため)。そのため、関数の変更の必要性にすぐ気づくことができます。
このパターンはTypeScriptプログラミングにおいて頻出です。オブジェクトで真似をしているため本物の代数的データ型に比べると記法が重いのが難点ですが仕方ありません。
union型オブジェクトのプロパティ
オブジェクト同士のunion型を作った場合、そのプロパティアクセスの挙動は概ね期待通りの結果となります。少し前の例に出てきたHogePiyo
型を思い出しましょう。
interface Hoge {
foo: string;
bar: number;
}
interface Piyo {
foo: number;
baz: boolean;
}
type HogePiyo = Hoge | Piyo;
function getFoo(obj: HogePiyo): string | number {
// obj.foo は string | number型
return obj.foo;
}
Hoge | Piyo
型の変数obj
のfoo
プロパティはアクセス可能であると述べましたが、実はその型はstring | number
型となります。これは、obj
がHoge
型の場合にobj.foo
はstring
型であり、obj
がPiyo
型の場合はobj.foo
はnumber
型となることから、obj.foo
はstring
型である場合とnumber
型である場合があるという説明ができます。一方、bar
やbaz
はHoge | Piyo
型には存在しない可能性があるためアクセスできません。この挙動は実情に合っているし直感的ですね。
配列の要素もオブジェクトのプロパティの一種なので、同じ挙動となります。
const arr: string[] | number[] = [];
// string[] | number[] 型の配列の要素は string | number 型
const elm = arr[0];
never型
union型を触り始めるとたまに出てくるのがnever型です。never型は「属する値が存在しない型」であり、部分型関係の一番下にある(任意の型の部分型となっている)型です。どんな値もnever型の変数に入れることはできません。
// エラー: Type '0' is not assignable to type 'never'.
const n: never = 0;
一方、never型の値はどんな型にも入れることができます。
// never型の値を作る方法が無いのでdeclareで宣言だけする
declare const n: never;
const foo: string = n;
こう聞くとany型のように危険な型であるように思えるかもしれませんが、そんなことはありません。never型に当てはまる値は存在しないため、never型の値を実際に作ることはできません。よって、(TypeScriptの型システムを欺かない限りは)never型の値を持っているという状況があり得ないので、never型の値を他の型の変数に入れるということがソースコード上であったとしても、実際には起こりえないのです。
何を言っているのか分からない人もいるかもしれませんが、型システムを考える上ではこのような型はけっこう自然に出てきます。とりあえず具体例を見てみましょう。これは先ほどのOption<T>
の例を少し変更したものです。
interface Some<T> {
type: 'Some';
value: T;
}
interface None {
type: 'None';
}
type Option<T> = Some<T> | None;
function map<T, U>(obj: Option<T>, f: (obj: T)=> U): Option<U> {
switch (obj.type) {
case 'Some':
return {
type: 'Some',
value: f(obj.value),
};
case 'None':
return {
type: 'None',
};
default:
// ここでobjはnever型になっている
return obj;
}
}
switch文にdefaultケースが追加されました。実はこの中でobjの型はnever
になっています。なぜなら、それまでのcase文によってobjの可能性が全て調べ尽くされてしまったからです。これが意味することは、実際にはdefault節が実行される可能性は無く、この中ではobjの値の候補が全く無いということです。そのような状況を、objにnever
型を与えることで表現しています。
また、もうひとつnever型が出てくる可能性があるのは、関数の返り値です。
function func(): never {
throw new Error('Hi');
}
const result: never = func();
関数の返り値の型がnever
型となるのは、関数が値を返す可能性が無いときです。これは返り値が無いことを表すvoid
型とは異なり、そもそも関数が正常に終了して値が返ってくるということがあり得ない場合を表します。上の例では、関数funcは必ずthrowします。ということは、関数の実行は中断され、値を返すことなく関数を脱出します。特に、上の例でfuncの返り値を変数resultに代入していますが、実際にはresultに何かが代入されることはあり得ません。ゆえに、resultにはnever型を付けることができるのです。
なお、上の例ではfuncの返り値に型注釈でnever
と書いていますが、省略すると返り値の型はvoid
に推論されます。これは値を返さないぞということを明示したい場合はnever
と型註釈で明示する必要があります。もし返り値をnever
型とすることが不可能(何か値が返る可能性を否定できない)な場合はちゃんと型エラーになるので安心です。
intersection型(交差型)
union型とある意味で対になるものとしてintersection型があります。2つの型T, Uに対してT & U
と書くと、T
でもありU
でもあるような型を表します。
interface Hoge {
foo: string;
bar: number;
}
interface Piyo {
foo: string;
baz: boolean;
}
const obj: Hoge & Piyo = {
foo: 'foooooooo',
bar: 3,
baz: true,
};
たとえばこの例では、Hoge & Piyo
というのはHoge
でもありPiyo
でもある型を表します。ですから、この型の値はstring
型のプロパティfooとnumber
型のプロパティbarを持ち、さらにboolean
型のプロパティbazを持つ必要があります。
ちなみに、union型とintersection型を組みあわせると楽しいです。次の例を見てください。
interface Hoge {
type: 'hoge';
foo: string;
}
interface Piyo {
type: 'piyo';
bar: number;
}
interface Fuga {
baz: boolean;
}
type Obj = (Hoge | Piyo) & Fuga;
function func(obj: Obj) {
// objはFugaなのでbazを参照可能
console.log(obj.baz);
if (obj.type === 'hoge') {
// ここではobjは Hoge & Fuga
console.log(obj.foo);
} else {
// ここではobjはPiyo & Fuga
console.log(obj.bar);
}
}
Obj
型は(Hoge | Piyo) & Fuga
ですが、実はこれは(Hoge & Fuga) | (Piyo & Fuga)
と同一視されます。よって、union型のときと同様にif文で型を絞り込むことができるのです。
union型を持つ関数との関係
一瞬だけ話をintersection型からunion型に戻します。先ほどは省略しましたが、関数型を含むunion型というものも考えることができます。当然ながら、関数とそれ以外のunion型を作った場合はそれを関数として呼ぶことはできません。下の例では、Func | MyObj
型の値obj
はMyObj
型の可能性がある、つまり関数ではない可能性がありますので、obj(123)
のように関数として使うことはできません。
type Func = (arg: number) => number;
interface MyObj {
prop: string;
}
const obj : Func | MyObj = { prop: '' };
// エラー: Cannot invoke an expression whose type lacks a call signature.
// Type 'MyObj' has no compatible call signatures.
obj(123);
では、union型の構成要素が全部関数だったら呼べそうな気がします。次の例はどうでしょうか。
type StrFunc = (arg: string) => string;
type NumFunc = (arg: number) => string;
declare const obj : StrFunc | NumFunc;
// エラー: Argument of type '123' is not assignable to parameter of type 'string & number'.
// Type '123' is not assignable to type 'string'.
obj(123);
この例では、StrFunc | NumFunc
型の変数obj
を作っています。StrFunc
型は文字列を受け取って文字列を返す関数の型で、NumFunc
型は数値を受け取って文字列を返す関数の型です。
しかし、obj
を呼ぶところでまだエラーが発生しています。エラーメッセージから察しがついている方もいらっしゃると思いますが、このStrFunc | NumFunc
型の関数を呼ぶことは実質できません。なぜなら、obj
はStrFunc
型かもしれないので引数は文字列でないといけません。一方、NumFunc
型かもしれないので引数は数値でないといけません。つまり、引数が文字列であることと数値であることが同時に要求されています。エラーメッセージに出てくるstring & number
という型はこのことを表しています。文字列であると同時に数値であるような値(つまりstring & number
型の値)は存在しないため、この関数を呼ぶことは実質的にできないのです。
このように、関数同士のunionを考えるとき、結果の関数の引数はもともとの引数同士のintersection型を持つ必要があります4。難しい言葉でいうと、これは関数の引数の型が反変 (contravariant) の位置にあることが影響しています。
引数の型がintersection型で表現されるということで、intersection型をとっても意味がある例を見てみます。
interface Hoge {
foo: string;
bar: number;
}
interface Piyo {
foo: string;
baz: boolean;
}
type HogeFunc = (arg: Hoge) => number;
type PiyoFunc = (arg: Piyo) => boolean;
declare const func: HogeFunc | PiyoFunc;
// resは number | boolean 型
const res = func({
foo: 'foo',
bar: 123,
baz: false,
});
この例ではfunc
はHogeFunc | PiyoFunc
型です。HogeFunc
の引数はHoge
でPiyoFunc
の引数はPiyo
なので、func
の引数の型はHoge & Piyo
型である必要があります。よって、Hoge & Piyo
型を持つオブジェクトを作ることでfunc
を呼ぶことができます。
この例ではres
の型はnumber | boolean
型となります。これは、func
の型がHogeFunc
の場合は返り値がnumber
であり、PiyoFunc
の場合は返り値がboolean
であることから説明できます。
このように、関数同士のunion型を持つ関数を呼びたい場合にintersection型の知識が役に立ちます。特に、先ほど見たようにエラーメッセージにintersection型が現れますから、intersection型のことも覚えておいたほうがよいでしょう。(そんな機会がどれだけあるのかは聞いてはいけません。)
なお、この辺りの処理は扱いが難しいためか、現在のところ制限があります。具体的には関数のオーバーロードがある場合やジェネリクスが関わる場合に関数が呼べなかったり、引数の型が推論できなかったりする場合があります。一応例だけ示しておきますが、困る機会はそんなに無いのではないかと思います。
const arr: string[] | number[] = [];
// エラー: Parameter 'x' implicitly has an 'any' type.
arr.forEach(x => console.log(x));
// エラー: Cannot invoke an expression whose type lacks a call signature.
const arr2 = arr.map(x => x);
オブジェクト型再訪
さて、union型を紹介したので、オブジェクト型にもうちょっと深入りして説明することができます。オブジェクト型はプロパティ名: 型;
という定義の集まりでしたが、実はプロパティに対して修飾子を付けることができます。修飾子は?
とreadonly
の2種類あります。
?
: 省略可能なプロパティ
?
を付けて宣言したプロパティは省略可能になります。
interface MyObj {
foo: string;
bar?: number;
}
let obj: MyObj = {
foo: 'string',
};
obj = {
foo: 'foo',
bar: 100,
};
この例ではbarが省略可能なプロパティです。barは省略可能なので、fooだけを持つオブジェクトと、fooとbarを両方持つオブジェクトはどちらもMyObj
型の値として認められます。
オプショナルなプロパティに対するアクセス
ところで、実際のJavaScriptでは存在しないプロパティにアクセスするとundefinedが返ります。ということは、MyObj
型の値に対してbarプロパティを取得しようとすると、undefinedの可能性があるということです。このことを反映して、MyObj
のbarプロパティにアクセスした場合に得られる型はnumber | undefined
となります。このように、?
修飾子を付けられたプロパティを取得する場合は自動的にundefined
型とのunion型になります。よって、それを使う側はこのようにundefinedチェックを行う必要があります。
function func(obj: MyObj): number {
return obj.bar !== undefined ? obj.bar * 100 : 0;
}
なお、?
を使わずに自分でbarの型をnumber | undefined
としても同じ意味にはなりません。
interface MyObj {
foo: string;
bar: number | undefined;
}
// エラー:
// Type '{ foo: string; }' is not assignable to type 'MyObj'.
// Property 'bar' is missing in type '{ foo: string; }'.
let obj: MyObj = {
foo: 'string',
};
?
修飾子を使わない場合は、たとえundefinedが許されているプロパティでもきちんと宣言しないといけないのです。多くの場合、bar?: number;
よりもbar: number | undefined
を優先して使用することをお勧めします。前者はbar
が無い場合に本当に無いのか書き忘れなのか区別ができず、ミスの原因になります。後者の場合は書き忘れを防ぐことができます。
本当に「無くても良い」場面は関数にオプションオブジェクトを渡すくらいしか無く、以下に紹介する記事でも「その他のオブジェクトが長期間生存するようなケースでは, そもそもオプショナルなプロパティ自体を避けましょう」とされています。筆者もこれに同意しており、「便利さ」よりも「安全性」を取りたい多くの場面ではオプショナルなプロパティではなくundefined
などとのユニオン型としたほうが賢明です。
exactOptionalPropertyTypes
コンパイラオプションとの関係
オプショナルなプロパティの挙動は、exactOptionalPropertyTypes
コンパイラオプションが有効かどうかによって変わります。デフォルトではこのオプションは無効で、比較的最近 (TypeScript 4.4) 追加されたオプションということもあり、無効にしているプロジェクトの方が多いでしょう。
このオプションが無効の場合は、bar?: number;
というのはbar?: number | undefined;
と書いたのと同じ意味になります。つまり、オプショナルなプロパティには明示的にundefined
を入れることができます。
// exactOptionalPropertyTypesが無効の場合
interface MyObj {
foo: string;
bar?: number;
}
// 全部OK
const obj1: MyObj = { foo: "pichu" };
const obj2: MyObj = { foo: "pikachu", bar: 25 };
const obj3: MyObj = { foo: "raichu", bar: undefined };
一方で、exactOptionalPropertyTypes
が有効の場合、オプショナルなプロパティにundefined
を入れることができなくなります。
// exactOptionalPropertyTypesが有効の場合
interface MyObj {
foo: string;
bar?: number;
}
const obj1: MyObj = { foo: "pichu" };
const obj2: MyObj = { foo: "pikachu", bar: 25 };
// エラー: Type 'undefined' is not assignable to type 'number'.
const obj3: MyObj = { foo: "raichu", bar: undefined };
これまでbar?: number;
と書いたら自動的にbar: undefined
が可能になるのはあまり直感的な挙動ではありませんでしたが、exactOptionalPropertyTypes
を有効にすることでこれが改善されます。
また、このオプションが有効な状態ではbar?: number;
と宣言されたプロパティに対しては「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
プロパティに対してもうひとつ可能な修飾子はreadonly
です。これを付けて宣言されたプロパティは再代入できなくなります。
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';
つまるところ、constのプロパティ版であると思えばよいでしょう。素のJavaScriptではプロパティのwritable属性が相当しますが、プロパティの属性を型システムに組み込むのは筋が悪くて厳しいためTypeScriptではこのような独自の方法をとっているのでしょう。
ただし、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にそのような型を与えたので、obj.foo
やobj.bar
などはみんなnumber
型を持っています。
これは便利ですが明らかに危ないですね。objは実際には{}
なのでobj.foo
などはundefinedになるはずなのに、その可能性が無視されています。
そんな危ない型が平然と許されている理由は、オブジェクトを辞書として使うような場合に必要だとか、配列型の定義にも必要とかそんなところでしょう。実際、配列型の定義は概ね下のような感じです。
interface Array<T> {
[idx: number]: T;
length: number;
// メソッドの定義が続く
// ...
}
なお、この例のようにインデックスシグネチャの他にプロパティがあった場合、そちらが優先されます。
一応最近のJavaScriptならば、インデックスシグネチャの利用をできるだけ避けることはできます。オブジェクトを辞書として使う場合は、代わりにMapを使いましょう。配列の場合は、インデックスによるアクセスを避けてfor-of文を使うなどの方法で避けられます。
関数シグネチャ
実は、オブジェクト型の記法で関数型を表現する方法があります。
interface Func {
(arg: number): void;
}
const f: Func = (arg: number)=> { console.log(arg); };
(arg: number): void;
の部分で、このオブジェクトはnumber
型の引数をひとつ取る関数であることを表しています。
この記法は通常のプロパティの宣言と同時に使うことができるので、関数だけど同時に特定のプロパティを持っているようなオブジェクトを表すことができます。さらに、複数の関数シグネチャを書くことができ、それによってオーバーローディングを表現できます。
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;
ここで作ったCtor<T>
型は、0引数でnewするとT
型の値が返るような関数を表しています。ここで定義したクラスFooはnewするとFooのインスタンス(すなわちFoo
型の値)が返されるので、Ctor<Foo>
に代入可能です。
ちなみに、関数の型を(foo: string) => number
のように書けたのと同様に、newシグネチャしかない場合はコンストラクタの型をnew() => Foo
のように書くこともできます。
as
によるダウンキャスト
ここで型に関連する話題として、as
によるダウンキャストを紹介します。これはTypeScript独自の構文で、式 as 型
と書きます。ダウンキャストなので当然型安全ではありませんが、TypeScriptを書いているとたまに必要になる場面があります。なお、ダウンキャストというのは、派生型の値を部分型として扱うためのものです。
const value = rand();
const str = value as number;
console.log(str * 10);
function rand(): string | number {
if (Math.random() < 0.5) {
return 'hello';
} else {
return 123;
}
}
この例でvalueはstring | number
型の値ですが、value as number
の構文によりnumber
型として扱っています。よって変数strはnumber
型となります。
これは安全ではありません。なぜなら、valueは実際にはstring
型、すなわち文字列かもしれないので、変数strに文字列が入ってしまう可能性があるからです。
なお、as
を使っても全く関係ない2つの値を変換することはできません。
const value = 'foo';
// エラー: Type 'string' cannot be converted to type 'number'.
const str = value as number;
その場合、any
型か後述のunknown
型を経由すれば変換できます。
const value = 'foo';
const str = value as unknown as number;
ちなみに、この例に見えるように、as
はアップキャストもできます。最初のas unknown
で行われているのはダウンキャストではなくアップキャストであり、その後as number
でダウンキャストしています。他のアップキャストの例としては、const foo: string = 'foo';
とする代わりにconst foo = 'foo' as string;
とするような場合が挙げられます('foo'
型をstring
型にアップキャストしている)。アップキャスト自体はas
を使わなくてもできる安全な操作です。アップキャストにas
を使うのは、危険なダウンキャストと見分けがつかないのでやめたほうがよいでしょう。
readonlyな配列とタプル
ちょっと前に、オブジェクト型のreadonly
修飾子を紹介しました。これによって、オブジェクトの特定のプロパティを型システム上readonly(書き換え不可)にすることができるのでした。実は、配列型やタプル型においてもreadonly
の概念が存在します。ただし、オブジェクトの場合はプロパティごとにreaodnly
か否かを制御できましたが、配列やタプルは要素ごとの制御はできません。配列やタプル全体がreadonly
か否かという区別をすることになります。
readonly
な配列型はreadonly T[]
のように書きます(T
は要素の型)。次の例はreadonly number[]
型の配列の例です。
// arrは readonly number[] 型
const arr: readonly number[] = [1, 2, 3];
// arrの要素を書き換えるのは型エラー
// エラー: Index signature in type 'readonly number[]' only permits reading.
arr[0] = 100;
// readonly配列型にはpushなどの書き換えメソッドは存在しない
// エラー: Property 'push' does not exist on type 'readonly number[]'
arr.push(4);
reaodnly
なプロパティと同じく、readonly
な配列のプロパティを書き換えようとするとエラーとなります。また、readonly
な配列は、push
などの配列を破壊的に書き換えるメソッドは除去されており使うことができません。この2つの機能により、readonly
配列の書き換え不可能性を型システム上で担保しています。
なお、T[]
型をArray<T>
と書くことができるのと同様に、readonly T[]
型はReadonlyArray<T>
と書けます。
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と呼ばれるもので、TypeScript 4.0で導入されました。この機能のすごい点は、...型変数
の形で使うことができ、型推論の材料にできることです。
function removeFirst<T, Rest extends readonly unknown[]>(
arr: [T, ...Rest]
): Rest {
const [, ...rest] = arr;
return rest;
}
// const arr: [number, number, string]
const arr = removeFirst([1, 2, 3, "foo"]);
この例では、変数arr
の型が[number, number, string]
であることが型推論されます。これは、removeFirst
の型引数T
およびRest
がそれぞれnumber
および[number, number, string]
であることがTypeScriptに理解されたことを意味しています。特に、引数[1, 2, 3, "foo"]
を引数の型[T, ...Rest]
に当てはめるという推論をTypeScriptが行なっています。これが...型変数
の型変数が推論の対象になるということです。
Variadic Tuple Typesの導入により、タプル型の操作に関するTypeScriptの推論能力が強化されました。上の例で言えば、removeFirst
内の変数rest
の型が自動的にRest
と推論されている点も注目に値します。
また、少し前に見たように、タプル型は関数の可変長引数の制御にも使われます。ここでもVariadic Tuple Typesが活躍するでしょう。
なお、型引数を...T
の形で使う場合はその型引数が配列型またはタプル型であるという制約をこちらで宣言してあげる必要があります。上の例のようにextends readonly unknown[]
とすると良いでしょう。
[...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;
}
// const arr1: number[]
const arr1 = func1([1, 2, 3]);
// const arr2: [number, number, number]
const arr2 = func2([1, 2, 3]);
この例のように、型引数の推論時に配列がT
型の引数に当てはめられた場合は配列型が推論されますが、[...T]
型の引数に当てはめられた場合はタプル型が推論されます。関数に渡された配列の各要素の型を得たいのに配列型になってしまうという場面で活用できるかもしれません。
テンプレートリテラル型
テンプレートリテラル型は、特定の形の文字列のみを受け入れる型です。この記事のはじめの方で見た文字列のリテラル型はただ一種類の文字列のみを受け入れる型でしたが、テンプレートリテラル型はもう少し柔軟です。
テンプレートリテラル型の構文はテンプレート文字列リテラルと類似しており、基本的には`なんらかの文字列`
という形で、その通りの文字列のみが受け入れられます。この中に混ぜて${ 型 }
という構文を入れることがで、その型
に当てはまる任意の値(の文字列表現)をそこに当てはめることができます。例えば、次の例で定義するHelloStr
型はHello,
の後に任意のstring
型の値がくる文字列という意味です。これは実質、Hello,
で始まる文字列だけを受け入れる型となります。
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!";
${string}
以外にもいくつかの型が使用可能です。例えば${number}
とすると数値のみが入れられるようになります。次の例のように、数値(number
型の値)を文字列に変換したときに可能な文字列が${number}
の位置に入るものとして受け入れられます。
ただし、Infinity
とNaN
は${number}
に含まれないようです。このため、${number}
はあまり実用的ではありません。実用に耐えるのは、${string}
や、あるいは文字列のリテラル型のユニオン型といったものを${ }
の中に入れる場合です。
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円":
テンプレートリテラル型はTypeScript 4.1 で導入された際に大きな話題になりました。それは、後述のconditional types(infer
)と組み合わせることで型レベル計算における文字列操作が可能となったからです。
as const
ちょっと前に出てきたreadonly
に関連する話題として、as const
を紹介します。これはTypeScriptに型推論の方法を指示するための構文です。同じくちょっと前に紹介したas
によるダウンキャストと似ていますが、型の代わりにconst
と書くのが特徴です。
as const
は、各種リテラル(文字列・数値・真偽値のリテラル、オブジェクトリテラル・配列リテラル)に付加することができ、その値が書き換えを意図していない値であることを表します。
この記事の最初のほうでリテラル型の型推論について説明したのを覚えているでしょうか。リテラル型の値をvar
やlet
で宣言した変数に入れると、その値があとから書き換えられるかもしれないためリテラル型ではなく対応するプリミティブ型が推論されるのでした。
// foo は string 型
var foo = '123';
では、この'123'
にas const
を付けるとどうなるでしょうか。それが次の例です。
// foo2 は "123" 型
var foo2 = '123' as const;
このようにas const
を付けると'123'
は書き換えられることを意図していない値として扱われるため、変数foo2
はstring
型ではなく"123"
型となります。
ここまでの話はconst
を使えば済むような話でしたが、as const
の本領はここからです。次に、オブジェクトリテラルの例を見ましょう。
// obj は { foo: string; bar: number[] } 型
const obj = {
foo: "123",
bar: [1, 2, 3]
};
/*
obj2 は
{
readonly foo: "123";
readonly bar: readonly [1, 2, 3];
}
型
*/
const obj2 = {
foo: "123",
bar: [1, 2, 3]
} as const;
まず、as const
無しのobj
の型は{ foo: string; bar: number[] }
型となっています。注目すべき点は、例えばfoo
プロパティの型が"123"
型ではなくstring
型となっている点です。これは"123"
をvar
やlet
の変数に入れたときと同じ挙動ですね。オブジェクトのプロパティは(readonly
でない限り)obj.foo = "456";
のように書き換えることができるので、普通はリテラル型がつかないのです。
一方、as const
をオブジェクトリテラルに付けたobj2
の場合は、foo
プロパティには"123"
型がついています。また、foo
プロパティ自体もreadonly
になっています。後者は、obj2
は書き換えることを意図していないオブジェクトであるためobj2.foo = "456";
のような変更を禁止するためにreadonly
となっているわけですね。また、as const
は再帰的にその中身にも適用されるため、中の"123"
も"123" as const
相当になり、型がstring
ではなく"123"
型になっています。
bar
についても同様です。配列リテラルにas const
を使用した場合は対応するreadonly
タプル型が推論されます。この場合、[1, 2, 3] as const
の型がreadonly [1, 2, 3]
と推論されることから、obj2.bar
の型もreadonly [1, 2, 3]
型となっています。readonly [number, number, number]
ではなくreadonyly [1, 2, 3]
になっているのは、やはり配列リテラルの中身についてもas const
が適用されているからです。
他にも、as const
はテンプレート文字列リテラルに対しても特殊な効果を持ちます。テンプレート文字列リテラルの型は通常string
型となりますが、as const
が適用された場合はテンプレートリテラル型が得られます。
const world: string = "world";
// string型
const str1 = `Hello, ${world}!`;
// `Hello, ${string}!` 型
const str2 = `Hello, ${world}!` as const;
このように、as const
はリテラルの型推論で型を広げてほしくないときに使用することができます。as const
をリテラルにつけたときに推論される型の挙動をまとめるとこのようになります。
- 文字列・数値・真偽値リテラルはそれ自体のリテラル型を持つものとして推論されます。(例:
"foo" as const
は"foo"
型) - テンプレート文字列リテラルはテンプレートリテラル型に推論されます。
- オブジェクトリテラルは各プロパティが
readonly
を持つようになります。 - 配列リテラルの型は
readonly
タプル型になります。
object
型と{}
型
あまり目にすることがない型のひとつにobject
型があります。これは「プリミティブ以外の値の型」です。JavaScriptにはオブジェクトのみを引数に受け取る関数があり、そのような関数を表現するための型です。例えば、Object.create
は引数としてオブジェクトまたはnullのみを受け取る関数です。
// エラー: Argument of type '3' is not assignable to parameter of type 'object | null'.
Object.create(3);
ところで、{}
という型について考えてみてください。これは、何もプロパティがないオブジェクト型です。プロパティが無いといっても、構造的部分型により、{foo: string}
のような型を持つオブジェクトも{}
型として扱うことができます。
const obj = { foo: 'foo' };
const obj2: {} = obj;
となると、任意のオブジェクトを表す型として{}
ではだめなのでしょうか。
もちろん、答えはだめです。じつは、{}
という型はオブジェクト以外も受け付けてしまうのです。ただし、undefinedとnullはだめです。
const o: {} = 3;
これは、JavaScriptの仕様上、プリミティブに対してもプロパティアクセスができることと関係しています。例えばプリミティブのひとつである文字列に対してlengthというプロパティを見るとその長さを取得できます。ということで、次のようなコードが可能になります。
interface Length {
length: number;
}
const o: Length = 'foobar';
このことから、{}
というのはundefinedとnull以外は何でも受け入れてしまうようなとても弱い型であるということが分かります。
weak type
ところで、オプショナルなプロパティ(?
修飾子つきで宣言されたプロパティ)しかない型にも同様の問題があることがお分かりでしょうか。そのような型は、関数に渡すためのオプションオブジェクトの型としてよく登場します。
そこで、そのような型はweak typeと呼ばれ5、特殊な処理が行われます。
interface Options {
foo?: string;
bar?: number;
}
const obj1 = { hoge: 3 };
// エラー: Type '{ hoge: number; }' has no properties in common with type 'Options'
const obj2: Options = obj1;
// エラー: Type '5' has no properties in common with type 'Options'.
const obj3: Options = 5;
最後の2行に対するエラーはweak typeに特有のものです。obj2の行は、{ hoge: number; }
型の値をOptions
型の値として扱おうとしていますがエラーとなっています。構造的部分型の考えに従えば、{ hoge: number; }
型のオブジェクトはfooとbarが省略されており、余計なプロパティhogeを持ったOptions
型のオブジェクトと見なせそうですが、weak type特有のルールによりこれは認められません。具体的には、weak typeの値として認められるためにはエラーメッセージにある通りweak typeが持つプロパティを1つ以上持った型である必要があります。実際、そうでない値をOptions
型のオブジェクトとして扱いたい場面はほとんど無いためこういうのはエラーとして弾きたいところ、weak typeは値に対する制限が弱すぎるためこのような追加のルールが導入されているのです。ただし例外として、{}
はOptions
型の値として扱えるようです。
また、weak typeはオブジェクトではないものも同様に弾いてくれます。
unknown
先ほど、{}
はとても弱い型であると述べました。実は最近(TypeScript 3.0から)、本当に最も弱い型であるunknown型が利用可能になりました。どんな型の値もunknown型として扱うことができます。難しい言葉で言うと、これはいわゆるtop型です。すなわち、never型のちょうど逆にあたる、すべての型を部分型として持つような、部分型関係の頂点にある型です。
const u1: unknown = 3;
const u2: unknown = null;
const u3: unknown = (foo: string)=> true;
任意の値を取ることができるというのは上で紹介したany型と同じ特徴ですが、unknown型はany型とは異なり安全に扱うことができる型です。というのも、unknown型の値はどんな値なのか分からないため、できることが制限されているのです。例えば、数値の足し算をすることもできませんし、プロパティアクセスもできません。
const u: unknown = 3;
// エラー: Object is of type 'unknown'.
const sum = u + 5;
// エラー: Object is of type 'unknown'.
const p = u.prop;
このように、どんな値でもいい、最悪nullやundefinedかもしれないということは、その値に対してほとんど何も出来ないことを意味しているのです。従来、関数の引数などが「どんな値でもいい」ことを表したい場合にはanyなどが主に使われてきましたが、any型の値は型システムを無視して好きなように扱うことができて危ない型だったので、代わりにunknown型を使うことが有効です。
unknown型の値を使いたいときは、union型と同様に型の絞り込みが可能です。これにより、unknown型の値として受け取った値が特定の型のときにのみ処理をするということが可能になります。
const u: unknown = 3;
if (typeof u === 'number') {
// この中ではuはnumber型
const foo = u + 5;
}
また、クラスの型とinstanceofを組み合わせることによる絞り込みも可能です。
const u: unknown = 3;
class MyClass {
public prop: number = 10;
}
if (u instanceof MyClass) {
// ここではuはMyClass型
u.prop;
}
unknown型とvoid型の意外な関係
実は、unknown
型とvoid
型は結構似ているところがあります。
それは関数の返り値の型としてvoid
型が登場する場合に現れます。次の例は正しいTypeScriptコードです(コンパイルエラーが起きません)。
const func1: () => number = () => 123;
const f: (() => void) = func1;
ここでfunc1
は() => number
型、つまり数値を返す関数です。上の例は、これを() => void
型として扱っても良いということを表しています。
ここがvoid
型の特殊なところで、部分型関係において**void
型はunknown
型と同様の振る舞いをします**。ただし、この記事の序盤で説明したように、void
型を(関数の返り値などではなく)直に扱う場合は追加の制限が入ります(これにより、const v: void = 123
のようなものはエラーとなります)。
これは特にアロー関数と組み合わせた場合に役に立つ挙動です。func1
を呼びたいけど返り値には興味がない場合、() => func1()
のようなアロー関数を作るとこれは() => number
型となります。しかし返り値はどうでもいいので気持ち的にはこれは() => void
型とも言えますね。この気持ちをうまく表現するために、このような関数を() => void
型として扱えるようになっているのです。
ということは、裏を返せば返り値がvoid
型の関数の返り値は何が入っているのか全くわからないということになります。上の例に続けて以下を実行したとすると、void
型の変数voidValue
の中身は実は123
になります。
const voidValue: void = f();
console.log(voidValue); // 123 が表示される
なので、void
型の値を得てもそれがundefined
である保証すら無いことになります。void
型を持つ値の挙動はやはりunknown
型と似ており、基本的に使うことができません(unknown
型として使うことはできます)。
ということで、実はvoid
型というのはunknown
にさらに追加の制限が加わった型という見方ができるのでした。登場したのはunknown
型の方が後なのですが。
typeof
TypeScriptのちょっと便利な機能として、typeof型というのがあります。これは、typeof 変数
と書くと、その変数の型が得られるものです。
let foo = 'str';
type FooType = typeof foo; // FooTypeはstringになる
const str: FooType = 'abcdef';
keyof
ここからがいよいよTypeScriptのよく分からないところです。ここから先はそれぞれが単独でQiitaの記事になるほどのポテンシャルを秘めているので、探せば記事があると思います。
あるT
を型とすると、keyof T
という型の構文があります。keyof T
は、「T
のプロパティ名全ての型」です。
interface MyObj {
foo: string;
bar: number;
}
let key: keyof MyObj;
key = 'foo';
key = 'bar';
// エラー: Type '"baz"' is not assignable to type '"foo" | "bar"'.
key = 'baz';
この例では、MyObj
型のオブジェクトはプロパティfooとbarを持ちます6。なので、プロパティ名として可能な文字列は'foo'
と'bar'
のみであり、keyof MyObj
はそれらの文字列のみを受け付ける型、すなわち'foo' | 'bar'
になります。よって、keyof MyObj
型の変数であるkeyに'baz'
を代入しようとするとエラーとなります。
なお、JavaScriptではプロパティ名は文字列のほかにシンボルである可能性もあります。よって、keyof型はシンボルの型を含む可能性もあります。
// 新しいシンボルを作成
const symb = Symbol();
const obj = {
foo: 'str',
[symb]: 'symb',
};
//ObjType = 'foo' | typeof symb
type ObjType = keyof (typeof obj);
この例では、obj
のプロパティ名の型をkeyofで得ました。ObjType
は'foo' | typeof symb
となっています。これは'foo' | symbol
とはならない点に注意してください。TypeScriptではシンボルはsymbol
型ですが、プロパティ名としてはシンボルはひとつずつ異なるためsymb
に入っている特定のシンボルでないといけないのです7。
さらに、keyof型にはnumber
の部分型が含まれる場合もあります。それは、数値リテラルを使ってプロパティを宣言した場合です。
const obj = {
foo: 'str',
0: 'num',
};
// ObjType = 0 | 'foo'
type ObjType = keyof (typeof obj);
JavaScriptではプロパティ名に数値は使えません(使おうとした場合文字列に変換されます)が、TypeScriptでは数値をプロパティ名に使用した場合は型の上ではそれを保とうとするということです。
なお、先ほど出てきたインデックスシグネチャを持つオブジェクトの場合は少し特殊な挙動をします。
interface MyObj {
[foo: string]: number;
}
// MyObjKey = string | number
type MyObjKey = keyof MyObj;
この例で定義したMyObj
型は任意のstring
型の名前に対してその名前のプロパティはnumber
型を持つという意味になっています。ということは、MyObj
型のオブジェクトのキーとしてはstring
型の値すべてが使用できます。よって、keyof MyObj
はstring
になることが期待できますね。
しかし、実際にはこれはstring | number
となります。これは、数値の場合もどうせ文字列に変換されるのだからOKという意図が込められています。
一方、インデックスシグネチャのキーの型はnumber
の場合はkeyof MyObj
はnumber
のみとなります。
Lookup Types T[K]
keyofとセットで使われることが多いのがLookup Typesです。これは型T
とK
に対してT[K]
という構文で書きます。K
がプロパティ名の型であるとき、T[K]
はT
のそのプロパティの型となります。
言葉で書くと分かりにくいので例を見ましょう。
interface MyObj {
foo: string;
bar: number;
}
// strの型はstringとなる
const str: MyObj['foo'] = '123';
この例ではMyObj['foo']
という型が登場しています。上で見たT[K]
という構文と比べると、T
がMyObj
型でK
が'foo'
型となります。
よって、MyObj['foo']
はMyObj
型のオブジェクトのfooというプロパティの型であるstring
となります。
同様に、MyObj['bar']
はnumber
となります。MyObj['baz']
のようにプロパティ名ではない型を与えるとエラーとなります。厳密かつ大雑把に言えば、K
はkeyof T
の部分型である必要があります。
逆に言えば、MyObj[keyof MyObj]
という型は可能です。これはMyObj['foo' | 'bar']
という意味になりますが、プロパティ名がfooまたはbarということは、その値はstring
またはnumber
になるということなので、MyObj['foo' | 'bar']
はstring | number
になります。
keyofとLookup Typesを使うと例えばこんな関数を書けます。
function pick<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const obj = {
foo: 'string',
bar: 123,
};
const str: string = pick(obj, 'foo');
const num: number = pick(obj, 'bar');
// エラー: Argument of type '"baz"' is not assignable to parameter of type '"foo" | "bar"'.
pick(obj, 'baz');
この関数pickは、pick(obj, 'foo')
とするとobj.foo
を返してくれるような関数です。注目すべき点は、この関数にちゃんと型を付けることができているという点です。pick(obj, 'foo')
の返り値の型はobj.foo
の型であるstring
型になっています。同様にpick(obj, 'bar')
の型はnumber
型になっています。
pickは型変数を2つ持ち、2つ目はK extends keyof T
と書かれています。これは初出の文法ですが、ここで宣言する型変数K
はkeyof T
の部分型でなければならないという意味です。この条件が無いと、返り値の型T[K]
が妥当でない可能性が生じるためエラーとなります。
pick(obj, 'foo')
という呼び出しでは、T
が{ foo: string; bar: number; }
型、K
が'foo'
型となるため、返り値の型は({ foo: string; bar: number; })['foo']
型、すなわちstring
型となります。
Mapped Types
さて、以上の2つと同時に導入されたのがmapped typeと呼ばれる型です。日本語でどう呼べばいいのかはよく分かりません。mapped typeは{[P in K]: T}
という構文を持つ型です。ここでP
は型変数、K
とT
は何らかの型です。ただし、K
はstring
の部分型である必要があります。例えば、{[P in 'foo' | 'bar']: number}
という型が可能です。
{[P in K]: T}
という型の意味は、「K
型の値として可能な各文字列P
に対して、型T
を持つプロパティP
が存在するようなオブジェクトの型」です。上の例ではK
は'foo' | 'bar'
なので、P
としては'foo'
と'bar'
が可能です。よってこの型が表わしているのはnumber
型を持つプロパティfooとbarが存在するようなオブジェクトです。
すなわち、{[P in 'foo' | 'bar']: number}
というのは{ foo: number; bar: number; }
と同じ意味です。
type Obj1 = {[P in 'foo' | 'bar']: number};
interface Obj2 {
foo: number;
bar: number;
}
const obj1: Obj1 = {foo: 3, bar: 5};
const obj2: Obj2 = obj1;
const obj3: Obj1 = obj2;
これだけでは何が面白いのか分かりませんね。実は、{[P in K]: T}
という構文において、型T
の中でP
を使うことができるのです。例えば次の型を見てください。
type PropNullable<T> = {[P in keyof T]: T[P] | null};
interface Foo {
foo: string;
bar: number;
}
const obj: PropNullable<Foo> = {
foo: 'foobar',
bar: null,
};
ここでは型変数T
を持つ型PropNullable<T>
を定義しました。この型は、T
型のオブジェクトの各プロパティP
の型が、T[P] | null
、すなわち元の型であるかnull
であるかのいずれかであるようなオブジェクトの型です。具体的には、PropNullable<Foo>
というのは{foo: string | null; bar: number | null; }
という型になります。
また、mapped typeでは[P in K]
の部分に以前紹介した修飾子(?
とreadonly
)を付けることができます。例えば、次の型Partial<T>
はT
のプロパティを全てオプショナルにした型です。この型は便利なのでTypeScriptの標準ライブラリに定義されており、自分で定義しなくても使うことができます。全てのプロパティをreadonlyにするReadonly<T>
もあります。
type Partial<T> = {[P in keyof T]?: T[P]};
逆に、修飾子を取り除くこともTypeScript2.8から可能になりました。そのためには、?
やreadonly
の前に-
を付けます。例えば、すべてのプロパティから?
を取り除く、いわばPartial<T>
の逆のはたらきをするRequired<T>
は次のように書けます。
type Required<T> = {[P in keyof T]-?: T[P]};
これの使用例はこんな感じです。ReqFoo
では、bar
の?
が無くなっていることが分かります。
interface Foo {
foo: string;
bar?: number;
}
/// ReqFoo = { foo: string; bar: number; }
type ReqFoo = Required<Foo>;
このRequired<T>
も標準ライブラリに入っています。
もう少し実用的な例として、実際にmapped typeを使う関数を定義する例も見せておきます。
function propStringify<T>(obj: T): {[P in keyof T]: string} {
const result = {} as {[P in keyof T]: string};
for (const key in obj) {
result[key] = String(obj[key]);
}
return result;
}
この例ではasを使ってresultの型を{[P in keyof T]: string}
にしてから実際にひとつずつプロパティを追加していっています。asやanyなどを使わずにこの関数を書くのは難しい気がします。そのため、mapped typeはどちらかというと関数が使われる側の利便性のために使われるのが主でしょう。ライブラリの型定義ファイルを書く場合などは使うかもしれません。
ちなみに、mapped typeを引数の位置に書くこともできます。
function pickFirst<T>(obj: {[P in keyof T]: Array<T[P]>}): {[P in keyof T]: T[P] | undefined} {
const result: any = {};
for (const key in obj) {
result[key] = obj[key][0];
}
return result;
}
const obj = {
foo: [0, 1, 2],
bar: ['foo', 'bar'],
baz: [],
};
const picked = pickFirst(obj);
picked.foo; // number | undefined型
picked.bar; // string | undefined型
picked.baz; // undefined型
この例のすごいところは、pickFirstの型引数T
が推論できているところです。objは{ foo: number[]; bar: string[]; baz: never[]; }
という型を持っており、それが{[P in keyof T]: Array<T[P]>}
の部分型であることを用いて、T
を{ foo: number; bar: string; baz: never; }
とできることを推論できています。これをさらにmapped typeで移して、返り値の型は{ foo: number | undefined; bar: string | undefined; baz: undefined; }
となります。なお、bazの型はnever | undefined
ですが、neverはunion型の中では消えるのでこれはundefined
となります。
mapped typeは他にも色々な応用が出来るようです。実践的な例としては、Diff型をmapped typeなどを用いて実現することができます。ここまでの内容を理解していればこの記事も理解できると思います。
Conditional Types
上記のmapped typesが導入されたのがTypeScript 2.1のことで、そこから先しばらくは細々とした改良はあっても訳の分からないやばい型が導入されるようなことはありませんでした。その状況を打ち破り、TypeScript 2.8で久々に登場予定のやばい型、それがconditional typeです。これは型レベルの条件分岐が可能な型です。Qiitaに既にいい記事がありますのでそちらを参照してもらうのも良いですが、今回の一連の記事は一通り読めばTypeScriptの型が分かることを目指していますので、ここにも解説を書いておきます。
Conditional type(日本語でなんと言えばいいのかはやっぱり分かりません)は4つの型を用いてT extends U ? X : Y
という構文で表現される型です。いわゆる条件演算子を彷彿とさせる記法で、意味もその直感に従います。すなわち、この型はT
がU
の部分型ならばX
に、そうでなければY
になります。そんなもの何に使うんだと言いたくなるかもしれませんが、実はこの型の表現力は凄まじく、当該のPull Requestで指摘されているようにさまざまな問題を解決できます。まずそれについて少し述べます。
mapped typesの限界
mapped typeが導入された当初から指摘されていた問題として、deepなマッピングができないという問題がありました。先ほど組み込みのReadonly<T>
を紹介しましたが、これはプロパティをshallowにreadonly化します。例えば、
interface Obj{
foo: string;
bar: {
hoge: number;
};
}
という型に対してReadonly<Obj>
は{ readonly foo: string; readonly bar: { hoge: number; }; }
となります。つまり、barの中のhogeはreadonlyになりません。これはこれで役に立つかもしれませんが、ネストしているオブジェクトも含めて全部readonlyにしてくれるようなもの、すなわちDeepReadonly<T>
のほうが需要がありました。少し考えると、これは再帰的な定義にしなければならないことが分かります。しかし、次のような素朴な定義はうまくいきません。
type DeepReadonly<T> = {
readonly [P in keyof T]: DeepReadonly<T[P]>;
}
次に示すように、これは一見うまくいくように見えます。
type DeepReadonly<T> = {
readonly [P in keyof T]: DeepReadonly<T[P]>;
}
interface Obj{
foo: string;
bar: {
hoge: number;
};
}
type ReadonlyObj = DeepReadonly<Obj>;
const obj: ReadonlyObj = {
foo: 'foo',
bar: {
hoge: 3,
},
};
// エラー: Cannot assign to 'hoge' because it is a constant or a read-only property.
obj.bar.hoge = 3;
しかし、これはDeepReadonly<T>
のT
の型が何か判明しているからであり、次のような状況ではうまくいかなくなります。
type DeepReadonly<T> = {
readonly [P in keyof T]: DeepReadonly<T[P]>;
}
function readonlyify<T>(obj: T): DeepReadonly<T> {
// エラー: Excessive stack depth comparing types 'T' and 'DeepReadonly<T>'.
return obj as DeepReadonly<T>;
}
つまり、あのような単純な再帰では一般のT
に対してどこまでもmapped typeを展開してしまうことになり、それを防ぐためにconditional typeが必要となるわけです。
conditional typeによるDeepReadonly<T>
では、conditional typeを用いたDeepReadonly<T>
を
https://github.com/Microsoft/TypeScript/pull/21316 から引用します。
type DeepReadonly<T> =
T extends any[] ? DeepReadonlyArray<T[number]> :
T extends object ? DeepReadonlyObject<T> :
T;
interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}
type DeepReadonlyObject<T> = {
readonly [P in NonFunctionPropertyNames<T>]: DeepReadonly<T[P]>;
};
type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T];
DeepReadonly<T>
がconditional typeになっており、T
が配列の場合、配列以外のオブジェクトの場合、それ以外の場合(すなわちプリミティブの場合)に分岐しています。配列の場合はDeepReadonlyArray<T>
で処理し、それ以外のオブジェクトはDeepReadonlyObject<T>
で処理しています。プリミティブの場合はそのプロパティを考える必要はないため単にT
を返しています。
DeepReadonlyArray<T>
は、要素の型であるT
をDeepReadonly<T>
で再帰的に処理し、配列自体の型はReadonlyArray<T>
により表現しています。ReadonlyArray<T>
というのは標準ライブラリにある型で、各要素がreadonlyになっている配列です。T[number]
というのは配列であるT
に対してnumber
型のプロパティ名でアクセスできるプロパティの型ですから、すなわち配列T
の要素の型ですね。
DeepReadonlyObject<T>
は上の素朴な場合と同様にmapped typeを用いて各プロパティを処理しています。ただし、NonFunctionPropertyNames<T>
というのはT
のプロパティ名のうち関数でないものです。よく見るとこれもconditional typeで実装されています。さっき記事を紹介したDiffとアイデアは同じですが、conditional typeにより簡単に書けています。
つまり、このDeepReadonlyObject<T>
は実はT
からメソッド(関数であるようなプロパティ)を除去します。これにより、メソッドが自己を書き換える可能性を排除しているのでしょう。
実のところ、DeepReadonly<T>
の本質は、conditional typeが遅延評価されるところにあります。DeepReadonly<T>
の分岐条件はT
が何なのかわからないと判定できないので必然的にそうなりますが。これにより、評価時に無限に再帰することを防いでいます。
試してみたところ、
type List<T> = {
value: T;
next: List<T>;
} | undefined;
のような再帰的な型にもDeepReadonly<T>
を適用することができました。
conditional typeにおける型マッチング
実はconditional typeにはさらに強力な機能があります。それは、conditional typeの条件部で新たな型変数を導入できるという機能です。 https://github.com/Microsoft/TypeScript/pull/21496 から例を引用します。
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : T;
ReturnType<T>
は、T
が関数の型のとき、その返り値の型となります。ポイントは、関数の型の返り値部分にあるinfer R
です。このようにinfer
キーワードを用いることでconditional typeの条件部分で型変数を導入することができます。導入された型変数は分岐のthen側で利用可能になります。
つまり、このReturnType<T>
は、T
が(...args: any[]) => R
(の部分型)であるときR
に評価されるということです。then側でしか型変数が使えないのは、else側ではT
が(... args: any[]) => R
の形をしていないかもしれないことを考えると当然ですね。このことから分かるように、この機能は型に対するパターンマッチと見ることができます。
実は同じ型変数に対するinfer
が複数箇所に現れることも可能です。その場合、推論される型変数にunion型やintersection型が入ることもあります。人為的な例ですが、次の例で確かめられます。
type Foo<T> =
T extends {
foo: infer U;
bar: infer U;
hoge: (arg: infer V)=> void;
piyo: (arg: infer V)=> void;
} ? [U, V] : never;
interface Obj {
foo: string;
bar: number;
hoge: (arg: string)=> void;
piyo: (arg: number)=> void;
}
declare let t: Foo<Obj>; // tの型は[string | number, string & number]
部分型関係を考えれば、U
がunion型で表現されてV
がintersection型で表現される理由が分かります。U
はcovariantな位置に、V
はcontravariantな位置に出現しているからです。ちなみに、試しに両方の位置に出現させてみたところ、Foo<Obj>
が解決されなくなりました。
ちなみに、ReturnType<T>
など、conditional typesを使った型がいくつか標準ライブラリに組み込まれるようです。自分でconditional typesと戦わなくても恩恵を得られる場面が多いと思います。
Conditional Typesによる文字列操作
infer
とテンプレートリテラル型を組み合わせることで、型レベルの文字列操作が可能になります。例えば、"Hello, world!"
型というリテラル型からworld
部分を抜き出すには次のようにします。
type ExtractHelloedPart<S extends string> = S extends `Hello, ${infer P}!` ? P : unknown;
// type T1 = "world"
type T1 = ExtractHelloedPart<"Hello, world!">;
// type T2 = unknown
type T2 = ExtractHelloedPart<"Hell, world!">;
TypeScript 4.1のリリース前後には、これを活用して型レベルパーサーや型レベルインタプリタといった様々な作品が作られました。非常に大きな可能性を秘めた機能です。
まとめ
TypeScriptの型をひととおり紹介しました。今回は入門記事ということで、はしょったものもあります(this型とか。普段使わないので忘れているとも言いますが)。とはいえ、面白そうな部分はおおよそ紹介したつもりなので、TypeScriptの型で面白いことをやっているコードがあってもけっこう読めるのではないかと思います。
TypeScriptを使っている方は、mapped typeやconditional typeなどを機会があれば使ってみましょう(後者はTypeScript 2.8の正式版が出ないとなかなか使えないと思いますが)。mapped typeくらいなら意外と使える場面があります。
TypeScriptを使っていない方にとっては、特に後半は他の言語ではなかなか見ないような型が登場しており面白かったのではないでしょうか。型システムが強い言語は多々ありますが、TypeScriptの型システムはJavaScriptに型を付けるという難題に対する答えであり、それらとは一線を画したところがあります。
あわせて読みたい
- TypeScriptの型初級 続編ができました。
- TypeScriptの型演習 演習問題ができました。
- TypeScriptで型安全なBuilderパターン この記事で紹介したconditional typesやmapped typesなどをふんだんに活用して超型安全なBuilderパターンを書いてみた記事です。
- TypeScriptの型推論詳説 TypeScriptの型を理解したら、次は型推論を理解しましょう。
-
ただし、この挙動が実装されたのはTypeScript 2.6からで比較的最近です。また、
--strictFunctionTypes
オプションをオンにしている必要があります。 ↩ -
instanceof
の挙動においてはちょっとそうとも言い切れない部分がありますが。 ↩ -
JavaScriptでは一つの配列に異なる型の値を入れられる点に注意してください。 ↩
-
この挙動はTypeScript 3.3からの新しい挙動です。それ以前は、ことなる型の関数同士のunionは関数として呼ぶことができませんでした。 ↩
-
正確にはオプショナルなプロパティを1つ以上持っていなければweak typeとは呼びません。すなわち、
{}
はweak typeではありません。 ↩ -
正確には構造的部分型付けにより他のプロパティを持っているかもしれませんが、プロパティの存在が型システムによって保証されているのはfooとbarだけです。 ↩
-
実際のところ、型システム上では
symb
にはunique symbol
型という型がついています。これはリテラル型のシンボル版みたいなもので、シンボルがひとつひとつ異なることを反映しています。unique symbol
はリテラル型のようにリテラルで表せないので、代わりにそれが入っている変数名を使ってtypeof symb
のように表しているのです。 ↩