HTML5テクニカルノート
TypeScript: 高度な型
- ID: FN1805001
- Technique: HTML5 / ECMAScript 2015
- Package: TypeScript 3.7.2
TypeScriptには基本型に加え、より複雑な型を表す機能が備わっています。公式Handbook「Advanced Types」にもとづき、解説とコード例を大きく改めました。
01 交差型
交差型(Intersection Type)は、複数の型をひとつに合成します。すでにある型を組み合わせて、必要な機能の兼ね備わった型ができるのです。ひとつに合成された型には、もとの型のすべてのメンバーが含まれます。
つぎの関数(extend()
)は、引数ふたつのオブジェクトを合成して、交差型の新たなオブジェクトにして返します(コード001)。複数の型を結ぶのは演算子&
です。戻り値のオブジェクトは、もとのふたつのオブジェクトが備えるメソッドのどちらも呼び出せます。
コード001■ふたつの引数のオブジェクトを交差型オブジェクトにして返す関数
function extend<T, U>(first: T, second: U): T & U {
return { ...first, ...second };
}
const length = {
x: 0,
y: 0,
getLength() { return Math.hypot(this.x, this.y); }
};
const angle = {
x: 1,
y: Math.sqrt(3),
getAngle() { return Math.atan2(this.y, this.x); }
};
const polar = extend(length, angle);
console.log(polar.getLength(), polar.getAngle() * 180 / Math.PI);
// 2 59.99999999999999
なお、スプレッド構文で複数のオブジェクトをひとつにまとめたとき、同じ名前のプロパティはあとの値で上書きされます。
02 共用体型
共用体型(Union Type)も複数の型をひとつにまとめます。けれど、交差型が複数の型のすべてのメンバーを備えるのに対して、共用体型はいずれかひとつの型と一致すれば足ります。
つぎのコードは、共用体型を使っていない関数の例です。数値を文字列にしたうえで、頭に0を加えて桁揃えします(コードの中身については「数字の頭に0を加えて桁揃えする4つのやり方」の「String.prototype.padStart()メソッドで文字列の前に0を加える」参照)。第1引数の数は文字列も受け取れるよう、any
で型づけしました。第2引数は揃える桁数です。any
は数値(number
)と文字列(string
)にかぎらず文字どおり何のデータでもとおってしまうため、それ以外のオブジェクトなどを渡すと意図しない結果が返されてしまいます。
function setDigits(number: any, digits: number): string { if (typeof number === 'string') { number = parseInt(number); } else { number = Math.floor(number); } const number_string = String(number).padStart(digits, '0'); return number_string; } console.log(setDigits(7, 3)); // 007 console.log(setDigits('7', 3)); // 007 console.log(setDigits({}, 5)); // 00NaN
つぎのコード002は、関数の第1引数を数値(number
)と文字列(string
)の共用体型に書き替えました。このふたつの型以外(たとえばオブジェクト)は、コンパイルエラーとなって渡せません。ただし、整数に変換できない文字列は受け取れるものの、NaN
となってしまうので、空文字列を返すことにしました。
コード002■共用体型により数値と文字列のみ受け取る関数
function setDigits(number: number | string, digits: number): string {
if (typeof number === 'string') {
number = parseInt(number);
} else {
number = Math.floor(number);
}
if (Number.isNaN(number)) { return ''; }
const number_string = String(number).padStart(digits, '0');
return number_string;
}
console.log(setDigits(7, 3)); // 007
console.log(setDigits('7', 3)); // 007
console.log(setDigits('1abc', 3)); // 001
console.log(setDigits('abcd', 3)); // (空文字列)
// console.log(setDigits({}, 5)); // TypeScript error
共用体型で定めたデータには、いずれのメンバーでも与えられます。けれど、TypeScriptはその中のひとつの型に決められません。そのため、参照できるのは共通のメンバーだけです。
interface ILength { x: number; y: number; getLength: () => number; }; interface IAngle { x: number; y: number; getAngle: () => number; } const length: ILength | IAngle = { x: 0, y: 0, getLength() { return Math.hypot(this.x, this.y); }, getAngle() { return Math.atan2(this.y, this.x); }, }; const angle: ILength | IAngle = { x: 1, y: Math.sqrt(3), getAngle() { return Math.atan2(this.y, this.x); } }; console.log(length.x, length.y); // 0 0 // console.log(length.getLength()); // TypeScript error console.log(angle.getAngle() * 180 / Math.PI); // 59.99999999999999
ただし、as
によりメンバーを備えた型に変換すれば、参照できるようになります。
console.log((length as ILength).getLength()); // 0
03 型ガードの関数を定める
前掲コードのふたつのインタフェースにしたがい、それぞれにの型のオブジェクトを定めたのがつぎのコード003です。
コード003■ふたつのインタフェースとそれぞれの型にしたがったオブジェクトを定めた
interface ILength {
x: number;
y: number;
getLength: () => number;
};
interface IAngle {
x: number;
y: number;
getAngle: () => number;
}
const length: ILength = {
x: Math.SQRT1_2,
y: Math.SQRT1_2,
getLength() { return Math.hypot(this.x, this.y); },
};
const angle: IAngle = {
x: 1,
y: Math.sqrt(3),
getAngle() { return Math.atan2(this.y, this.x); }
};
console.log(length.getLength()); // 1.0000000000000002
console.log(angle.getAngle() * 180 / Math.PI); // 59.99999999999999
さてここで、引数がふたつのインタフェースの共用体型で定められた関数を考えましょう。戻り値は、それぞれにひとつずつ備わったメソッドが返す数値です。このような場合、JavaScriptではメソッドのあるなしを調べてから呼び出す、という手法がよく用いられます。ただし、共用体型で型づけしたとき気をつけるのは、前述のとおりすぺての型が備えているのでないメソッドを参照する場合は型変換しなければならないことです。
function calculate(object: ILength | IAngle) { if ((object as ILength).getLength) { return (object as ILength).getLength(); } else { return (object as IAngle).getAngle() * 180 / Math.PI; } } console.log(calculate(length)); // 1.0000000000000002 console.log(calculate(angle)); // 59.99999999999999
型ガード(Type Guard)という仕組みは、型を確かめたうえで、そのブロックのスコープ内は判別した型のデータとして扱うものです。そのため、ブロックの中では型変換はせずに済みます。型ガードを定めるのは、つぎの構文にもとづく関数です。
function isTypeA(parameter: TypeA | TypeB): parameter is TypeA { return ブール値; }
前掲の関数(calculate()
)のための型ガード(isLength()
)を定めたのが、以下のコード004です。この型ガードにより、つぎのように関数本体のif
とelse
ブロック内は型変換が要らなくなりました。
function calculate(object: ILength | IAngle) { // if ((object as ILength).getLength) { if (isLength(object)) { // return (object as ILength).getLength(); return object.getLength(); } else { // return (object as IAngle).getAngle() * 180 / Math.PI; return object.getAngle() * 180 / Math.PI; } }
コード004■型ガードを定める関数
function isLength(object: ILength | IAngle): object is ILength {
return !!(object as ILength).getLength;
}
04 typeof演算子による型ガード
引数に受け取った数値か文字列について、整数の桁数あるいは文字数を返すのがつぎのコードの関数(getLength()
)です。型ガードの関数(isNumber()
)が、typeof
演算子により数値化文字列かを判定しています。
function isNumber(value: number | string): value is number { return typeof value === 'number'; } function getLength(value: number | string) { if (isNumber(value)) { return Math.floor(Math.log10(Math.abs(value))) + 1; } else { return value.length; } } console.log(getLength(1234)); // 4 console.log(getLength('abcd')); // 4
もっとも、TypeScriptはtypeof
演算子による型の評価は、つぎの場合には関数を切り分けなくても型ガードとみなして扱います。
typeof
演算子の戻り値を厳密な等価(===
)もしくは不等価(!==
)で比較している。- 比較する値の文字列が、
"number"
、"string"
、"boolean"
、"symbol"
いずれかのプリミティブである。
したがって、上述のJavaScriptコードは型ガード関数を使うことなく、つぎのコード005のように書き替えられます。
コード005■typeof演算子で型ガードした関数
function getLength(value: number | string) {
if (typeof value === 'number') {
return Math.floor(Math.log10(Math.abs(value))) + 1;
} else {
return value.length;
}
}
05 instanceof演算子による型ガード
typeof
演算子による評価が型ガードになるのはプリミティブだけで、オブジェクトには使えませんでした。オブジェクトに用いることのできる型ガードは、instanceof
演算子です。オブジェクトのconstructor.prototype
プロパティのチェーンにコンストラクタのObject.prototype
が含まれるかどうかによって、型の一致を確かめます。ふたつのインタフェース(IPoint
とIPolar
)が実装されたクラスを定め、instanceof
演算子による型ガードでインスタンスを判別したのがつぎのコード006の関数(calculate()
)です。ひとつ目の型(Polar
)を調べたあと、もうひとつの型(Point
)も明示的に調べなければなりません。そのため、構文にelse
でなくelse ifを
用いました。
コード006■instanceof演算子で型ガードした関数
interface IPoint {
x: number;
y: number;
getLength: () => number;
};
interface IPolar {
x: number;
y: number;
getAngle: () => number;
}
class Point implements IPoint {
constructor(public x: number = 0, public y: number = 0) {}
getLength() { return Math.hypot(this.x, this.y); }
};
class Polar extends Point implements IPolar {
constructor(length: number = 1, angle: number = 0) {
super(length * Math.cos(angle), length * Math.sin(angle));
}
getAngle() { return Math.atan2(this.y, this.x); }
};
function calculate(object: IPoint | IPolar) {
if (object instanceof Polar) {
return object.getAngle() * 180 / Math.PI;
} else if (object instanceof Point) {
return object.getLength();
}
}
const point = new Point(Math.SQRT1_2, Math.SQRT1_2);
const polar = new Polar(1, 60 * Math.PI / 180);
console.log(calculate(point)); // 1.0000000000000002
console.log(calculate(polar)); // 59.99999999999999
console.log(polar.getLength()); // 1
console.log(polar instanceof Point); // true
06 nullを受け入れる型
06-01 --strictNullChecksモード
TypeScriptには、特別なnull
型とundefined
型があります。それぞれ、値はnull
とundefined
です。デフォルトでは、null
とundefined
はすべての型に代入できます。それを避けるためのフラグは、--strictNullChecks
です(「Compiler Options」参照)。なお、--strict
オプションがtrue
の場合も、--strictNullChecks
はtrue
として扱われます。
tsconfig.json{ "compilerOptions": { "strictNullChecks": true } }
--strictNullChecks
のモードでは、他の型で宣言された変数にはnull
もundefined
も値として納められません。これらの値を受け入れるためには、共用体型で型づけにはっきりと含めなければならないのです。また、このモードはnull
とundefined
を異なる扱いとすることにご注意ください。共用体型にnull
型を加えていても、undefined
は与えられません。そのためには、undefined
型も含めなければならないのです。
let pureString = 'string'; // pureString = null; // コンパイルエラー let stringIncludingNull: string | null = 'another string'; stringIncludingNull = null; // OK // stringIncludingNull = undefined; // コンパイルエラー
06-02 オプションの引数とプロパティ
--strictNullChecks
モードでは、オプションの引数は自動的にundefined
型が加わった共用体型になります。論理演算子||
を数値に用いた結果については、「if文なしに論理演算子で条件判定の処理をする」をお読みください。
function createPoint(x: number, y?: number) { return {x: x, y: y || 0}; } console.log(createPoint(1, Math.sqrt(3))); // {x: 1, y: 1.7320508075688772} console.log(createPoint(1)); // {x: 1, y: 0} console.log(createPoint(1, undefined)); // {x: 1, y: 0} // console.log(createPoint(1, null)); // コンパイルエラー
これは、クラスのオプションのプロパティについても同じです。
class Point { constructor(public x: number, public y?: number) { this.y = y || 0; } } const point = new Point(1, Math.sqrt(3)); console.log(point); // {x: 1, y: 1.7320508075688772} console.log(new Point(1)); // {x: 1, y: 0} console.log(new Point(1, undefined)); // {x: 1, y: 0} // console.log(new Point(1, null)); // コンパイルエラー point.y = undefined; console.log(point); // {x: 1, y: undefined} // point.x = undefined; // コンパイルエラー
06-03 型ガードと型変換
--strictNullChecks
モードで共用体型によりnull
を含めたとき、値から除くには型ガードのコードを書き加えなければなりません。たとえば、つぎのようなJavaScriptコードです。なお、演算子に厳密な等価===
でなく等価==
を用いた場合、null
とundefined
は等しいとして扱われます。
function getPureString(value: string | null): string { if (value == null) { return 'default'; } else { return value; } }
あるいは、論理演算子||
を用いて、もっと簡単に書くこともできます(「if文なしに論理演算子で条件判定の処理をする」01「初期化されていない変数にデフォルト値を与える」参照)。
function getPureString(value: string | null): string { return value || 'default'; }
つぎの例は、関数(callEpithet()
)が入れ子の関数(postfix()
)を含む場合です。関数本体のたとえばローカル変数(_name
)については、代入などから型推論できます。
function callEpithet(name: string | null): string { let _name = name || 'Bob'; function postfix(epithet: string) { return (_name).charAt(0) + '. the ' + epithet; } return postfix('great'); } console.log(callEpithet(null)); // B. the great
けれど、関数(callEpithet()
)が外からどう呼び出されたかを捉えることはできず、引数(name
)の型についてもあくまで宣言に頼るしかないのです。
function callEpithet(name: string | null): string { name = name || 'Bob'; function postfix(epithet: string) { return (name).charAt(0) + '. the ' + epithet; } return postfix('great'); } console.log(callEpithet('Bill')); // コンパイルエラー
それを避けるため、つぎのコードでは共用体型でnull
が含まる引数(name
)に対してメソッドを呼び出すために型アサーションas
で変換しています(「Type Assertion(型アサーション)」参照)。
function callEpithet(name: string | null): string { function postfix(epithet: string) { return (name as string).charAt(0) + '. the ' + epithet; } name = name || 'Bob'; return postfix('great'); } console.log(callEpithet(null)); // B. the great
この場合、非nullアサーション演算子!
をつぎのように変数(name
)のあとに添えれば、識別子の型からnull
とundefined
が除けます。
function callEpithet(name: string | null): string { function postfix(epithet: string) { return name!.charAt(0) + '. the ' + epithet; } name = name || 'Bob'; return postfix('great'); }
07 型エイリアス
型エイリアス(Type Alias)は、型に新たな名前を与えるものです(「型エイリアス(Type Alias)」参照)。インタフェースと似ています。けれど、プリミティブや共用体型、タプル型など、どのような型にもエイリアスはつくれます。宣言に用いるキーワードはtype
です。
type Numeric = number; type NumericResolver = (x: number) => number; type NumericOrResolver = Numeric | NumericResolver; function calc(n: number, operation: NumericOrResolver) { if (typeof operation === 'number') { return n * operation; } else if (operation instanceof Function) { return operation(n); } } console.log(calc(2, 3)); // 6 console.log(calc(0, Math.cos)); // 1
型エイリアスは、新たな型をつくるのではありません。型を参照する新しい名前ができるだけです。プリミティブの型エイリアスは、さほど有用とはいえません。けれど、ドキュメントのかたちで使われることはあるでしょう。
インタフェースと同じく、ジェネリック型の型エイリアスもできます。型パラメータを加えて、エイリアス宣言の右辺に用いるだけです。
type Container<T> = {value: T};
定めた型エイリアスのプロパティを、そのエイリアスで型づけすることもできます。
type Tree<T> = { value: T; left: Tree<T>; right: Tree<T>; };
さらに、交差型とともに用いると、複雑な型もつくれます。つぎの連結リストのコード007は、型エイリアス(LinkedList<T>
)を交差型で定め、プロパティ(next
)にそのエイリアスで型づけをしました。すると、そのプロパティのオブジェクトがまた同じ型エイリアスのプロパティをもつ、というように循環することになります。そこで、オブジェクトのクラス(PersonsList
)はコンストラクタが、オプションの引数(next
)にプロパティのオブジェクトを受け取ることにしました。前述06-02のとおり、オプションの引数はundefined
が受け取れます。そこで循環が切れるのです。
コード007■型エイリアスと交差型を用いた連結リスト
type LinkedList<T> = T & {next: LinkedList<T>};
interface Person {
name: string;
}
class PersonsList implements LinkedList<Person> {
public next: LinkedList<Person>;
constructor(public name: string, next?: LinkedList<Person>) {
this.next = next || this;
}
}
const people: LinkedList<Person> = new PersonsList(
'Alice', new PersonsList(
'Cheshire Cat', new PersonsList('Carroll')
)
);
console.log(people.name); // Alice
console.log(people.next.name); // Cheshire Cat
console.log(people.next.next.name); // Carroll
console.log(people.next.next.next.name); // Carroll
console.log(people.next.next.next.next.name); // Carroll
Handbook「Advanced Types」の「Type Aliases」の項は、型宣言の右辺に型エイリアスは使えないとしています。けれど、型エイリアスを右辺に用いること自体は可能です。
type People = {name: string}; type Persons = Array<People>; const persons: Persons = [ {name: 'Alice'}, {name: 'Cheshire Cat'}, {name: 'Carroll'} ];
ただ、例として示されているのは、型エイリアスそのものを右辺に置いたつぎの定めです。前掲コード007のような循環を切る考慮がされていないため、使えないということでしょう。
type Yikes = Array<Yikes>;
インタフェースと型エイリアスのどちらを使うか
型エイリアスは、インタフェースと同じように使えます。異なるのは、インタフェースはどこからでも使える新たな名前をつくるということです。これは、型エイリアスではできません。
TypeScript 2.7より前まで、型エイリアスは継承や実装ができませんでした。2.7からは交差型を使えば拡張は可能です。
type Cat = Animal & { purrs: true }
拡張に対して開かれたソフトウェアをつくるという原則からは、できるだけインタフェースを用いるぺきでしょう。他方で、共用体型やタプル型を用いるとき、インタフェースで型を表現するのがむずかしい場合もあります。そのようなときには、型エイリアスを使ってください。
08 文字列リテラル型
文字列リテラル型(String Literal Type)は、文字列型のとるべき値を直に定めた型です。共用体型と型ガードを組み合わせることで、決まった文字列だけを型の値として扱えます。文字列の値を使って列挙型のような処理ができるのです。
つぎのコード008は、文字列リテラル型で4つの矢印キー定めました。クラスのコンストラクタにキーの文字列を渡してつくったインスタンスは、その矢印キーが押されたかどうかを監視(押されたらコンソールに出力)するというものです。コンストラクタの引数に文字列リテラル型にない文字列を渡すと、コンパイルエラーになります。
コード008■文字列リテラル型で定めた矢印キーが押されたかどうかを監視する
type ArrowKey = 'left' | 'right' | 'up' | 'down';
class InspectArrowKey {
keyName?: string;
keyCode?: number;
constructor(key: ArrowKey) {
if (key === 'left') {
this.inspect(key, 37);
} else if (key === 'up') {
this.inspect(key, 38);
} else if (key === 'right') {
this.inspect(key, 39);
} else if (key === 'down') {
this.inspect(key, 40);
}
}
inspect(keyName:string, keyCode: number) {
this.keyName = keyName;
this.keyCode = keyCode;
document.addEventListener('keydown', (event) => {
if (event.keyCode === this.keyCode) {
console.log(this.keyName, this.keyCode);
}
});
}
}
const inspectLeft = new InspectArrowKey('left');
const inspectRight = new InspectArrowKey('right');
// const inspectShift = new InspectArrowKey('shift'); // コンパイルエラー
文字列リテラル型は、オーバーロードする関数を分けるために使うこともできます。つぎの関数は文字列の引数を文字列リテラル型で定めました。
function createElement(tagName: 'img'): HTMLImageElement; function createElement(tagName: 'input'): HTMLInputElement; // ... 必要があれば追加 ... function createElement(tagName: string): Element { const element = document.createElement(tagName); return element; } console.log(createElement('img')); // <img> console.log(createElement('input')); // <input> // console.log(createElement('div')); // コンパイルエラー
09 数値リテラル型
数値についても、数値リテラル型(Numeric Literal Type)があります。数値型のとるぺき値そのものを、数値で型として与えられるのです。
type DiceNum = 1 | 2 | 3 | 4 | 5 | 6; function rollDice(): DiceNum { const randomInt = (Math.floor(Math.random() * 6) + 1) as DiceNum; return randomInt; }
数値リテラル型は、暗黙で働くこともあります。まず、つぎのコードは働かない場合です。関数本体のif
条件の式に、論理演算子||
が用いられています。けれど、右の式は意味がありません。左の式がfalse
と評価されたとき、右の式はつねにfalse
だからです。
function compare(x: number) { if (x > 1 || x > 2) { // } }
以下のコードでは、うえの例の比較演算子(>
)をそれぞれ不等価(!==
)と等価(===
)に替えました。やはり、右の式は意味がありません。それだけでなく、右の式についてコンパイルエラーが起こります(図001)。エラーメッセージは、つぎのとおりです。変数(x
)は数値リテラル型として扱われます。そして値が2のとき左の式がtrue
と評価されるので、右の式が使われることはないのです。
型 '1' と '2' には重複がないため、この条件は常に 'false' を返します。function compare(x: number) { if (x !== 1 || x === 2) { // } }
図001■if条件の右の式がコンパイルエラーになる
10 列挙型メンバーの型
列挙型(enum
)は、メンバーすべてがリテラルで初期値を与えられている場合には、その値は型として扱われます。詳しくは、「Enums」の「Union enums and enum member types」の項をお読みください。
enum KeyEvent { keyUp = 'keyup', keyDown = 'keydown' } function InspectKeyDown(keyEvent: KeyEvent.keyDown) { document.addEventListener(keyEvent, (event) => { console.log(event.keyCode); }); } InspectKeyDown(KeyEvent.keyDown); // InspectKeyDown(KeyEvent.keyUp); // コンパイルエラー
11 判別共用体型
リテラル型に共用体型、さらに型ガードと型エイリアスを組み合わせると、判別共用体型(Discriminated Union)という応用的な型の仕組みがつくれます(判別共用体、あるいは代数的データ型、タグ付き共用体型などとも呼ばれる考え方です)。関数型プログラミングにとくに役立ちます。TypeScriptは、JavaScriptの構造にもとづいて判別共用体型を組み立てました。つぎの3つの要素から成り立ちます。
- 共通のリテラル型プロパティを備えた判別のための型 ー 判別
- 判別用の型からなる共用体型の型エイリアス ー 共用
- 共通のプロパティの型ガード
まず定めるのはインタフェースで、それをつぎに共用体型にまとめるという手順です。インタフェースには共通のプロパティ(kind
)をひとつ定め、判別のための異なる文字列リテラル型が与えられます。このプロパティは判別やタグとも呼ばれます。インタフェースのその他のプロパティは、それぞれ違って構いません。つまり、インタフェースは同じ名前のプロパティがあるほかは、互いに何も関わりはないのです。そのうえで、これらのインタフェースを共用体型にまとめて、型エイリアス(Shape
)として定めます。
type Shape = Square | Rectangle | Circle; interface Square { kind: 'square'; size: number; } interface Rectangle { kind: 'rectangle'; width: number; height: number; } interface Circle { kind: 'circle'; radius: number; }
それでは、インタフェースに共通に与えたプロパティ(kind
)に対して、型ガードの処理を加えましょう。関数の引数(shape
)は、共用体型のエイリアス(Shape
)で型づけします。そして、switch
文で共通のプロパティの型を確かめればよいのです。プロパティの文字列リテラル型に与えられていない文字列と比較しようとすれば、コンパイルエラーになります。
function area(shape: Shape) { switch (shape.kind) { case 'square': return shape.size ** 2; case 'rectangle': return shape.height * shape.width; case 'circle': return Math.PI * (shape.radius ** 2); // case 'triangle': // コンパイルエラー // return shape.base * shape.height / 2; } } const square: Square = {kind: 'square', size: 2}; const circle: Circle = {kind: 'circle', radius: 1 / Math.sqrt(Math.PI)}; console.log(area(square), area(circle)); // 4 0.9999999999999999
12 型チェックの徹底
前項のコード例に、新たなインタフェース(Triangle
)を加え、共用体型(Shape
)に含めてみます。
// type Shape = Square | Rectangle | Circle; type Shape = Square | Rectangle | Circle | Triangle; interface Triangle { kind: 'triangle'; base: number; height: number; }
すると、関数(area()
)本体の型ガードで、switch
文が判別共用体型のすべてのcase
を拾いきれていません。つまり、関数の戻り値にundefined
が含まれるということです。--strictNullChecks
モードであれば、戻り値に型(number
)を与えることによりコンパイルエラーで確かめられます。
// function area(shape: Shape) { function area(shape: Shape): number { // undefinedはコンパイルエラー switch (shape.kind) { case 'square': return shape.size ** 2; case 'rectangle': return shape.height * shape.width; case 'circle': return Math.PI * (shape.radius ** 2); // case 'triangle': がない } }
--strictNullChecks
モードでなくても型チェックを徹底できるのが、never
型です。取り得る型がすべて除かれた型を表します。条件分岐で取りこぼしがあると、取れる型が残されてしまいます。すると、never
型で受け入れられないために、コンパイルエラーが起こるのです(図002)。never
型判別の関数を新たに加えなければならないものの、拾い漏れをよりはっきりと確かめられます。コード全体は、以下のコード009にまとめました。
function area(shape: Shape): number { switch (shape.kind) { default: return assertNever(shape); } } function assertNever(object: never): never { throw new Error('Unexpected object: ' + object); }
図002■拾い漏れた型があるとコンパイルエラーになる
コード009■判別共用体型のコードで型チェックを徹底する
type Shape = Square | Rectangle | Circle | Triangle;
interface Square {
kind: 'square';
size: number;
}
interface Rectangle {
kind: 'rectangle';
width: number;
height: number;
}
interface Circle {
kind: 'circle';
radius: number;
}
interface Triangle {
kind: 'triangle';
base: number;
height: number;
}
function area(shape: Shape): number {
switch (shape.kind) {
case 'square':
return shape.size ** 2;
case 'rectangle':
return shape.height * shape.width;
case 'circle':
return Math.PI * (shape.radius ** 2);
case 'triangle':
return shape.base * shape.height / 2;
default: return assertNever(shape);
}
}
function assertNever(object: never): never {
throw new Error('Unexpected object: ' + object);
}
13 多態なthis型
多態なthis型(Polymorphic this type)というのは、クラスやインタフェースを継承する型です(F-bounded polymorphismと呼ばれます)。階層をつなぐインタフェースが簡単につくれます。たとえば、つぎのクラス(Point
)のメソッド(add()
とscale()
)は、プロパティの座標計算のあとインスタンス(this
)を返します。すると、メソッドをつなげて呼び出すことができるのです。
class Point { constructor(public x: number = 0, public y: number = 0) {} get length(): number { return Math.sqrt(this.x ** 2 + this.y ** 2); } add(x: number, y: number): Point { this.x += x; this.y += y; return this; } scale(scale: number): Point { this.x *= scale; this.y *= scale; return this; } } const point = new Point(1) .add(0, Math.sqrt(3)) .scale(1/2); console.log(point.length); // 0.9999999999999999
ただし、このクラスを継承すると問題が生じます。スーパークラス(Point
)のメソッドが返すthis
は親の型のインスタンスなので、サブクラス(Polar
)のメソッド(rotate()
)が呼び出せないのです。
class Polar extends Point { static get(length: number = 1, angle: number = 0) { return new Polar( length * Math.cos(angle), length * Math.sin(angle) ); } rotate(angle: number): Polar { const sin = Math.sin(angle); const cos = Math.cos(angle); [this.x, this.y] = [ this.x * cos - this.y * sin, this.x * sin + this.y * cos, ]; return this; } } const point = Polar.get() .add(0, Math.sqrt(3)) .rotate(Math.PI / 3); // コンパイルエラー
そこで、this
を返すメソッドは、戻り値をthis
で型づけします。すると、サブクラス(Polar
)のインスタンスがスーパークラス(Point
)のメソッドを呼び出しても、自分のクラスの型のインスタンスが得られるのです。以下のコード010は、メソッドの戻り値をthis
型に改めました。すると、たとえばつぎのように、親クラスのメソッドから返されたインスタンスに対して、自分のクラスのメソッドが呼び出せます。なお、サブクラスにはget
メソッド(angle()
)を加え、this
が戻り値のメソッド(rotate()
)はthis
で型づけしました(さらに継承できるということです)。
const point = Polar.get() .add(0, Math.sqrt(3)) .rotate(Math.PI / 3) .scale(1 / 2); console.log(point.length, point.angle * 180 / Math.PI); // 1 120.00000000000001
コード010■戻り値がthis型のスーパークラスのメソッドから自クラスで型づけされたインスタンスを得る
class Point {
constructor(public x: number = 0, public y: number = 0) {}
get length(): number {
return Math.sqrt(this.x ** 2 + this.y ** 2);
}
add(x: number, y: number): this {
this.x += x;
this.y += y;
return this;
}
scale(scale: number): this {
this.x *= scale;
this.y *= scale;
return this;
}
}
class Polar extends Point {
static get(length: number = 1, angle: number = 0) {
return new Polar(
length * Math.cos(angle),
length * Math.sin(angle)
);
}
get angle(): number {
return Math.atan2(this.y, this.x);
}
rotate(angle: number): this {
const sin = Math.sin(angle);
const cos = Math.cos(angle);
[this.x, this.y] = [
this.x * cos - this.y * sin,
this.x * sin + this.y * cos,
];
return this;
}
}
14 インデックス型
インデックス型(Index type)の役割は、動的なプロパティ名が使われたコードをコンパイラでチェックすることです。そのために用いる演算子を先にご紹介しましょう。
まず、インデックス型クエリ演算子keyof
です。オペランド(被演算子)の型が備えるpublic
のプロパティ名を文字列リテラル型としてまとめた共用体型を返します。もとの型からプロパティを取り出して、自動的に文字列リテラルの共用体型がつくられるということです。インデックス型クエリ演算子は、ジェネリック型のクラスや関数にも使えます。引数に渡されたオブジェクトが、プロパティを実際に備えているかどうか確かめられるのです。
interface Person { name: string; age: number; } let personProps: keyof Person; // 'name' | 'age'の共用体型 personProps = 'name'; // personProps = 'sex'; // コンパイルエラー
つぎに、インデックスアクセス演算子[]
です。型からプロパティを参照し、その値の型が返されます。
型[プロパティ名]
let string: Person['name'] = 'Theta'; let number: Person['age'] = 12; // let error: Person['age'] = true; // コンパイルエラー
インデックスアクセス演算子[]
は、インデックス型クエリ演算子keyof
とともにジェネリック型に用いられることで真価が発揮されます。その場合、型変数にextends
キーワードを添えることにより、keyof
の型が拡張して加わるようにします。
つぎの関数(getProperty()
)は、オブジェクトからプロパティ名で取り出した値を返すため、戻り値の型はインデックスアクセス演算子[]
で定めています。第2引数(name
)に渡すプロパティ名によりこの型が変わりますので、それに応じて戻り値のデータ型も異なることになるのです。第1引数(object
)のオブジェクトにないプロパティを第2引数に渡せば、コンパイルエラーになります。
interface Person { name: string; age: number; } let person: Person = { name: 'Alice', age: 7 }; function getProperty<T, K extends keyof T>(object: T, name: K): T[K] { return object[name]; } console.log(getProperty(person, 'name')); // Alice console.log(getProperty(person, 'age')); // 7 // getProperty(person, 'sex'); // コンパイルエラー
オブジェクトから複数のプロパティを取り出す関数(pluck()
)に、インデックスで型づけしたのが以下のコード010です。第2引数(names
)には、つぎのようにプロパティ名の配列を渡します。プロパティは、Array.map()
メソッドで新たな配列にして返しています。
const properties: (string | number)[] = pluck(person, ['name', 'age']); console.log(properties); // ["Alice", 7] // pluck(person, ['sex']); // コンパイルエラー
コード011■インデックスで型づけしたオブジェクトからプロパティを取り出して返す関数
interface Person {
name: string;
age: number;
}
let person: Person = {
name: 'Alice',
age: 7
};
function pluck<T, K extends keyof T>(object: T, names: K[]): T[K][] {
return names.map(n => object[n]);
}
インタフェースにインデックスアクセス演算子[]
でプロパティ(インデックスシグネチャ)を定めると、任意の名前が与えられます(「TypeScript: インタフェース」04「余分なプロパティの扱い」)。TypeScriptのプロパティはstring
またはnumber
(あるいはsymbol
)のいずれかでなければなりません。つぎの型エイリアス(Dictionary
)は、[]
演算子でその型をstring
にかぎりました。名前(key
)は型さえ合えば自由に与えられます。
interface Dictionary<T> { [key: string]: T; } let object: Dictionary<number> = {property: 10}; // object = {0: 'text'}; // インデックスシグネチャと値ともに認められない型
インタフェースにkeyof
演算子を用いるとプロパティ名の型が、[]
演算子で任意のプロパティ名を渡すとその値の型が得られるのです。プロパティ名はインタフェース(Dictionary
)で[]
演算子により任意としましたので、型(string
)さえ合えば何でも構いません。
interface Dictionary<T> { [key: string]: T; } let keys: keyof Dictionary<number> = 'name'; // string let value: Dictionary<number>['property'] = 10; // number
15 型のマップ
ある型にもとづいて新たな型をつくるのが型のマップ(Mapped type)です。つぎのインタフェースから取り出したプロパティに、マップで少し手が加わえられた新たな型をつくってみましょう。
interface Person { name: string; age: number; }
まずマップは使わず、プロパティのあとに?
を添えると、そのプロパティが省けるようになります(「TypeScript: インタフェース」02「省けるプロパティを定める」)
interface PersonPartial { name?: string; age?: number; }
同じ働きの型はインデックスシグネチャのマップにより、つぎのようにつくれます。プロパティ名とその型は、マップ先の型(TPartial
)にそのまま移されます。ただし、ここで省略可能の定め(?
)が新たに加えられたのです。
type TPartial<T> = { [P in keyof T]?: T[P]; }; let personPartial: TPartial<Person> = {name: 'Alice'}; console.log(personPartial); // {name: "Alice"}
つぎに、プロパティを読み取り専用にするインタフェースです。このときは、プロパティの前に修飾子readonly
を添えます。
interface ReadonlyPerson { readonly name: string; readonly age: number; }
この型も、インデックスシグネチャでマップすればつくれます。つぎのようにreadonly
修飾子をマップしたプロパティに加えればよいのです。
type TReadonly<T> = { readonly [P in keyof T]: T[P]; }; let personReadonly: TReadonly<Person> = {name: 'Alice', age: 7}; console.log(personReadonly); // {name: "Alice", age: 7} // personReadonly.name = 'Theta'; // コンパイルエラー
これらふたつの型のマップは便利なので、実はTypeScriptの標準ライブラリにそれぞれユーティリティ型Partial<T>
とReadonly<T>
として備わっています。
type PersonPartial = Partial<Person>; type ReadonlyPerson = Readonly<Person>;
ひとつ注意しなければならないのは、マップした型の中にメンバーは含められないということです。
type PartialWithNewMember<T> = { [P in keyof T]?: T[P]; newMember: boolean; // 構文エラー }
メンバーは交差型で加えてください。
type PartialWithNewMember<T> = { [P in keyof T]?: T[P]; } & { newMember: boolean }
型をマップする構文の基本的な考え方は、for...in
文と似ています。構成する要素はつぎの3つです。
- 型変数(
K
): 取り出したプロパティを順に割り当てる - 文字列リテラルの共用体型(
Keys
): 割り当てるプロバティが収められている - 新たな型(
Flags
): マップされたプロパティで定められる
type Keys = 'option1' | 'option2'; type Flags = {[K in Keys]: boolean}; let options: Flags = {option1: true, option2: false};
この例では、共用体型の文字列リテラルをキーワードin
により、プロパティ名として取り出しました。そのうえで、いずれにもboolean
を型に与えたのです。したがって、つぎの型と同じ定めになります。
type Flags = { option1: boolean; option2: boolean; };
実際のアプリケーションでよく使われるのは、Partial<T>
やReadonly<T>
のように、すでにある型にもとづいてプロパティに手を加えることです。その場合、keyof
演算子でプロパティ名を取り出し、[]
演算子により型づけします。たとえば、--strictNullChecksモード
でもプロパティにnull
が与えられる型をつくるのがつぎのコードです。
type NullablePerson = { [P in keyof Person]: Person[P] | null } let person: NullablePerson = {name: 'Alice', age: null}; // let person: Person = {name: 'Alice', age: null}; // コンパイルエラー
Partial<T>
やReadonly<T>
のようにジェネリック型にすれば、より使い勝手は増すでしょう。
type Nullable<T> = {[P in keyof T]: T[P] | null};
このように、keyof
演算子によりプロパティのリストを取り出し(keyof T
)、[]
演算子で得た型(T[P]
)に手を加えるのは、使い回しのきくマップの仕方です。そして、マップするときにもとのプロパティと型の構造を崩しません。このような変換は準同型と呼ばれます(「準同型写像」参照)。新たな型にはもとのプロパティがそのまま引き継がれ、そこに読み取り専用や省略可能の定めが加わえられるというかたちです。
以下のコード011に定めた関数は、引数に受け取ったオブジェクトのプロパティを別に定めた型でラップして返します。その結果、戻り値の新たなオブジェクトには、つぎのようにプロパティにget()
/set()
メソッドが備わるのです。
let props = {name: 'Alice', age: 7}; let proxyProps = proxify(props); console.log(proxyProps.name.get(), proxyProps.age.get()); // Alice 7 proxyProps.name.set('Theta'); proxyProps.age.set(12); console.log(proxyProps.name.get(), proxyProps.age.get()); // Theta 12
コード012■引数に受け取ったオブジェクトのプロパティを別に定めた型でラップして返す関数
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 result = {} as Proxify<T>;
for (const k in o) {
result[k] = {
get() {return o[k]},
set(value) {o[k] = value;}
};
}
return result;
}
Partial<T>
とReadonly<T>
のほかにも、TypeScriptの標準ライブラリには型を変換するユーティリティが備わっています。「Partial, Readonly, Record, and Pick」に紹介されているひとつは、Pick<T, K>
です。もとの型からプロパティの一部を取り出して新たな型にします。Partial<T>
やReadonly<T>
と同じく、もとの型の構造を崩さない準同型です。
let personName: Pick<Person, 'name'> = {name: 'Alice'}; // let personName: Pick<Person, 'name'> = {name: 'Alice', age: 7}; // コンパイルエラー let personNameAge: Pick<Person, 'name' | 'age'> = {name: 'Alice', age: 7};
もうひとつはRecord<K,T>
で、同じ型のプロパティを複数定めます。与えるプロパティ名は文字列リテラルの共用体型です。もととなる型をもったプロパティがないので、準同型の変換ではありません。
let stringProps: Record<'prop1' | 'prop2', string> = {prop1: 'one', prop2: 'two'}
なお、Pick
とRecord
の実装を示すと、つぎのとおりです。
type Pick<T, K extends keyof T> = { [P in K]: T[P]; }; type Record<K extends string, T> = { [P in K]: T; };
前掲コード011のオブジェクトをラップして返す関数の変換は準同型です。その場合、もとの型のオブジェクトに戻すことも簡単にできます。その関数が以下のコード012です。つぎのようにして試せます。
let props = {name: 'Alice', age: 7}; let proxyProps = proxify(props); let originalProps = unproxify(proxyProps); console.log(originalProps); // {name: "Alice", age: 7} // originalProps.name.get() // コンパイルエラー proxyProps.name.set('Theta'); proxyProps.age.set(12); console.log(proxyProps.name.get(), proxyProps.age.get()); // Theta 12
コード013■ラップされたオブジェクトからもとの型のオブジェクトを再現する関数
function unproxify<T>(t: Proxify<T>): T {
let result = {} as T;
for (const k in t) {
result[k] = t[k].get();
}
return result;
}
16 条件に応じた型
16-01 条件に応じて型を決める
TypeScript 2.8から備わった、ひとつの基準によらないマップを行うのが条件に応じた型(Conditional type)です。条件に応じた型づけの基本は、ふたつの型から型の関係が示された条件式により、ひとつを選びます。つぎの条件(三項)演算子に似た構文が表す型は、T
にU
が割り当てられるならX
、そうでない場合はY
ということです。
T extends U ? X : Y
つぎのコードは、引数が論理値(boolean
)の関数(f()
)を宣言(declare
)し、その値によって戻り値の型(string | number
)を変えています。
declare function f<T extends boolean>(x: T): T extends true ? string : number; // 型はstring | numberになる let x = f(Math.random() < 0.5);
アンビエント宣言(declare)
declare
は「アンビエント宣言」(Ambient declaration)と呼ばれ、素のJavaScriptコードで書かれたグローバルな変数や関数に、TypeScriptモジュールで型づけして参照できるようにします(「The Examples」参照)。前掲コードのdeclare
により、たとえばつぎのようなグローバルなJavaScriptの関数(f()
)が呼び出せるということです。
index.html<script> function f(isString) { if (isString) { return 'string'; } else { return 1; } } </script>
すると、前掲TypeScriptのコード例はつぎのように試せます。
const random = Math.random(); let x = f(random < 0.5); console.log(random, x); /* 出力例 0.8790960256388505 1 0.4960091115976537 "string" */ x = 'other string'; x = 0;
条件演算子の構文と同じく、条件に応じた型づけは複数の条件を入れ子にしても構いません。
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 StringType = TypeName<string>; let stringType: StringType = 'string'; // stringType = 'number' // コンパイルエラー let numOrBoolOrFunc: TypeName<number | true | (() => void)> = 'function'; numOrBoolOrFunc = 'boolean'; // numOrBoolOrFunc = 'undefined' // コンパイルエラー let objectType: TypeName<Date> = 'object';
つぎのコード例では、アンビエント宣言された関数(f()
)の戻り値が条件に応じた型です。定義された関数(foo()
)本体から呼び出されるまで、返される値の型は決まりません。けれど、分岐先の型のいずれかになることはわかっています。ですから、それらの共用体型(string | number
)で受け取れるのです。
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); // string | number型の変数に代入できる let b: string | number = a; }
16-02 条件に応じた型の分配
基本的な型に条件を与えた場合、インスタンス化のとき共用体型に分配(分配法則にもとづく式の変形)されます。具体的には、型T extends U ? X : Y
のT
にA | B | C
が渡されると、(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)
に解決されるということです。
前出の型エイリアス(TypeName<T>
)をふたたび例にとりましょう。
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 stringOrFunction = TypeName<string | (() => void)>; // 'string' | 'function' type stringObjectOrUndefined = TypeName<string | string[] | undefined>; // 'string' | 'object' | 'undefined' type object = TypeName<string[] | number[]>; // 'object'
条件を分配できる型
「Distributive conditional types」は、条件を分配できるのは「素の型」(naked type)だと説明しています。もっとも、この用語は一般的なものではないようです。検索しても適切な解説が見当たりません。素の型でない例を見て、イメージをつかむしかなさそうです。具体的に、つぎのふたつの場合が探せました。
- 型定義から取り出したプロパティの型(「TypeScriptのUnion Typeをプロパティの値によってフィルタする (Redux)」)
- タプルでラップした型(「Typescript: what is a “naked type parameter”」)
変数に収められた型定義そのものであれば、分配できるととらえてよさそうです。
型T extends U ? X : Y
で、分岐先のX
とY
はそれぞれ条件に定められた型T
が参照できます。T
に共有体型が与えられたとき、分配される型T
にはご注意ください。X
に渡されるT
にはU
が適用され、Yの参照する型
にU
は適用されません。
type BoxedValue<T> = { value: T }; type BoxedArray<T> = { array: T[] }; type Boxed<T> = T extends any[] ? BoxedArray<T[number]> : BoxedValue<T>; const boxedValue: Boxed<string> = { value: 'string' }; // BoxedValue<string> const boxedArray: Boxed<number[]> = { array: [0, 1, 2] }; // BoxedArray<number> let boxedValueOrArray: Boxed<string | number[]> // BoxedValue<string> | BoxedArray<number> boxedValueOrArray = { value: 'string' }; boxedValueOrArray = { array: [0, 1, 2] };
条件を分配できる型は、共用体型のフィルタリングに使うと便利です。つぎの型の定めは、U
が適用できるT
の型を省いたり(Diff
)、逆に適用できる型だけが取り出せます(Filter
)。
type Diff<T, U> = T extends U ? never : T; // Uが適用できるTの型は除く type Filter<T, U> = T extends U ? T : never; // Uが適用できるTの型を取り出す type B_D = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "b" | "d" type A_C = Filter<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "a" | "c" type StringOrNumber = Diff<string | number | (() => void), Function>; // string | number type FunctionType = Filter<string | number | (() => void), Function>; // () => void
フィルタリングする型(Diff<T, U>
)により、さらに新たな型を定めたのがつぎのコードです。条件に参照される型(T
)から、null
とundefined
を除きます。なお、このフィルタリングする型は、TypeScriptの組み込み済みユーティリティ型Exclude<T,U>
として備わっています。
type NonNullable<T> = Diff<T, null | undefined>; // nullとundefinedを型Tから除く type stringOrStringArray = NonNullable<string | string[] | null | undefined>; // string | string[]
関数の引数を型づけしたのがつぎの例です。第2引数(y
)がnull
とundefined
を許さない型(NonNullable<T>
)で定められています。第2引数は第1引数に代入できても、null
やundefined
を含みうる第1引数の値は制限の強い第2引数が受けつけません。また、ふたつめの関数(func2()
)では、第1引数(x
)の型にstring | undefined
を適用したため、string
で型づけされた変数(s1
)に代入しようとすればエラーになります。
function func1<T>(x: T, y: NonNullable<T>) { x = y; // OK // y = x; // エラー } function func2<T extends string | undefined>(x: T, y: NonNullable<T>) { x = y; // OK // y = x; // エラー // let s1: string = x; // エラー let s2: string = y; // OK }
今度は、つぎのインタフェース(Part
)を例にとります。4つめのプロパティ(updatePart
)の型がFunction
です。
interface Part { id: number; name: string; subparts: Part[]; updatePart(newName: string): void; }
条件に応じた型づけは、マップと組み合わせることで応用の幅が広がります。前掲インタフェース(Part
)から条件つきマップによりプロパティ名を取り出したのが、つぎのふたつの型です。
type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T]; type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T]; type FunctionPorpNames = FunctionPropertyNames<Part>; // "updatePart" type NonFunctionPorpNames = NonFunctionPropertyNames<Part>; // "id" | "name" | "subparts"
これらプロパティ名が収められたふたつの型から、ユーティリティ型Pick<T,K>
でインタフェース(Part
)のプロパティを取り出して型エイリアスとして定めました。
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>; type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>; type FunctionPorps = FunctionProperties<Part>; // { updatePart(newName: string): void } type NonFunctionPorps = NonFunctionProperties<Part>; // { id: number, name: string, subparts: Part[] }
条件に応じた型は、それ自身を再帰的に参照することはできません。 たとえば、つぎのコードはエラーになります(図003)。
type ElementType<T> = T extends any[] ? ElementType<T[number]> : T; // エラー
図003■条件に応じた型を循環参照するとエラーが示される
16-03 条件の型を型推論で決める
条件のextends
には、infer
宣言で推論された型を用いることができます。型変数が推論により決められるということです。たとえば、関数の戻り値を型に定めるユーティリティ型ReturnType<T>
の実装はつぎのように定められています。
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
extends
の条件としたのは、任意の数の引数をもつ関数(つまりすべての関数)です。戻り値の型(R
)が推論できるときはその型、できないときはany
で型づけされます。
type StringType = ReturnType<() => string>; // string type VoidType = ReturnType<(s: string) => void>; // void type AnyType = ReturnType<never>; // any
条件に応じた型づけは、条件演算子のように入れ子にすることができます。つぎの型(Unpacked<T>
)は、Handbook「Advanced Types」の「Type inference in conditional types」に示されたジェネリクスによる入れ子にした宣言の例です。infer
が用いられており、具体的に適用した型エイリアスが5つ(T0
〜T5
)挙げられています。
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
ただ、どの条件から導かれたのかひと目でわかりにくいので、順序を並べ替えてみました。
// (infer U)[] type T1 = Unpacked<string[]>; // string type T4 = Unpacked<Promise<string>[]>; // Promise<string> // (...args: any[]) => infer U type T2 = Unpacked<() => string>; // string // Promise<infer U> type T3 = Unpacked<Promise<string>>; // string // T type T0 = Unpacked<string>; // string
T5
はふたつの条件が組み合わさった結果です。まず(infer U)[]
により、Promise<string>
となります。するとT3
と同じですから、string
に解決されるわけです。はじめの条件で解決が済んでしまうT4
との違いにご注目ください。
// (infer U)[] // Promise<infer U> type T5 = Unpacked<Unpacked<Promise<string>[]>>; // string
つぎの型(InferUnion<T>
)は、条件として複数のプロパティが同じ型変数(U
)でinfer
宣言された例です。このとき、異なるふたつの型は、共用体型に解決されます。
type InferUnion<T> = T extends { a: infer U, b: infer U } ? U : never; type StringType = InferUnion<{ a: string, b: string }>; // string type StringOrNumberType = InferUnion<{ a: string, b: number }>; // string | number
つぎの型(InferIntersection<T>
)は複数のプロパティが関数で、引数の条件に同じ型変数(U
)をinfer
宣言しています。このときの解決は、交差型です。ふたつのプリミティブ型の交差(string & number
)は、つまりnever
型になります。
type InferIntersection<T> = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never; type StringType = InferIntersection<{ a: (x: string) => void, b: (x: string) => void }>; // string type NeverType = InferIntersection<{ a: (x: string) => void, b: (x: number) => void }>; // string & number
共変と反変
Handbookの「Type inference in conditional types」では、同じ型変数で推論されるふたつのプロパティを「共変」(covariant)、プロパティの型に定めた関数の引数は「反変」(contravariant)と表現しています。Wikipediaの「共変性と反変性 (計算機科学)」によれば、ふたつの意味はつぎのとおりです。
- 共変(covariant): 広い型から狭い型へ変換すること。
- 反変(contravariant) : 狭い型から広い型へ変換すること。
さらに詳しく知りたい方には「TypeScript における変性(variance)について」が参考になるでしょう。
複数の型を多重に定めることがあります。たとえば、つぎのような関数のオーバーロード(多重定義))です。この場合、最後の型定義から推論されます。結果として、すべての型を含めることになります。複数の引数の型ごとに、多重に解決することはできません。
declare function func(x: string): number; declare function func(x: number): string; declare function func(x: string | number): string | number; type OverloadedType = ReturnType<typeof func>; // string | number
通常の型定義の左辺に、型パラメータの制約としてinfer
宣言を用いることはできません。
type ReturnType<T extends (...args: any[]) => infer R> = R; // サポートされていないのでエラー
infer
宣言を型パラメータの制約から除き、右辺の条件に移せば多くの場合望んだ型が定められるでしょう。
type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : any;
つぎのようにパラメータの型定義(AnyFunction
)を分けると、より見やすくなります。
type AnyFunction = (...args: any[]) => any; type ReturnType<T extends AnyFunction> = T extends (...args: any[]) => infer R ? R : any;
16-04 組み込み済みのユーティリティ型
TypeScript 2.8には、条件で定めるつぎのようなユーティリティ型が備わりました。
Exclude<T, U>
型T
からU
に適合するすべてのプロパティを除いた型がつくられます。
type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c" type T1 = Exclude<"a" | "b" | "c", "a" | "b">; // "c" type T2 = Exclude<string | number | (() => void), Function>; // string | number
Extract<T,U>
型T
からU
に適合するプロパティだけを取り出した型がつくられます。
type T0 = Extract<"a" | "b" | "c", "a" | "f">; // "a" type T1 = Extract<string | number | (() => void), Function>; // () => void
NonNullable<T>
型T
からnull
とundefined
を除いた型がつくられます。
type T0 = NonNullable<string | number | undefined>; // string | number type T1 = NonNullable<string[] | null | undefined>; // string[]
ReturnType<T>
関数T
の戻り値の型からなる新たな型がつくられます。
declare function f1(): { a: number, b: string } type T0 = ReturnType<() => string>; // string type T1 = ReturnType<(s: string) => void>; // void type T2 = ReturnType<(<T>() => T)>; // {} type T3 = ReturnType<(<T extends U, U extends number[]>() => T)>; // number[] type T4 = ReturnType<typeof f1>; // { a: number, b: string } type T5 = ReturnType<any>; // any type T6 = ReturnType<never>; // any type T7 = ReturnType<string>; // エラー type T8 = ReturnType<Function>; // エラー
InstanceType<T>
コンストラクタ関数の型T
からインスタンスの型が得られます。
class C { x = 0; y = 0; } type T0 = InstanceType<typeof C>; // C type T1 = InstanceType<any>; // any type T2 = InstanceType<never>; // any type T3 = InstanceType<string>; // エラー type T4 = InstanceType<Function>; // エラー type T5 = InstanceType<typeof Date> // Date
作成者: 野中文雄
更新日: 2020年08月02日 最新のドキュメントに合わせて本文全体を改訂および加筆。コード例も大幅に書き替えた。
作成日: 2018年05月01日
Copyright © 2001-2020 Fumio Nonaka. All rights reserved.