TypeScript Handbook を読み進めていく第十回目。
- Basic Types
- Variable Declarations
- Interfaces
- Classes
- Functions
- Generics
- Enums
- Type Inference
- Type Compatibility
- Advanced Types (今ココ)
- Symbols
- Iterators and Generators
- Modules
- Namespaces
- Namespaces and Modules
- Module Resolution
- Declaration Merging
- JSX
- Decorators
- Mixins
- Triple-Slash Directives
- Type Checking JavaScript Files
Advanced Types
Intersection Types
交差型は Person & Serializable & Loggable
のように複数の型をひとつにまとめたもので、これらの型の全メンバを持ちます。
交差型を使用した mixin の例を以下に示します。
function extend<T, U>(first: T, second: U): T & U {
let result = <T & U>{};
for (let id in first) {
(<any>result)[id] = (<any>first)[id];
}
for (let id in second) {
if (!result.hasOwnProperty(id)) {
(<any>result)[id] = (<any>second)[id];
}
}
return result;
}
class Person {
constructor(public name: string) { }
}
interface Loggable {
log(): void;
}
class ConsoleLogger implements Loggable {
log() {
// ...
}
}
var jim = extend(new Person("Jim"), new ConsoleLogger());
var n = jim.name;
jim.log();
extend
関数は最初の引数と 2 番目の引数をマージするので、その戻り値は両方の引数の型を足し合わせたもの (T & U
) になるというわけだ
Union Types
共用型は交差型と似ていると思うかもしれませんが、まったくの別物です。
例として、number
と string
のいずれかの引数を取る関数を考えてみましょう。
/**
* 文字列を受け取り、左側に "padding" を加える。
* 'padding' が文字列の場合、'padding' を左側に加える。
* 'padding' が数値の場合、その数だけ空白を左側に加える。
*/
function padLeft(value: string, padding: any) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
padLeft("Hello world", 4); // 戻り値は " Hello world"
この関数の問題点は、padding
の型を any
としていることです。
そのため、number
や string
以外の引数を渡してもコンパイルエラーになりません。
let indentedString = padLeft("Hello world", true); // コンパイルは通るが、実行時にエラーになる
伝統的なオブジェクト指向プログラミングでは共通の基底クラスを定義することでこれを実現しようとするかもしれませんが、ここでそれをするには若干大げさすぎます。
そこで、padding
の型として any
の代わりに 共用型 を使用します。
共用型は複数の型のうち、どれかであることを表す型で、それぞれの型を縦棒 (|
) で区切って記述します。(例: number | string | boolean
)
/**
* 文字列を受け取り、左側に "padding" を加える。
* 'padding' が文字列の場合、'padding' を左側に加える。
* 'padding' が数値の場合、その数だけ空白を左側に加える。
*/
function padLeft(value: string, padding: string | number) {
// ...
}
let indentedString = padLeft("Hello world", true); // コンパイル時にエラーになる
共用型の値に対しては、すべての型に共通のメンバにしかアクセスすることはできません。
interface Bird {
fly();
layEggs();
}
interface Fish {
swim();
layEggs();
}
function getSmallPet(): Fish | Bird {
// ...
}
let pet = getSmallPet();
pet.layEggs(); // Bird、Fish 両方にあるので OK
pet.swim(); // Fish にしかないのでエラー
すべての型に共通のメンバであれば、共用型にどの型が入っていたとしても確実にアクセスできる。
逆に、特定の型にしかないメンバは他の型が入っていた時にどうしようもなくなるので、そもそもアクセスさせない、と。
Type Guards and Differentiating Types
共用型は複数の型を受け取るときに便利ですが、実際にどの型か判断するにはどうすれば良いでしょうか。
JavaScript で一般的に用いられるのは以下のようにメンバの存在チェックを行う方法です。
let pet = getSmallPet();
// この書き方ではエラーになる
if (pet.swim) {
pet.swim();
}
else if (pet.fly) {
pet.fly();
}
このコードを動作させるには、キャストを使用する必要があります。
let pet = getSmallPet();
if ((<Fish>pet).swim) {
(<Fish>pet).swim();
}
else {
(<Bird>pet).fly();
}
User-Defined Type Guards
このままでは毎回キャストする必要がありますが、実際には、一度型チェックを行えばその条件分岐の中ではどの型か分かっていることになります。
このように特定のスコープにおける型を保証することを 型ガード と読んでおり、それには以下のような 型述語 を返す関数を定義して使用します。
型述語は parameterName is Type
の形式で記述しますが、この時の parameterName
は関数の引数名と同じである必要があります。
function isFish(pet: Fish | Bird): pet is Fish {
return (<Fish>pet).swim !== undefined;
}
isFish
を呼び出すと、もしそれが元の型と互換性がある型であれば、TypeScript は自動的にその型として扱ってくれます。
// 'swim'、'fly' の両方にアクセスしても問題ない
if (isFish(pet)) {
pet.swim();
}
else {
pet.fly();
}
Else 句の中では自動的に Bird 型と判断してくれるのがすごい
typeof
type guards
最初の padLeft
の例について、型述語を使用して書き直してみましょう。
function isNumber(x: any): x is number {
return typeof x === "number";
}
function isString(x: any): x is string {
return typeof x === "string";
}
function padLeft(value: string, padding: string | number) {
if (isNumber(padding)) {
return Array(padding + 1).join(" ") + value;
}
if (isString(padding)) {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
毎回、このような型述語関数を定義するのは大変なため、TypeScript では一部の型に対して自動的に型ガードを認識してくれます。
function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
この時、自動的に型ガードとして認識してくれるのは typeof v === "typename"
または typeof v !== "typename"
のいずれかの形式で記述した時だけです。
また、"typename"
には "number"
、 "string"
、 "boolean"
、 "symbol"
のいずれかのみを指定可能です。
typeof v == "typename"
(=
2 つ) でも型ガードとして認識してくれるっぽい
instanceof
type guards
JavaScript では typeof
以外に instanceof
もよく使用します。
instanceof
型ガード ではコンストラクタ関数を使用して型を特定します。
interface Padder {
getPaddingString(): string
}
class SpaceRepeatingPadder implements Padder {
constructor(private numSpaces: number) { }
getPaddingString() {
return Array(this.numSpaces + 1).join(" ");
}
}
class StringPadder implements Padder {
constructor(private value: string) { }
getPaddingString() {
return this.value;
}
}
function getRandomPadder() {
return Math.random() < 0.5 ?
new SpaceRepeatingPadder(4) :
new StringPadder(" ");
}
// 'SpaceRepeatingPadder | StringPadder' 型
let padder: Padder = getRandomPadder();
if (padder instanceof SpaceRepeatingPadder) {
padder; // 'SpaceRepeatingPadder' 型として扱われる
}
if (padder instanceof StringPadder) {
padder; // 'StringPadder' 型として扱われる
}
instanceof
の右側はコンストラクタ関数である必要があります。
また、TypeScript は以下の順で型を特定します。
- 型が
any
でない場合、コンストラクタ関数のprototype
の型 - コンストラクタ関数の戻り値の型をまとめた共用型
Nullable types
Basic Types の章 でも述べたように、TypeScript では null
と undefined
という 2 つの型を定義しています。
デフォルトでは null
と undefined
はどの型に対しても代入できますが、逆に言えば、それらを許容したくない場合でも代入を防げないことを意味しています。
--strictNullChecks
を使用することで、 そのままでは null
や undefined
を代入できなくすることが可能です。
その場合、null
や undefined
を代入できる変数は明示的に共用型として宣言する必要があります。
let s = "foo";
s = null; // エラー。'null' は 'string' に代入できない
let sn: string | null = "bar";
sn = null; // OK
sn = undefined; // エラー。'undefined' は'string | null' に代入できない
JavaScript と異なり、TypeScript では null
と undefined
を区別して扱っていることに注意してください。
つまり、string | null
、 string | undefined
、 string | undefined | null
はすべて異なる型になります。
Optional parameters and properties
--strictNullChecks
を指定すると自動的に任意の引数に | undefined
が追加されます。
function f(x: number, y?: number) {
return x + (y || 0);
}
f(1, 2);
f(1);
f(1, undefined);
f(1, null); // エラー。'null' は 'number | undefined' には代入できない
任意のプロパティについても同様です。
class C {
a: number;
b?: number;
}
let c = new C();
c.a = 12;
c.a = undefined; // エラー。'undefined' は 'number' には代入できない
c.b = 13;
c.b = undefined; // OK
c.b = null; // エラー。'null' は 'number | undefined' には代入できない
Type guards and type assertions
null
を代入可能な型は共用型として表現されるため、null
を取り除くためには型ガードを使用する必要がありますが、これは JavaScript と同じように記述できます。
function f(sn: string | null): string {
if (sn == null) {
return "default";
}
else {
return sn;
}
}
上記のやり方は null
を除外しようとしていることが非常に明確ですが、もっと簡潔な演算子を使用することもできます。
function f(sn: string | null): string {
return sn || "default";
}
コンパイラが null
や undefined
を取り除くことができない場合、キャストを使用して明示的にそれらを取り除くことができます。
識別子の末尾に !
を付けることで、その識別子の型から null
と undefined
が取り除かれます。
function broken(name: string | null): string {
function postfix(epithet: string) {
return name.charAt(0) + '. the ' + epithet; // エラー。'name' は null の可能性がある
}
name = name || "Bob";
return postfix("great");
}
function fixed(name: string | null): string {
function postfix(epithet: string) {
return name!.charAt(0) + '. the ' + epithet; // OK
}
name = name || "Bob";
return postfix("great");
}
この例ではネストされた関数を使用していますが、ネストされた関数の呼び出しのすべてを追跡することができないため、コンパイラは実行時の name
の型が何になるか (null
になるかどうか) を判断することはできません。
だから
!
を使って自分で「これはnull
ではない」ということを表明する必要がある、と。
といいつつ、!
を指定しなくても振る舞いは変わらないっぽいけど、何が違うんだろう?
Type Aliases
型エイリアスを使って、型に別名を付けることができます。
型エイリアスはインタフェースと似ていますが、プリミティブ型、共用型、タプル、その他の自作クラスに対して適用することができます。
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
if (typeof n === "string") {
return n;
}
else {
return n();
}
}
インタフェースと同様に、エイリアスもジェネリックにすることができます。
そのためにはエイリアス宣言の右側に型引数を追加するだけです。
type Container<T> = { value: T };
プロパティからエイリアス自身を参照することも可能です。
type Tree<T> = {
value: T;
left: Tree<T>;
right: Tree<T>;
}
これらと交差型を組み合わせることでクラクラするような型を作り出すことができます。
type LinkedList<T> = T & { next: LinkedList<T> };
interface Person {
name: string;
}
var people: LinkedList<Person>;
var s = people.name;
var s = people.next.name;
var s = people.next.next.name;
var s = people.next.next.next.name;
ただし、上記の他には宣言の右側にエイリアスを記述することはできません。
type Yikes = Array<Yikes>; // エラー
Interfaces vs. Type Aliases
前述したように、型エイリアスとインタフェースはよく似ていますが、微妙に異なる点があります。
ひとつ目の違いは、インタフェースがどこでも使用できる新しい名前を作り出すのに対し、型エイリアスは新しい名前を作ることはしません。
例えば、エラーメッセージではエイリアス名は表示されませんし、以下のコードでは interfaced
をエディタ上でマウスホバーすると Interface
を返却する関数と表示されますが、aliased
をマウスホバーしてもオブジェクトリテラルを返却する関数としか表示されません。
type Alias = { num: number }
interface Interface {
num: number;
}
declare function aliased(arg: Alias): Alias;
declare function interfaced(arg: Interface): Interface;
ふたつ目の違いは、エイリアスを拡張したり、実装する (または他の型を拡張/実装してエイリアスを作る) ことができない点です。
そのため、理想的なソフトウェアは 拡張に対して開いている べきであることを考慮すると、なるべく型エイリアスではなく、インタフェースを使用するようにしてください。
一方で、インタフェースを用いて型を表現できず、共用型やタプル型を使用する必要がある場合には型エイリアスを使用するべきです。
String Literal Types
リテラル文字列型を使用し、文字列の取りうる値を限定することが可能です。
さらに共用型、型ガード、型エイリアスと組み合わせることで、文字列を enum のように使用することができます。
type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
animate(dx: number, dy: number, easing: Easing) {
if (easing === "ease-in") {
// ...
}
else if (easing === "ease-out") {
}
else if (easing === "ease-in-out") {
}
else {
// エラー! null や undefined を渡すべきではない
}
}
}
let button = new UIElement();
button.animate(0, 0, "ease-in");
button.animate(0, 0, "uneasy"); // エラー。 "uneasy" を指定することはできない
指定された 3 種類以外の文字列を引数として渡すと、以下のようなエラーになります。
Argument of type '"uneasy"' is not assignable to parameter of type '"ease-in" | "ease-out" | "ease-in-out"'
リテラル文字列型はオーバーロードにも使用することができます。
function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// ... 他のオーバーロード ...
function createElement(tagName: string): Element {
// ... 処理をここに書く ...
}
オーバーロードせずに直接
createElement(tagName: "img" | "input")
としても良いような気がするけど、オーバーロードの特徴である「引数に応じて戻り値の型を変える」ことができるのがこのやり方のメリットかな
Discriminated Unions
リテラル文字列型、共用型、型ガード、型エイリアスを組み合わせることで、判別共用型 (または タグ付き共用型、代数的データ型) と呼ばれるパターンを構築することができます。
TypeScript での判別共用型は以下の 3 つの要素から成り立ちます。
- リテラル文字列型の共通プロパティを持つ型 (判別子)
- 複数の判別子からなる型エイリアス (共用型)
- 共通プロパティに対する型ガード
まず、後で結合させるインタフェースを定義します。
各インタフェースでは 判別子 (または タグ) となるリテラル文字列型の kind
プロパティを個別に定義します。
また、それ以外のプロパティも各インタフェースに定義します。
この時、各インタフェースには何の関連もないことを覚えておいてください。
「判別子」はインタフェース全体のことを指すのかと思ったら、各インタフェースで共通で持ってるプロパティのことも「判別子」と呼んだりしていてイマイチ意味が不明な説明だけど、あまり深く考えずに流す方向で…
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
radius: number;
}
続いてこれらのインタフェースを結合しましょう。
type Shape = Square | Rectangle | Circle;
この判別共用型の使用方法は以下の通りです。
function area(s: Shape) {
switch (s.kind) {
case "square": return s.size * s.size;
case "rectangle": return s.height * s.width;
case "circle": return Math.PI * s.radius ** 2;
}
}
Exhaustiveness checking
もしも判別共用型のすべての値を網羅していなかったらコンパイラに警告してほしいと思うことでしょう。
例えば、Shape
に Triangle
を追加した時に area
の修正を忘れた場合です。
type Shape = Square | Rectangle | Circle | Triangle;
function area(s: Shape) {
switch (s.kind) {
case "square": return s.size * s.size;
case "rectangle": return s.height * s.width;
case "circle": return Math.PI * s.radius ** 2;
}
// "triangle" のパターンを追加していないため、エラーになってほしい
}
これを実現するための方法として 2 通りのやり方があります。
ひとつ目は --strictNullChecks
と戻り値の型を指定することです。
switch
文が網羅的でない場合、戻り値として undefinded
が含まれることになります (number | undefied
) が、実際に指定している戻り値の型 (number
) と矛盾するため、エラーとなります。
function area(s: Shape): number { // エラー: 戻り値は number | undefined になる
switch (s.kind) {
case "square": return s.size * s.size;
case "rectangle": return s.height * s.width;
case "circle": return Math.PI * s.radius ** 2;
}
}
しかし、この方法は意図が分かりにくい上に、常に --strictNullChecks
を使用できるとも限りません。
ふたつ目の方法は never
型を使用することです。
以下の例の assertNever
では条件分岐で指定されていない型がない (never
型である) ことをチェックしています。
条件分岐に漏れがあった場合には s
がその型となるため、コンパイルエラーとなります。
function assertNever(x: never): never {
throw new Error("Unexpected object: " + x);
}
function area(s: Shape) {
switch (s.kind) {
case "square": return s.size * s.size;
case "rectangle": return s.height * s.width;
case "circle": return Math.PI * s.radius ** 2;
default: return assertNever(s); // 条件分岐が不足している場合、ここでエラーになる
}
}
この方法は追加の関数が必要となりますが、やりたいことが非常に明確です。
Polymorphic this
types
ポリモーフィックな this
型はそれ自身のクラス/インタフェースの子孫クラスであることを表します。
これは F-bounded polymorphism と呼ばれるもので、階層構造を持つ流れるようなインタフェースを表現しやすくしてくれます。
以下の例では各演算の後に this
を返却しています。
class BasicCalculator {
public constructor(protected value: number = 0) { }
public currentValue(): number {
return this.value;
}
public add(operand: number): this {
this.value += operand;
return this;
}
public multiply(operand: number): this {
this.value *= operand;
return this;
}
// ... 他の演算が続く ...
}
let v = new BasicCalculator(2)
.multiply(5)
.add(1)
.currentValue();
このクラスでは this
型を使用しているため、このクラスを拡張した新しいクラスでもそのまま元のメソッドを使用できます。
class ScientificCalculator extends BasicCalculator {
public constructor(value = 0) {
super(value);
}
public sin() {
this.value = Math.sin(this.value);
return this;
}
// ... 他の演算が続く ...
}
let v = new ScientificCalculator(2)
.multiply(5)
.sin()
.add(1)
.currentValue();
this
型を使用しなかった場合、流れるようなインタフェースを維持したまま BasicCalculator
を継承することはできなかったでしょう。
というのも、multiply
の戻り値は BasicCalculator
なので、そこから sin
を呼び出すことはできないためです。
this
型を使用すると multiply
の戻り値が this
(ここでは ScientificCalculator
) になるため、このように記述することが可能です。
Index types
インデックス型を使用すると動的なプロパティ名のチェックが可能になります。
例として、オブジェクトのプロパティの一部を取り出すパターンを見てみましょう。
JavaScript では以下のようになります。
function pluck(o, names) {
return names.map(n => o[n]);
}
TypeScript では以下のように インデックス型クエリ と 添字アクセス演算子 を使用します。
function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
return names.map(n => o[n]);
}
interface Person {
name: string;
age: number;
}
let person: Person = {
name: 'Jarid',
age: 35
};
let strings: string[] = pluck(person, ['name']); // OK。string[] になる
この例にはいくつか新しい演算子が登場するため、順番に説明していきましょう。
最初に登場する演算子は keyof T
です。
これは インデックス型クエリ演算子 と呼ばれるもので、任意の型 T
に対し、T
の public プロパティの名前の集合を表します。
let personProps: keyof Person; // 'name' | 'age'
keyof Person
は 'name' | 'age'
と宣言するのと同じですが、新たなプロパティを Person
に追加した場合に keyof Person
は自動的に更新される点が異なります。
また、それ以外に keyof
は pluck
のようにジェネリッククラス/関数と組み合わせて使用することも可能です。
つまり、pluck
に正しいプロパティ名を渡しているかコンパイラがチェックしてくれるということです。
pluck(person, ['age', 'unknown']); // エラー。'unknown' は 'name' | 'age' に含まれない
ふたつ目の演算子は T[K]
です。
これは 添字アクセス演算子 と呼ばれるもので、その式の表す型を表します。
つまり、person['name']
は Person['name']
型 (上記の例では string
) ということになります。
また、インデックス型クエリと同様に T[K]
はジェネリッククラス/関数で使用することが可能です。
ただし、型変数として K extends keyof T
を宣言し忘れないようにしてください。
以下の例では o: T
、name: K
であることから、o[name]
は T[K]
型になります。
function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
return o[name]; // o[name] は T[K] 型
}
コンパイラは T[K] を実際の型に解釈してくれるため、getProperty
は指定されたプロパティに応じて様々な型を返却できます。
let name: string = getProperty(person, 'name');
let age: number = getProperty(person, 'age');
let unknown = getProperty(person, 'unknown'); // エラー。'unknown' は 'name' | 'age' に含まれない
Index types and string index signatures
keyof
と T[K]
は文字列型インデックスシグネチャの影響を受けます。
文字列型インデックスシグネチャを宣言した場合、keyof T
は string
になり、T[string]
はそのインデックスシグネチャの型になります。
interface Map<T> {
[key: string]: T;
}
let keys: keyof Map<number>; // string
let value: Map<number>['foo']; // number
Mapped types
既存の型に対し、各プロパティを任意にしたり、
interface PersonPartial {
name?: string;
age?: number;
}
読み取り専用にしたいということはよくあるでしょう。
interface PersonReadonly {
readonly name: string;
readonly age: number;
}
TypeScript ではこのような古い型に基づく新しい型 (マップ型) を作成する機能を提供しています。
マップ型では古い型の各プロパティに対し、同じ変換を行うことで新しい型のプロパティを作成します。
例として、すべてのプロパティを readonly
または任意にする例を見てみましょう。
type Readonly<T> = {
readonly [P in keyof T]: T[P];
}
type Partial<T> = {
[P in keyof T]?: T[P];
}
type 〜 =
は単なる型エイリアスの宣言で、キモは[P in keyof T]: T[P]
の部分
これらの使い方は以下の通りです。
type PersonPartial = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;
また、一番簡単なマップ型の例を見てみましょう。
type Keys = 'option1' | 'option2';
type Flags = { [K in Keys]: boolean };
マップ型の構文は for ... in
の構文と似ており、3 つのパートから成ります。
- 型変数
K
: 各プロパティを表す - リテラル文字列共用体
Keys
: プロパティ名の集合 - そのプロパティの型
上記の例では Keys
はハードコーディングされたプロパティ名のリストで、プロパティの型は常に boolean でした。
つまり、このマップ型は以下のようになります。
type Flags = {
option1: boolean;
option2: boolean;
}
実際のアプリケーションでは最初の Readonly
や Partial
のように既存の型に対して変換を行うでしょう。
そのため、keyof
と添字アクセス演算子を使って以下のように記述します。
type NullablePerson = { [P in keyof Person]: Person[P] | null }
type PartialPerson = { [P in keyof Person]?: Person[P] }
これのジェネリック版はもっと役に立つことでしょう。
type Nullable<T> = { [P in keyof T]: T[P] | null }
type Partial<T> = { [P in keyof T]?: T[P] }
この例では keyof T
がプロパティリスト、T[P]
を変換したものがプロパティの型になります。
また、元のプロパティの修飾子は新しいプロパティにも引き継がれます。
つまり、Person.name
が読み取り専用であれば Partial<Person>.name
は読み取り専用かつ任意になります。
他の例として、T[P]
を Proxy<T>
でラップした例を見てみましょう。
type Proxy<T> = {
get(): T;
set(value: T): void;
}
type Proxify<T> = {
[P in keyof T]: Proxy<T[P]>;
}
function proxify<T>(o: T): Proxify<T> {
// ... プロキシでラップする ...
}
let proxyProps = proxify(props);
Readonly<T>
と Partial<T>
は非常に便利なため、Pick
、Record
と一緒に TypeScript の標準ライブラリに含まれています。
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
}
type Record<K extends string, T> = {
[P in K]: T;
}
Pick
はプロパティの一部を抽出した型を作る。
Record
は任意のプロパティを特定の型に統一する。(なんでRecord
という名前なんだろう)
Readonly
、Partial
、Pick
は準同型であるのに対し、Record
はコピー元となる入力クラスを受け取らないため、準同型ではありません。
type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>
非準同型な型は新しいプロパティを作るため、修飾子 (readonly 等) はコピーされません。
Inference from mapped types
ラップしたプロパティの取り出しは非常に簡単です。
function unproxify<T>(t: Proxify<T>): T {
let result = {} as T;
for (const k in t) {
result[k] = t[k].get();
}
return result;
}
let originalProps = unproxify(proxyProps);
マップ型が準同型である場合のみ、型推論が行われることに注意してください。
もしマップ型が非準同型の場合、明示的に型引数を指定する必要があります。
Conditional Types
TypeScript 2.8 から条件付き型が導入されました。
これは一様でない型マッピングを表現するためのもので、条件に基づいて 2 つの型のうち、いずれかが選択されます。
T extends U ? X : Y
この場合の型は T
が U
に代入可能であれば X
、そうでなければ Y
になります。
条件付き型 T extends U ? X : Y
が X
または Y
に 解決される か、評価が 遅延 されるかは、条件が型変数に依存するかどうかによります。
T
、U
に型変数が含まれている場合、型システムが T
は常に U
に代入可能と判断できるだけの情報を持っているかどうかに応じて、X
または Y
に解決されるか、評価が遅延されるかが決まります。
型がすぐに解決される例を見てみましょう。
declare function f<T extends boolean>(x: T): T extends true ? string : number;
// 型は 'string | number
let x = f(Math.random() < 0.5)
true
型なんてものがあるのかー。
そしてコンパイル時に引数がtrue
になるかfalse
になるか分からないから、どちらになっても受け取れるstring | number
型になるということかな。
次の例では TypeName
型エイリアスを使用していますが、これはネストした条件付き型です。
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";
type T0 = TypeName<string>; // "string"
type T1 = TypeName<"a">; // "string"
type T2 = TypeName<true>; // "boolean"
type T3 = TypeName<() => void>; // "function"
type T4 = TypeName<string[]>; // "object"
逆に、評価が遅延される例を見てみましょう。
interface Foo {
propA: boolean;
propB: boolean;
}
declare function f<T>(x: T): T extends Foo ? string : number;
function foo<U>(x: U) {
// 'U extends Foo ? string : number' 型になる
let a = f(x);
// ただし、この代入は可能!
let b: string | number = a;
}
この例では、a
はどちらの型になるか決定できない条件付き型となります。
他のコードから foo
が呼び出されると U
の型が決まるため、条件が再評価されてどちらの型になるかが決定されます。
それまでの間でも、条件付き型のどちらの型でも代入可能な型であれば、条件付き型を代入することができます。
上記の例では U extends Foo ? string : number
型を string | number
型に代入していますが、これは条件付き型が string
型か number
型のいずれかに解決されることが分かっているため、問題ありません。
Distributive conditional types
チェック対象の型が裸の型引数である条件付き型は 分配条件付き型 と呼ばれており、インスタンス化時には自動的に共用型に分配されます。
例えば、T extends U ? X : Y
型において、T
を A | B | C
としてインスタンス化する場合、(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)
として解決されます。
type T10 = TypeName<string | (() => void)>; // "string" | "function"
type T12 = TypeName<string | string[] | undefined>; // "string" | "object" | "undefined"
type T11 = TypeName<string[] | number[]>; // "object"
分配条件付き型 T extends U ? X : Y
をインスタンス化する場合、T
への参照は共用型の各要素に解決されます。(つまり、条件付き型の分配 後、T
は共用型の個々の要素を表す事になります)
さらに、X
内の T
の参照も U
に対する追加制約 (つまり、U
に代入可能かどうか) を受けることになります。
type BoxedValue<T> = { value: T };
type BoxedArray<T> = { array: T[] };
type Boxed<T> = T extends any[] ? BoxedArray<T[number]> : BoxedValue<T>;
type T20 = Boxed<string>; // BoxedValue<string>;
type T21 = Boxed<number[]>; // BoxedArray<number>;
type T22 = Boxed<string | number[]>; // BoxedValue<string> | BoxedArray<number>;
この例では Boxed<T>
の条件が真となった場合に T
が any[]
という追加の制約を受けるため、配列の要素の型を T[number]
として参照することができます。
また、最後の例で条件付き型がどのように共用型へ分配されるかについても確認してください。
何も考えずに
T extends any[] ? BoxedArray<T> : BoxedValue<T>
としてしまうとBoxedArray<number[]>
になってしまうというわけだ。
これを理解して活用するのはかなり無理ゲーな気が...。
条件付き型の分配プロパティは filter 共用型として使用するのに便利です。
type Diff<T, U> = T extends U ? never : T; // U に代入可能な型を T から取り除く
type Filter<T, U> = T extends U ? T : never; // U に代入できない型を T から取り除く
type T30 = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "b" | "d"
type T31 = Filter<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "a" | "c"
type T32 = Diff<string | number | (() => void), Function>; // string | number
type T33 = Filter<string | number | (() => void), Function>; // () => void
type NonNullable<T> = Diff<T, null | undefined>; // T から null、undefined を取り除く
type T34 = NonNullable<string | number | undefined>; // string | number
type T35 = NonNullable<string | string[] | null | undefined>; // string | string[]
function f1<T>(x: T, y: NonNullable<T>) {
x = y; // OK
y = x; // エラー
}
function f2<T extends string | undefined>(x: T, y: NonNullable<T>) {
x = y; // OK
y = x; // エラー
let s1: string = x; // エラー
let s2: string = y; // OK
}
条件付き型はマップ型と組み合わせると特に有用です。
type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T];
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;
type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T];
type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;
interface Part {
id: number;
name: string;
subparts: Part[];
updatePart(newName: string): void;
}
type T40 = FunctionPropertyNames<Part>; // "updatePart"
type T41 = NonFunctionPropertyNames<Part>; // "id" | "name" | "subparts"
type T42 = FunctionProperties<Part>; // { updatePart(newName: string): void }
type T43 = NonFunctionProperties<Part>; // { id: number, name: string, subparts: Part[] }
共用型や交差型と同様に、条件付き型も自分自身への参照は許可されていません。
そのため、以下の例はエラーとなります。
type ElementType<T> = T extends any[] ? ElementType<T[number]> : T; // エラー
Type inference in conditional types
条件付き型の extends
句において、推測すべき型変数を指定する infer
宣言が使用できるようになりました。
推測された型は条件付き型の真側の分岐で使用できます。
また、同じ型変数で複数の infer
を使用することも可能です。
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
(...args: any[]) => R
のR
を型として使用する、という意味ね
条件付き型はネストすることができます。
type Unpacked<T> =
T extends (infer U)[] ? U :
T extends (...args: any[]) => infer U ? U :
T extends Promise<infer U> ? U :
T;
type T0 = Unpacked<string>; // string
type T1 = Unpacked<string[]>; // string
type T2 = Unpacked<() => string>; // string
type T3 = Unpacked<Promise<string>>; // string
type T4 = Unpacked<Promise<string>[]>; // Promise<string>
type T5 = Unpacked<Unpacked<Promise<string>[]>>; // string
共変の位置にある同じ型変数において、複数の候補がある場合に共用型が推測される例を以下に示します。
type Foo<T> = T extends { a: infer U, b: infer U } ? U : never;
type T10 = Foo<{ a: string, b: string }>; // string
type T11 = Foo<{ a: string, b: number }>; // string | number
同様に、反変の位置にある同じ型変数において、複数の候補がある場合に交差型が推測される例を以下に示します。
type Bar<T> = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never;
type T20 = Bar<{ a: (x: string) => void, b: (x: string) => void }>; // string
type T21 = Bar<{ a: (x: string) => void, b: (x: number) => void }>; // string & number
複数の呼び出しシグネチャ (関数オーバーロードなど) から型を推測する場合、(おそらく一番許容的である) 最後の 呼び出しシグネチャを基に型が推測されます。
そのため、引数の型からオーバーロードを解決することはできません。
declare function foo(x: string): number;
declare function foo(x: number): string;
declare function foo(x: string | number): string | number;
type T30 = ReturnType<typeof foo>; // string | number
ReturnType
の宣言は上の例参照。
通常の型引数で infer
宣言を使用することはできません。
type ReturnType<T extends (...args: any[]) => infer R> = R; // エラー、未対応
ただし、制約内の型変数を消去し、代わりに条件付き型を指定することで、ほぼ同等の効果を得ることができます。
type AnyFunction = (...args: any[]) => any;
type ReturnType<T extends AnyFunction> = T extends (...args: any[]) => infer R ? R : any;
Predefined conditional types
TypeScript 2.8 において、いくつかの条件付き型が lib.d.ts
に追加されています。
-
Exclude<T, U>
–U
に代入可能な型をT
から取り除く。 -
Extract<T, U>
–U
に代入可能な型をT
から抽出する。 -
NonNullable<T>
–T
からnull
、undefined
を取り除く。 -
ReturnType<T>
– 関数型の戻り値の型を取得する。 -
InstanceType<T>
– コンストラクタ関数型からインスタンスの型を取得する。
type T00 = Exclude<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "b" | "d"
type T01 = Extract<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "a" | "c"
type T02 = Exclude<string | number | (() => void), Function>; // string | number
type T03 = Extract<string | number | (() => void), Function>; // () => void
type T04 = NonNullable<string | number | undefined>; // string | number
type T05 = NonNullable<(() => string) | string[] | null | undefined>; // (() => string) | string[]
function f1(s: string) {
return { a: 1, b: s };
}
class C {
x = 0;
y = 0;
}
type T10 = ReturnType<() => string>; // string
type T11 = ReturnType<(s: string) => void>; // void
type T12 = ReturnType<(<T>() => T)>; // {}
type T13 = ReturnType<(<T extends U, U extends number[]>() => T)>; // number[]
type T14 = ReturnType<typeof f1>; // { a: number, b: string }
type T15 = ReturnType<any>; // any
type T16 = ReturnType<never>; // any
type T17 = ReturnType<string>; // エラー
type T18 = ReturnType<Function>; // エラー
type T20 = InstanceType<typeof C>; // C
type T21 = InstanceType<any>; // any
type T22 = InstanceType<never>; // any
type T23 = InstanceType<string>; // エラー
type T24 = InstanceType<Function>; // エラー
Note:
Exclude
型は ここ で提案されていた Diff
型を適切に実装したもので、定義済みの Diff
を持つコードを壊さないように Exclude
という名前を使用するようにしました。それに加えて、この名前は型の意味をよく伝えていると思っています。
また、Omit<T, K>
型は Pick<T, Exclude<keyof T, K>>
とすることで簡単に実装できるため、この中に含めていません。