LoginSignup
54
43

More than 5 years have passed since last update.

TypeScript Handbook を読む (10. Advanced Types)

Last updated at Posted at 2017-04-01

TypeScript Handbook を読み進めていく第十回目。

  1. Basic Types
  2. Variable Declarations
  3. Interfaces
  4. Classes
  5. Functions
  6. Generics
  7. Enums
  8. Type Inference
  9. Type Compatibility
  10. Advanced Types (今ココ)
  11. Symbols
  12. Iterators and Generators
  13. Modules
  14. Namespaces
  15. Namespaces and Modules
  16. Module Resolution
  17. Declaration Merging
  18. JSX
  19. Decorators
  20. Mixins
  21. Triple-Slash Directives
  22. Type Checking JavaScript Files

Advanced Types

原文

Intersection Types

交差型は Person & Serializable & Loggable のように複数の型をひとつにまとめたもので、これらの型の全メンバを持ちます。
交差型を使用した mixin の例を以下に示します。

TypeScript
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

共用型は交差型と似ていると思うかもしれませんが、まったくの別物です。

例として、numberstring のいずれかの引数を取る関数を考えてみましょう。

TypeScript
/**
 * 文字列を受け取り、左側に "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 としていることです。
そのため、numberstring 以外の引数を渡してもコンパイルエラーになりません。

TypeScript
let indentedString = padLeft("Hello world", true); // コンパイルは通るが、実行時にエラーになる

伝統的なオブジェクト指向プログラミングでは共通の基底クラスを定義することでこれを実現しようとするかもしれませんが、ここでそれをするには若干大げさすぎます。
そこで、padding の型として any の代わりに 共用型 を使用します。
共用型は複数の型のうち、どれかであることを表す型で、それぞれの型を縦棒 (|) で区切って記述します。(例: number | string | boolean)

TypeScript
/**
 * 文字列を受け取り、左側に "padding" を加える。
 * 'padding' が文字列の場合、'padding' を左側に加える。
 * 'padding' が数値の場合、その数だけ空白を左側に加える。
 */
function padLeft(value: string, padding: string | number) {
    // ...
}

let indentedString = padLeft("Hello world", true); // コンパイル時にエラーになる

共用型の値に対しては、すべての型に共通のメンバにしかアクセスすることはできません。

TypeScript
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 で一般的に用いられるのは以下のようにメンバの存在チェックを行う方法です。

TypeScript
let pet = getSmallPet();

// この書き方ではエラーになる
if (pet.swim) {
    pet.swim();
}
else if (pet.fly) {
    pet.fly();
}

このコードを動作させるには、キャストを使用する必要があります。

TypeScript
let pet = getSmallPet();

if ((<Fish>pet).swim) {
    (<Fish>pet).swim();
}
else {
    (<Bird>pet).fly();
}

User-Defined Type Guards

このままでは毎回キャストする必要がありますが、実際には、一度型チェックを行えばその条件分岐の中ではどの型か分かっていることになります。

このように特定のスコープにおける型を保証することを 型ガード と読んでおり、それには以下のような 型述語 を返す関数を定義して使用します。
型述語は parameterName is Type の形式で記述しますが、この時の parameterName は関数の引数名と同じである必要があります。

TypeScript
function isFish(pet: Fish | Bird): pet is Fish {
    return (<Fish>pet).swim !== undefined;
}

isFish を呼び出すと、もしそれが元の型と互換性がある型であれば、TypeScript は自動的にその型として扱ってくれます。

TypeScript
// 'swim'、'fly' の両方にアクセスしても問題ない

if (isFish(pet)) {
    pet.swim();
}
else {
    pet.fly();
}

Else 句の中では自動的に Bird 型と判断してくれるのがすごい

typeof type guards

最初の padLeft の例について、型述語を使用して書き直してみましょう。

TypeScript
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 では一部の型に対して自動的に型ガードを認識してくれます。

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 型ガード ではコンストラクタ関数を使用して型を特定します。

TypeScript
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 では nullundefined という 2 つの型を定義しています。
デフォルトでは nullundefined はどの型に対しても代入できますが、逆に言えば、それらを許容したくない場合でも代入を防げないことを意味しています。

--strictNullChecks を使用することで、 そのままでは nullundefined を代入できなくすることが可能です。
その場合、nullundefined を代入できる変数は明示的に共用型として宣言する必要があります。

TypeScript
let s = "foo";
s = null; // エラー。'null' は 'string' に代入できない

let sn: string | null = "bar";
sn = null; // OK

sn = undefined; // エラー。'undefined' は'string | null' に代入できない

JavaScript と異なり、TypeScript では nullundefined を区別して扱っていることに注意してください。
つまり、string | nullstring | undefinedstring | undefined | null はすべて異なる型になります。

Optional parameters and properties

--strictNullChecks を指定すると自動的に任意の引数に | undefined が追加されます。

TypeScript
function f(x: number, y?: number) {
    return x + (y || 0);
}
f(1, 2);
f(1);
f(1, undefined);
f(1, null); // エラー。'null' は 'number | undefined' には代入できない

任意のプロパティについても同様です。

TypeScript
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 と同じように記述できます。

TypeScript
function f(sn: string | null): string {
    if (sn == null) {
        return "default";
    }
    else {
        return sn;
    }
}

上記のやり方は null を除外しようとしていることが非常に明確ですが、もっと簡潔な演算子を使用することもできます。

TypeScript
function f(sn: string | null): string {
    return sn || "default";
}

コンパイラが nullundefined を取り除くことができない場合、キャストを使用して明示的にそれらを取り除くことができます。
識別子の末尾に ! を付けることで、その識別子の型から nullundefined が取り除かれます。

TypeScript
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

型エイリアスを使って、型に別名を付けることができます。
型エイリアスはインタフェースと似ていますが、プリミティブ型、共用型、タプル、その他の自作クラスに対して適用することができます。

TypeScript
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
    if (typeof n === "string") {
        return n;
    }
    else {
        return n();
    }
}

インタフェースと同様に、エイリアスもジェネリックにすることができます。
そのためにはエイリアス宣言の右側に型引数を追加するだけです。

TypeScript
type Container<T> = { value: T };

プロパティからエイリアス自身を参照することも可能です。

TypeScript
type Tree<T> = {
    value: T;
    left: Tree<T>;
    right: Tree<T>;
}

これらと交差型を組み合わせることでクラクラするような型を作り出すことができます。

TypeScript
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;

ただし、上記の他には宣言の右側にエイリアスを記述することはできません。

TypeScript
type Yikes = Array<Yikes>; // エラー

Interfaces vs. Type Aliases

前述したように、型エイリアスとインタフェースはよく似ていますが、微妙に異なる点があります。

ひとつ目の違いは、インタフェースがどこでも使用できる新しい名前を作り出すのに対し、型エイリアスは新しい名前を作ることはしません。
例えば、エラーメッセージではエイリアス名は表示されませんし、以下のコードでは interfaced をエディタ上でマウスホバーすると Interface を返却する関数と表示されますが、aliased をマウスホバーしてもオブジェクトリテラルを返却する関数としか表示されません。

TypeScript
type Alias = { num: number }
interface Interface {
    num: number;
}
declare function aliased(arg: Alias): Alias;
declare function interfaced(arg: Interface): Interface;

ふたつ目の違いは、エイリアスを拡張したり、実装する (または他の型を拡張/実装してエイリアスを作る) ことができない点です。
そのため、理想的なソフトウェアは 拡張に対して開いている べきであることを考慮すると、なるべく型エイリアスではなく、インタフェースを使用するようにしてください。

一方で、インタフェースを用いて型を表現できず、共用型やタプル型を使用する必要がある場合には型エイリアスを使用するべきです。

String Literal Types

リテラル文字列型を使用し、文字列の取りうる値を限定することが可能です。
さらに共用型、型ガード、型エイリアスと組み合わせることで、文字列を enum のように使用することができます。

TypeScript
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"'

リテラル文字列型はオーバーロードにも使用することができます。

TypeScript
function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// ... 他のオーバーロード ...
function createElement(tagName: string): Element {
    // ... 処理をここに書く ...
}

オーバーロードせずに直接 createElement(tagName: "img" | "input") としても良いような気がするけど、オーバーロードの特徴である「引数に応じて戻り値の型を変える」ことができるのがこのやり方のメリットかな

Discriminated Unions

リテラル文字列型、共用型、型ガード、型エイリアスを組み合わせることで、判別共用型 (または タグ付き共用型、代数的データ型) と呼ばれるパターンを構築することができます。
TypeScript での判別共用型は以下の 3 つの要素から成り立ちます。

  1. リテラル文字列型の共通プロパティを持つ型 (判別子)
  2. 複数の判別子からなる型エイリアス (共用型)
  3. 共通プロパティに対する型ガード

まず、後で結合させるインタフェースを定義します。
各インタフェースでは 判別子 (または タグ) となるリテラル文字列型の kind プロパティを個別に定義します。
また、それ以外のプロパティも各インタフェースに定義します。
この時、各インタフェースには何の関連もないことを覚えておいてください。

「判別子」はインタフェース全体のことを指すのかと思ったら、各インタフェースで共通で持ってるプロパティのことも「判別子」と呼んだりしていてイマイチ意味が不明な説明だけど、あまり深く考えずに流す方向で…

TypeScript
interface Square {
    kind: "square";
    size: number;
}
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
interface Circle {
    kind: "circle";
    radius: number;
}

続いてこれらのインタフェースを結合しましょう。

TypeScript
type Shape = Square | Rectangle | Circle;

この判別共用型の使用方法は以下の通りです。

TypeScript
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

もしも判別共用型のすべての値を網羅していなかったらコンパイラに警告してほしいと思うことでしょう。
例えば、ShapeTriangle を追加した時に area の修正を忘れた場合です。

TypeScript
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) と矛盾するため、エラーとなります。

TypeScript
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 がその型となるため、コンパイルエラーとなります。

TypeScript
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 を返却しています。

TypeScript
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 型を使用しているため、このクラスを拡張した新しいクラスでもそのまま元のメソッドを使用できます。

TypeScript
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 では以下のようになります。

JavaScript
function pluck(o, names) {
    return names.map(n => o[n]);
}

TypeScript では以下のように インデックス型クエリ添字アクセス演算子 を使用します。

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 プロパティの名前の集合を表します。

TypeScript
let personProps: keyof Person; // 'name' | 'age'

keyof Person'name' | 'age' と宣言するのと同じですが、新たなプロパティを Person に追加した場合に keyof Person は自動的に更新される点が異なります。
また、それ以外に keyofpluck のようにジェネリッククラス/関数と組み合わせて使用することも可能です。
つまり、pluck に正しいプロパティ名を渡しているかコンパイラがチェックしてくれるということです。

TeyScript
pluck(person, ['age', 'unknown']); // エラー。'unknown' は 'name' | 'age' に含まれない

ふたつ目の演算子は T[K] です。
これは 添字アクセス演算子 と呼ばれるもので、その式の表す型を表します。
つまり、person['name']Person['name'] 型 (上記の例では string) ということになります。
また、インデックス型クエリと同様に T[K] はジェネリッククラス/関数で使用することが可能です。
ただし、型変数として K extends keyof T を宣言し忘れないようにしてください。

以下の例では o: Tname: K であることから、o[name]T[K] 型になります。

TypeScript
function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
    return o[name]; // o[name] は T[K] 型
}

コンパイラは T[K] を実際の型に解釈してくれるため、getProperty は指定されたプロパティに応じて様々な型を返却できます。

TypeScript
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

keyofT[K] は文字列型インデックスシグネチャの影響を受けます。
文字列型インデックスシグネチャを宣言した場合、keyof Tstring になり、T[string] はそのインデックスシグネチャの型になります。

TypeScript
interface Map<T> {
    [key: string]: T;
}
let keys: keyof Map<number>; // string
let value: Map<number>['foo']; // number

Mapped types

既存の型に対し、各プロパティを任意にしたり、

TypeScript
interface PersonPartial {
    name?: string;
    age?: number;
}

読み取り専用にしたいということはよくあるでしょう。

TypeScript
interface PersonReadonly {
    readonly name: string;
    readonly age: number;
}

TypeScript ではこのような古い型に基づく新しい型 (マップ型) を作成する機能を提供しています。
マップ型では古い型の各プロパティに対し、同じ変換を行うことで新しい型のプロパティを作成します。

例として、すべてのプロパティを readonly または任意にする例を見てみましょう。

TypeScript
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] の部分

これらの使い方は以下の通りです。

TypeScript
type PersonPartial = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;

また、一番簡単なマップ型の例を見てみましょう。

TypeScript
type Keys = 'option1' | 'option2';
type Flags = { [K in Keys]: boolean };

マップ型の構文は for ... in の構文と似ており、3 つのパートから成ります。

  1. 型変数 K: 各プロパティを表す
  2. リテラル文字列共用体 Keys: プロパティ名の集合
  3. そのプロパティの型

上記の例では Keys はハードコーディングされたプロパティ名のリストで、プロパティの型は常に boolean でした。
つまり、このマップ型は以下のようになります。

TypScript
type Flags = {
    option1: boolean;
    option2: boolean;
}

実際のアプリケーションでは最初の ReadonlyPartial のように既存の型に対して変換を行うでしょう。
そのため、keyof と添字アクセス演算子を使って以下のように記述します。

TypeScript
type NullablePerson = { [P in keyof Person]: Person[P] | null }
type PartialPerson = { [P in keyof Person]?: Person[P] }

これのジェネリック版はもっと役に立つことでしょう。

TypScript
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> でラップした例を見てみましょう。

TypeScript
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> は非常に便利なため、PickRecord と一緒に TypeScript の標準ライブラリに含まれています。

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 という名前なんだろう)

ReadonlyPartialPick は準同型であるのに対し、Record はコピー元となる入力クラスを受け取らないため、準同型ではありません。

TypeScript
type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>

非準同型な型は新しいプロパティを作るため、修飾子 (readonly 等) はコピーされません。

Inference from mapped types

ラップしたプロパティの取り出しは非常に簡単です。

TypeScript
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 つの型のうち、いずれかが選択されます。

TypeScript
T extends U ? X : Y

この場合の型は TU に代入可能であれば X、そうでなければ Y になります。

条件付き型 T extends U ? X : YX または Y解決される か、評価が 遅延 されるかは、条件が型変数に依存するかどうかによります。
TU に型変数が含まれている場合、型システムが T は常に U に代入可能と判断できるだけの情報を持っているかどうかに応じて、X または Y に解決されるか、評価が遅延されるかが決まります。

型がすぐに解決される例を見てみましょう。

TypeScript
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 型エイリアスを使用していますが、これはネストした条件付き型です。

TypeScript
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"

逆に、評価が遅延される例を見てみましょう。

TypeScript
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 型において、TA | B | C としてインスタンス化する場合、(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y) として解決されます。

TypeScript
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 に代入可能かどうか) を受けることになります。

TypeScript
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> の条件が真となった場合に Tany[] という追加の制約を受けるため、配列の要素の型を T[number] として参照することができます。
また、最後の例で条件付き型がどのように共用型へ分配されるかについても確認してください。

何も考えずに T extends any[] ? BoxedArray<T> : BoxedValue<T> としてしまうと BoxedArray<number[]> になってしまうというわけだ。
これを理解して活用するのはかなり無理ゲーな気が...。

条件付き型の分配プロパティは filter 共用型として使用するのに便利です。

TypeScript
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
}

条件付き型はマップ型と組み合わせると特に有用です。

TypeScript
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[] }

共用型や交差型と同様に、条件付き型も自分自身への参照は許可されていません。
そのため、以下の例はエラーとなります。

TypeScript
type ElementType<T> = T extends any[] ? ElementType<T[number]> : T;  // エラー

Type inference in conditional types

条件付き型の extends 句において、推測すべき型変数を指定する infer 宣言が使用できるようになりました。
推測された型は条件付き型の真側の分岐で使用できます。
また、同じ型変数で複数の infer を使用することも可能です。

TypeScript
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

(...args: any[]) => RR を型として使用する、という意味ね

条件付き型はネストすることができます。

TypeScript
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

共変の位置にある同じ型変数において、複数の候補がある場合に共用型が推測される例を以下に示します。

TypeScript
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

同様に、反変の位置にある同じ型変数において、複数の候補がある場合に交差型が推測される例を以下に示します。

TypeScript
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

複数の呼び出しシグネチャ (関数オーバーロードなど) から型を推測する場合、(おそらく一番許容的である) 最後の 呼び出しシグネチャを基に型が推測されます。
そのため、引数の型からオーバーロードを解決することはできません。

TypeScript
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 宣言を使用することはできません。

TypeScript
type ReturnType<T extends (...args: any[]) => infer R> = R;  // エラー、未対応

ただし、制約内の型変数を消去し、代わりに条件付き型を指定することで、ほぼ同等の効果を得ることができます。

TypeScript
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 から nullundefined を取り除く。
  • ReturnType<T> – 関数型の戻り値の型を取得する。
  • InstanceType<T> – コンストラクタ関数型からインスタンスの型を取得する。
TypeScript
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>> とすることで簡単に実装できるため、この中に含めていません。

54
43
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
54
43