LoginSignup
9
2

More than 5 years have passed since last update.

TypeScript公式Handbook「Advanced Types」の動かないコードを動かす

Last updated at Posted at 2018-05-01

TypeScript公式Handbook「Advanced Types」(以下「公式解説」)にもとづいて、ノート「TypeScript: 高度な型」という解説を書きました。TypeScriptに備わる、より複雑な型を表す機能についての説明です。邦訳ではなく、同じ内容をできるだけわかりやすく書き改めました。

そして、とくにこの章は、動かないコード例が少なくありません。ドキュメントの誤りでなく、こんな感じというあらすじを示すだけの「なんちゃってコード」。つまり、動かす気がないサンプルなのです。

linked_list_no_work.png

前出の解説を書くにあたって、サンプルはすべて実際に動かして確かめられるコードに改めました。本稿では、公式解説から、動かない・動かす気のないコード例を採り上げて、動くコードをご紹介します。詳しい構文などについては、ノートの解説をお読みください。

交差型(Intersection Type)

交差型(Intersection Type)は、複数の型をひとつに合成します。複数の型を結ぶのは記号&です。つぎの関数は公式解説の例で、これには問題ありません。

function extend<T, U>(first: T, second: U): T & U {
    const 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 ConsoleLogger implements Loggable {
    log() {
        // ...
    }
}
var jim = extend(new Person("Jim"), new ConsoleLogger());
var n = jim.name;
jim.log();

そこで、つぎのような座標に関わるふたつのクラスを定めました。前掲関数は、ふたつのインスタンスから、mixinで合成したオブジェクトを返します。このオブジェクトは、どちらのクラスのメソッドも呼び出せるのです。

interface IPoint {
    x: number;
    y: number;
}
class Point implements IPoint {
    constructor(public x: number = 0, public y: number = 0) {}
    getLength(): number {
        return Math.sqrt(this.x ** 2 + this.y ** 2);
    }
}
class Polar implements IPoint {
    x: number;
    y: number;
    constructor(length: number = 1, angle: number = 0) {
        this.x = length * Math.cos(angle);
        this.y = length * Math.sin(angle);
    }
    getAngle(): number {
        return Math.atan2(this.y, this.x);
    }
}
const vector = extend(new Point(1, Math.sqrt(3)), new Polar());
console.log(vector.getLength(), vector.getAngle() * 180 / Math.PI);
// 1.9999999999999998 59.99999999999999

なお、このコードはtsconfig.jsonのtargetをes6にすると、正しく動きません。クラスに定めたメソッドが、for...in文で取り出せないからです。

{
    "compilerOptions": {
        "target": "es6",

    }
}

型エイリアス(Type Alias)

型エイリアス(Type Alias)は、型に新たな名前を与える機能です。公式解説のコード例はつぎのとおりで、動かそうという気概が清々しいほど感じられません。

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 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 ? 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

文字列リテラル型(String Literal Type)

文字列リテラル型(String Literal Type)は、文字列型のとるべき値を直に定めた型です。公式解説のつぎのコード例は、いわんとすることはわかります。でも、できればコードを動かして確かめたいところです。

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 {
            // error! should not pass null or undefined.
        }
    }
}
let button = new UIElement();
button.animate(0, 0, "ease-in");
button.animate(0, 0, "uneasy"); // error: "uneasy" is not allowed here

定数として使えるということが示したかったのでしょうから、簡単に確かめられるキーイベントで動かしてみました。コンストラクタに矢印キーのイベント定数を渡してつくったインスタンスは、そのキーが押されると定数値とキーコードをコンソールに出力します。

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;
// ... more overloads ...
function createElement(tagName: string): Element {
    // ... code goes here ...
}

でも、動かしてみるために書き加えるのはわずか数行です。引数の定数が限定され、その値によって戻り値の型も変わることは、確かめてみる価値はあると思います。

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'));  // コンパイルエラー

型のマップ(Mapped type)

型のマップ(Mapped type)というのは、ある型にもとづいて新たな型をつくることです。公式解説のつぎのコードを書いてみると、コンパイルエラーになります。

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
}
type Partial<T> = {
    [P in keyof T]?: T[P];
}

エラーメッセージは、識別子が重複していると告げます。

識別子 'Readonly' が重複しています。
識別子 'Partial' が重複しています。

理由は、どちらもTypeScript 2.1から標準ライブラリに備わるキーワードだからです(「Partial, Readonly, Record, and Pick」)。つまり、うえのコードはサンプルではありません。実装を示したものということなのです。つぎの公式解説のコード例は、PartialReadonlyの型定義などせずに動きます。

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

公式解説がさらに、型をマップする例として掲げるのはつぎのコードです。関数が引数に受け取ったオブジェクトのプロパティを、別に定めた型でラップして返します。ラップというのが具体的に何をするのかわかりません。さらに、ラップされたオブジェクトの扱いも書かれていないので、これも不親切なコードです。

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> {
    // ... wrap proxies ...
}
let proxyProps = proxify(props);

動かすためには、関数をつぎのように実装すればよいでしょう。

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

この関数は、たとえばつぎのコードで試せます。

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

ノート「TypeScript: 高度な型」では、ほかにも多くのコードを書き替え、解説の仕方や順序も改めました。本稿で使われている構文やコードの中身については、このノートをお読みください。

9
2
2

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
9
2