Archive - TypeScript

TypeScript 0.9のジェネリクス対応でlinq.jsの型定義作って苦労した話

久しぶりのTypeScriptAnnouncing TypeScript 0.9というわけで、ついに待望のジェネリクスが搭載されました。やったね!というわけで、ジェネリクス対応のlinq.jsの型定義を早速作りました、と。ver 3.0.4-Beta5です。まだまだBeta、すみませんすみません、色々忙しくて……。NuGetからもしくはサイトからのダウンロードで公開してます。

とりあえず例として使ってみた感じ。

// booleanしか受け入れないwhereにnumberを突っ込んだらちゃんと怒ってくれるよ!
// Call signatures of types '(x: any) => number' and '(element: number, index: number) => boolean' are incompatible.
var seq = Enumerable.from([1, 10, 100, 100]).where(x => x * 2);

やったー。これだよこれ!LINQはジェネリクスがあって本当の本当の真価を発揮するんです!!!

面白げな型定義

C#とちょっと違うところとしては、ジェネリクスの型変数のところにもオブジェクトを突っ込めるんですね。だから

// from(obj)の型定義はこんな感じ。
from(obj: any): IEnumerable<{ key: string; value: any }>;
 
// ちなみにfromのオーバーロードはいっぱいある
// linq.js自体はJScriptのIEnumerableとWinMDのIIterable<T>にも対応してるのですがTSで表現できないので、そこは未定義……。。。
from(): IEnumerable<any>; // empty
from<T>(obj: IEnumerable<T>): IEnumerable<T>;
from(obj: number): IEnumerable<number>;
from(obj: boolean): IEnumerable<boolean>;
from(obj: string): IEnumerable<string>;
from<T>(obj: T[]): IEnumerable<T>;
from<T>(obj: { length: number;[x: number]: T; }): IEnumerable<T>;

このkey,valueはどこから出てきたんだよって感じでわかりづらかったので、こうして見えてくれると嬉しい度高い。

型消去

場合によってはIEnumerable<any>となるため、Tの型をつけてあげたかったり、もしくは強引に変換したかったり(例えば↑のIIterable<T>は{key,value}に解釈されてしまうので、明示的に変換してあげる必要がある)する場合のために、castメソッドを用意しました。

// any[]型で渡ってきたりする場合に
var seq: any[] = [1, 2, 3, 4, 5];
 
var result = Enumerable.from(seq)
    .cast<number>() // numberに変換
    .select(x => x * x) // x:number
    .toArray(); // result:number[]

さて、ところで、TypeScriptのジェネリクスは型消去(Type Erasure)式です。コンパイル結果のJSは以下のような感じで

var result = Enumerable.from(seq)
    .cast()
    .select(function (x) { return x * x; })
    .toArray();

一切の型は消えています。なので、TypeScriptでTの型を取って実行時に扱ったりはできません。型が欲しければ型を渡せ方式。

linq.jsにはofTypeという、型でフィルタリングするメソッドがあるのですが

// 混在した配列から、型でフィルタして取り出す
var mixed: any[] = [1, "hoge", 100, true, "nano"];
 
// このままじゃ型が分からないのでresultはany[]
var result1 = Enumerable.from(mixed)
    .ofType(Number) // 数値型のみでフィルタ、つまり[1, 100]
    .toArray();
 
// ofTypeの後にcastするか、もしくはofTypeで型指定するか
var result2 = Enumerable.from(mixed)
    .ofType<number>(Number)
    .toArray();

というように、<number>(Number)と連続するのが非常に不恰好……。まあ、この辺はそういうものなのでしょうがないと諦めましょう。

地雷ふんだり

さて、そんな素敵なTypeScript 0.9なのですが、残念なお知らせ。現在の、というか、この0.9ですが、完成度はものすごーーーーーーーーく、低いです。はい、超低いです。Visual Studioと組み合わせて使う場合、半端無く動作も補完も遅いです。正直、ベータどころかアルファぐらいのクオリティで、何故に堂々と出してきたのか理解に苦しむ。遅いだけならまだしも、ジェネリクスの解釈が非常に怪しく、地雷を踏むと補完やエラー通知が消え去ります。いや、消え去るだけならまだよくて、Visual Studioと裏で動くTypeScript Compilerが大暴走をはじめてCPUが100%に張り付いたりします。もうヤヴァい。タスクマネージャー開きっぱにして警戒しながらじゃないと書けないです。linq.jsの型定義書くの超絶苦労した……。

interface IEnumerable<T> {
    // 戻り値の型をIEnumerable<IGrouping<TKey, T>>にしたいのですが、
    // interface側で定義しているTをネストした型変数に使うとVSが大暴走して死ぬ
    // ので、今回のバージョンではanyにしてます
    groupBy<TKey>(keySelector: (element: T) => TKey): IEnumerable<IGrouping<TKey, any>>;
}

具体的に踏んだのは↑ですねえ。最初原因が分かってなくてかなり時間食われちゃいました。んもー。最小セットで試してる限りでは、コンパイルエラーにはなるんですが(A generic type may not reference itself with a wrapped form of its own type parameters.)暴走はしない、んですがlinq.d.tsで弄ってると死ぬ。おうふ。もう定義の仕方が悪いのかコンパイラがアレなのか判断つかないのでしんどい。

そんなわけで、0.8の完成度からだいぶ退化して、実用性はゼロになってしまいました。私の環境だけ、じゃあないよねえ…‥?とりあえず0.9.1を待ちましょう。どうやらコンパイラをまるっと書き換えたそうですしねー、初物だからshoganaiと思うことにして。しかし泣きたい。

linq.d.ts

とりあえず、こんな感じになってます。

// Type Definition for linq.js, ver 3.0.4-Beta5
 
declare module linqjs {
    interface IEnumerator<T> {
        current(): T;
        moveNext(): boolean;
        dispose(): void;
    }
 
    interface Enumerable {
        Utils: {
            createLambda(expression: any): (...params: any[]) => any;
            createEnumerable<T>(getEnumerator: () => IEnumerator<T>): IEnumerable<T>;
            createEnumerator<T>(initialize: () => void , tryGetNext: () => boolean, dispose: () => void ): IEnumerator<T>;
            extendTo(type: any): void;
        };
        choice<T>(...params: T[]): IEnumerable<T>;
        cycle<T>(...params: T[]): IEnumerable<T>;
        empty<T>(): IEnumerable<T>;
        // from<T>, obj as JScript's IEnumerable or WinMD IIterable<T> is IEnumerable<T> but it can't define.
        from(): IEnumerable<any>; // empty
        from<T>(obj: IEnumerable<T>): IEnumerable<T>;
        from(obj: number): IEnumerable<number>;
        from(obj: boolean): IEnumerable<boolean>;
        from(obj: string): IEnumerable<string>;
        from<T>(obj: T[]): IEnumerable<T>;
        from<T>(obj: { length: number;[x: number]: T; }): IEnumerable<T>;
        from(obj: any): IEnumerable<{ key: string; value: any }>;
        make<T>(element: T): IEnumerable<T>;
        matches<T>(input: string, pattern: RegExp): IEnumerable<T>;
        matches<T>(input: string, pattern: string, flags?: string): IEnumerable<T>;
        range(start: number, count: number, step?: number): IEnumerable<number>;
        rangeDown(start: number, count: number, step?: number): IEnumerable<number>;
        rangeTo(start: number, to: number, step?: number): IEnumerable<number>;
        repeat<T>(element: T, count?: number): IEnumerable<T>;
        repeatWithFinalize<T>(initializer: () => T, finalizer: (element) => void ): IEnumerable<T>;
        generate<T>(func: () => T, count?: number): IEnumerable<T>;
        toInfinity(start?: number, step?: number): IEnumerable<number>;
        toNegativeInfinity(start?: number, step?: number): IEnumerable<number>;
        unfold<T>(seed: T, func: (value: T) => T): IEnumerable<T>;
        defer<T>(enumerableFactory: () => IEnumerable<T>): IEnumerable<T>;
    }
 
    interface IEnumerable<T> {
        constructor(getEnumerator: () => IEnumerator<T>);
        getEnumerator(): IEnumerator<T>;
 
        // Extension Methods
        traverseBreadthFirst(func: (element: T) => IEnumerable<T>): IEnumerable<T>;
        traverseBreadthFirst<TResult>(func: (element: T) => IEnumerable<T>, resultSelector: (element: T, nestLevel: number) => TResult): IEnumerable<TResult>;
        traverseDepthFirst<TResult>(func: (element: T) => Enumerable): IEnumerable<T>;
        traverseDepthFirst<TResult>(func: (element: T) => Enumerable, resultSelector?: (element: T, nestLevel: number) => TResult): IEnumerable<TResult>;
        flatten(): IEnumerable<any>;
        pairwise<TResult>(selector: (prev: T, current: T) => TResult): IEnumerable<TResult>;
        scan(func: (prev: T, current: T) => T): IEnumerable<T>;
        scan<TAccumulate>(seed: TAccumulate, func: (prev: TAccumulate, current: T) => TAccumulate): IEnumerable<TAccumulate>;
        select<TResult>(selector: (element: T, index: number) => TResult): IEnumerable<TResult>;
        selectMany<TOther>(collectionSelector: (element: T, index: number) => IEnumerable<TOther>): IEnumerable<TOther>;
        selectMany<TCollection, TResult>(collectionSelector: (element: T, index: number) => IEnumerable<TCollection>, resultSelector: (outer: T, inner: TCollection) => TResult): IEnumerable<TResult>;
        selectMany<TOther>(collectionSelector: (element: T, index: number) => TOther[]): IEnumerable<TOther>;
        selectMany<TCollection, TResult>(collectionSelector: (element: T, index: number) => TCollection[], resultSelector: (outer: T, inner: TCollection) => TResult): IEnumerable<TResult>;
        selectMany<TOther>(collectionSelector: (element: T, index: number) => { length: number;[x: number]: TOther; }): IEnumerable<TOther>;
        selectMany<TCollection, TResult>(collectionSelector: (element: T, index: number) => { length: number;[x: number]: TCollection; }, resultSelector: (outer: T, inner: TCollection) => TResult): IEnumerable<TResult>;
        where(predicate: (element: T, index: number) => boolean): IEnumerable<T>;
        choose(selector: (element: T, index: number) => T): IEnumerable<T>;
        ofType<TResult>(type: any): IEnumerable<TResult>;
        zip<TResult>(second: IEnumerable<T>, resultSelector: (first: T, second: T, index: number) => TResult): IEnumerable<TResult>;
        zip<TResult>(second: { length: number;[x: number]: T; }, resultSelector: (first: T, second: T, index: number) => TResult): IEnumerable<TResult>;
        zip<TResult>(second: T[], resultSelector: (first: T, second: T, index: number) => TResult): IEnumerable<TResult>;
        zip<TResult>(...params: any[]): IEnumerable<TResult>; // last one is selector
        merge<TResult>(...params: IEnumerable<T>[]): IEnumerable<T>;
        merge<TResult>(...params: { length: number;[x: number]: T; }[]): IEnumerable<T>;
        merge<TResult>(...params: T[][]): IEnumerable<T>;
        join<TInner, TKey, TResult>(inner: IEnumerable<TInner>, outerKeySelector: (outer: T) => TKey, innerKeySelector: (inner: TInner) => TKey, resultSelector: (outer: T, inner: TKey) => TResult, compareSelector?: (obj: T) => TKey): IEnumerable<TResult>;
        join<TInner, TKey, TResult>(inner: { length: number;[x: number]: TInner; }, outerKeySelector: (outer: T) => TKey, innerKeySelector: (inner: TInner) => TKey, resultSelector: (outer: T, inner: TKey) => TResult, compareSelector?: (obj: T) => TKey): IEnumerable<TResult>;
        join<TInner, TKey, TResult>(inner: TInner[], outerKeySelector: (outer: T) => TKey, innerKeySelector: (inner: TInner) => TKey, resultSelector: (outer: T, inner: TKey) => TResult, compareSelector?: (obj: T) => TKey): IEnumerable<TResult>;
        groupJoin<TInner, TKey, TResult>(inner: IEnumerable<TInner>, outerKeySelector: (outer: T) => TKey, innerKeySelector: (inner: TInner) => TKey, resultSelector: (outer: T, inner: TKey) => TResult, compareSelector?: (obj: T) => TKey): IEnumerable<TResult>;
        groupJoin<TInner, TKey, TResult>(inner: { length: number;[x: number]: TInner; }, outerKeySelector: (outer: T) => TKey, innerKeySelector: (inner: TInner) => TKey, resultSelector: (outer: T, inner: TKey) => TResult, compareSelector?: (obj: T) => TKey): IEnumerable<TResult>;
        groupJoin<TInner, TKey, TResult>(inner: TInner[], outerKeySelector: (outer: T) => TKey, innerKeySelector: (inner: TInner) => TKey, resultSelector: (outer: T, inner: TKey) => TResult, compareSelector?: (obj: T) => TKey): IEnumerable<TResult>;
        all(predicate: (element: T) => boolean): boolean;
        any(predicate?: (element: T) => boolean): boolean;
        isEmpty(): boolean;
        concat(...sequences: IEnumerable<T>[]): IEnumerable<T>;
        concat(...sequences: { length: number;[x: number]: T; }[]): IEnumerable<T>;
        concat(...sequences: T[]): IEnumerable<T>;
        insert(index: number, second: IEnumerable<T>): IEnumerable<T>;
        insert(index: number, second: { length: number;[x: number]: T; }): IEnumerable<T>;
        alternate(alternateValue: T): IEnumerable<T>;
        alternate(alternateSequence: { length: number;[x: number]: T; }): IEnumerable<T>;
        alternate(alternateSequence: IEnumerable<T>): IEnumerable<T>;
        alternate(alternateSequence: T[]): IEnumerable<T>;
        contains(value: T): boolean;
        contains<TCompare>(value: T, compareSelector?: (element: T) => TCompare): boolean;
        defaultIfEmpty(defaultValue?: T): IEnumerable<T>;
        distinct(): IEnumerable<T>;
        distinct<TCompare>(compareSelector: (element: T) => TCompare): IEnumerable<T>;
        distinctUntilChanged(): IEnumerable<T>;
        distinctUntilChanged<TCompare>(compareSelector: (element: T) => TCompare): IEnumerable<T>;
        except(second: { length: number;[x: number]: T; }): IEnumerable<T>;
        except<TCompare>(second: { length: number;[x: number]: T; }, compareSelector: (element: T) => TCompare): IEnumerable<T>;
        except(second: IEnumerable<T>): IEnumerable<T>;
        except<TCompare>(second: IEnumerable<T>, compareSelector: (element: T) => TCompare): IEnumerable<T>;
        except(second: T[]): IEnumerable<T>;
        except<TCompare>(second: T[], compareSelector: (element: T) => TCompare): IEnumerable<T>;
        intersect(second: { length: number;[x: number]: T; }): IEnumerable<T>;
        intersect<TCompare>(second: { length: number;[x: number]: T; }, compareSelector: (element: T) => TCompare): IEnumerable<T>;
        intersect(second: IEnumerable<T>): IEnumerable<T>;
        intersect<TCompare>(second: IEnumerable<T>, compareSelector: (element: T) => TCompare): IEnumerable<T>;
        intersect(second: T[]): IEnumerable<T>;
        intersect<TCompare>(second: T[], compareSelector: (element: T) => TCompare): IEnumerable<T>;
        union(second: { length: number;[x: number]: T; }): IEnumerable<T>;
        union<TCompare>(second: { length: number;[x: number]: T; }, compareSelector: (element: T) => TCompare): IEnumerable<T>;
        union(second: IEnumerable<T>): IEnumerable<T>;
        union<TCompare>(second: IEnumerable<T>, compareSelector: (element: T) => TCompare): IEnumerable<T>;
        union(second: T[]): IEnumerable<T>;
        union<TCompare>(second: T[], compareSelector: (element: T) => TCompare): IEnumerable<T>;
        sequenceEqual(second: { length: number;[x: number]: T; }): boolean;
        sequenceEqual<TCompare>(second: { length: number;[x: number]: T; }, compareSelector: (element: T) => TCompare): boolean;
        sequenceEqual(second: IEnumerable<T>): boolean;
        sequenceEqual<TCompare>(second: IEnumerable<T>, compareSelector: (element: T) => TCompare): boolean;
        sequenceEqual(second: T[]): boolean;
        sequenceEqual<TCompare>(second: T[], compareSelector: (element: T) => TCompare): boolean;
        orderBy<TKey>(keySelector: (element: T) => TKey): IOrderedEnumerable<T>;
        orderByDescending<TKey>(keySelector: (element: T) => TKey): IOrderedEnumerable<T>;
        reverse(): IEnumerable<T>;
        shuffle(): IEnumerable<T>;
        weightedSample(weightSelector: (element: T) => number): IEnumerable<T>;
        // truly, return type is IEnumerable<IGrouping<TKey, T>> but Visual Studio + TypeScript Compiler can't compile.
        groupBy<TKey>(keySelector: (element: T) => TKey): IEnumerable<IGrouping<TKey, T>>;
        groupBy<TKey, TElement>(keySelector: (element: T) => TKey, elementSelector: (element: T) => TElement): IEnumerable<IGrouping<TKey, TElement>>;
        groupBy<TKey, TElement, TResult>(keySelector: (element: T) => TKey, elementSelector: (element: T) => TElement, resultSelector: (key: TKey, element: IEnumerable<TElement>) => TResult): IEnumerable<TResult>;
        groupBy<TKey, TElement, TResult, TCompare>(keySelector: (element: T) => TKey, elementSelector: (element: T) => TElement, resultSelector: (key: TKey, element: IEnumerable<TElement>) => TResult, compareSelector: (element: T) => TCompare): IEnumerable<TResult>;
        // :IEnumerable<IGrouping<TKey, T>>
        partitionBy<TKey>(keySelector: (element: T) => TKey): IEnumerable<IGrouping<TKey, any>>;
        // :IEnumerable<IGrouping<TKey, TElement>>
        partitionBy<TKey, TElement>(keySelector: (element: T) => TKey, elementSelector: (element: T) => TElement): IEnumerable<IGrouping<TKey, TElement>>;
        partitionBy<TKey, TElement, TResult>(keySelector: (element: T) => TKey, elementSelector: (element: T) => TElement, resultSelector: (key: TKey, element: IEnumerable<TElement>) => TResult): IEnumerable<TResult>;
        partitionBy<TKey, TElement, TResult, TCompare>(keySelector: (element: T) => TKey, elementSelector: (element: T) => TElement, resultSelector: (key: TKey, element: IEnumerable<TElement>) => TResult, compareSelector: (element: T) => TCompare): IEnumerable<TResult>;
        buffer(count: number): IEnumerable<T>;
        aggregate(func: (prev: T, current: T) => T): T;
        aggregate<TAccumulate>(seed: TAccumulate, func: (prev: TAccumulate, current: T) => TAccumulate): TAccumulate;
        aggregate<TAccumulate, TResult>(seed: TAccumulate, func: (prev: TAccumulate, current: T) => TAccumulate, resultSelector: (last: TAccumulate) => TResult): TResult;
        average(selector?: (element: T) => number): number;
        count(predicate?: (element: T, index: number) => boolean): number;
        max(selector?: (element: T) => number): number;
        min(selector?: (element: T) => number): number;
        maxBy<TKey>(keySelector: (element: T) => TKey): T;
        minBy<TKey>(keySelector: (element: T) => TKey): T;
        sum(selector?: (element: T) => number): number;
        elementAt(index: number): T;
        elementAtOrDefault(index: number, defaultValue?: T): T;
        first(predicate?: (element: T, index: number) => boolean): T;
        firstOrDefault(predicate?: (element: T, index: number) => boolean, defaultValue?: T): T;
        last(predicate?: (element: T, index: number) => boolean): T;
        lastOrDefault(predicate?: (element: T, index: number) => boolean, defaultValue?: T): T;
        single(predicate?: (element: T, index: number) => boolean): T;
        singleOrDefault(predicate?: (element: T, index: number) => boolean, defaultValue?: T): T;
        skip(count: number): IEnumerable<T>;
        skipWhile(predicate: (element: T, index: number) => boolean): IEnumerable<T>;
        take(count: number): IEnumerable<T>;
        takeWhile(predicate: (element: T, index: number) => boolean): IEnumerable<T>;
        takeExceptLast(count?: number): IEnumerable<T>;
        takeFromLast(count: number): IEnumerable<T>;
        indexOf(item: T): number;
        indexOf(predicate: (element: T, index: number) => boolean): number;
        lastIndexOf(item: T): number;
        lastIndexOf(predicate: (element: T, index: number) => boolean): number;
        asEnumerable(): IEnumerable<T>;
        cast<TResult>(): IEnumerable<TResult>;
        toArray(): T[];
        // truly, return type is ILookup<TKey, T> but Visual Studio + TypeScript Compiler can't compile. 
        toLookup<TKey>(keySelector: (element: T) => TKey): ILookup<TKey, any>;
        toLookup<TKey, TElement>(keySelector: (element: T) => TKey, elementSelector: (element: T) => TElement): ILookup<TKey, TElement>;
        toLookup<TKey, TElement, TCompare>(keySelector: (element: T) => TKey, elementSelector: (element: T) => TElement, compareSelector: (key: TKey) => TCompare): ILookup<TKey, TElement>;
        toObject(keySelector: (element: T) => any, elementSelector?: (element: T) => any): Object;
        // :IDictionary<TKey, T>
        toDictionary<TKey>(keySelector: (element: T) => TKey): IDictionary<TKey, any>;
        toDictionary<TKey, TValue>(keySelector: (element: T) => TKey, elementSelector: (element: T) => TValue): IDictionary<TKey, TValue>;
        toDictionary<TKey, TValue, TCompare>(keySelector: (element: T) => TKey, elementSelector: (element: T) => TValue, compareSelector: (key: TKey) => TCompare): IDictionary<TKey, TValue>;
        toJSONString(replacer: (key: string, value: any) => any): string;
        toJSONString(replacer: any[]): string;
        toJSONString(replacer: (key: string, value: any) => any, space: any): string;
        toJSONString(replacer: any[], space: any): string;
        toJoinedString(separator?: string): string;
        toJoinedString<TResult>(separator: string, selector: (element: T, index: number) => TResult): string;
        doAction(action: (element: T, index: number) => void ): IEnumerable<T>;
        doAction(action: (element: T, index: number) => boolean): IEnumerable<T>;
        forEach(action: (element: T, index: number) => void ): void;
        forEach(action: (element: T, index: number) => boolean): void;
        write(separator?: string): void;
        write<TResult>(separator: string, selector: (element: T) => TResult): void;
        writeLine(): void;
        writeLine<TResult>(selector: (element: T) => TResult): void;
        force(): void;
        letBind<TResult>(func: (source: IEnumerable<T>) => { length: number;[x: number]: TResult; }): IEnumerable<TResult>;
        letBind<TResult>(func: (source: IEnumerable<T>) => TResult[]): IEnumerable<TResult>;
        letBind<TResult>(func: (source: IEnumerable<T>) => IEnumerable<TResult>): IEnumerable<TResult>;
        share(): IDisposableEnumerable<T>;
        memoize(): IDisposableEnumerable<T>;
        catchError(handler: (exception: any) => void ): IEnumerable<T>;
        finallyAction(finallyAction: () => void ): IEnumerable<T>;
        log(): IEnumerable<T>;
        log<TValue>(selector: (element: T) => TValue ): IEnumerable<T>;
        trace(message?: string): IEnumerable<T>;
        trace<TValue>(message: string, selector: (element: T) => TValue ): IEnumerable<T>;
    }
 
    interface IOrderedEnumerable<T> extends IEnumerable<T> {
        createOrderedEnumerable<TKey>(keySelector: (element: T) => TKey, descending: boolean): IOrderedEnumerable<T>;
        thenBy<TKey>(keySelector: (element: T) => TKey): IOrderedEnumerable<T>;
        thenByDescending<TKey>(keySelector: (element: T) => TKey): IOrderedEnumerable<T>;
    }
 
    interface IDisposableEnumerable<T> extends IEnumerable<T> {
        dispose(): void;
    }
 
    interface IDictionary<TKey, TValue> {
        add(key: TKey, value: TValue): void;
        get(key: TKey): TValue;
        set(key: TKey, value: TValue): boolean;
        contains(key: TKey): boolean;
        clear(): void;
        remove(key: TKey): void;
        count(): number;
        toEnumerable(): IEnumerable<{ key: TKey; value: TValue }>;
    }
 
    interface ILookup<TKey, TElement> {
        count(): number;
        get(key: TKey): IEnumerable<TElement>;
        contains(key: TKey): boolean;
        toEnumerable(): IEnumerable<IGrouping<TKey, TElement>>;
    }
 
    interface IGrouping<TKey, TElement> extends IEnumerable<TElement> {
        key(): TKey;
    }
}
 
// export definition
declare var Enumerable: linqjs.Enumerable;

アップデートを全然追ってないので、初期に作った定義の仕方のまんまなので、大丈夫かな、まあ、大丈夫じゃないかな、きっと。

とりあえずとにかく面倒くさかった。しかし定義する人が苦労すれば、利用者はハッピーになれるから!!!なのではやくVS対応がまともになってください。ぶんぶんぶん回したいのだけれどねえ←その前にlinq.js ver.3がいつまでもBetaなのをなんとかしろ

既存JavaScriptをTypeScriptとして修正する方法

JavaScriptはTypeScriptです。ほぼほぼ。.jsを.tsとして変更すれば動きます。というほど世の中甘くなくて、まあ、大抵は動きません。えー、なにそれ、欠陥品じゃないの?と思われるかもですが、いえ、結構単純な話です。例えばですが

var x = 0;
x = "hogehoge";

このコード、JavaScriptとしては正しいですが、TypeScriptとしては間違っていてコンパイル通りません。xがnumberとして推論されるので、”hogehoge”が代入できないからです。じゃあどうするの?というと、

var x: any = 0;
x = "hogehoge";

anyとして型定義してやればいいんですね。もしくは

var x = <any>0;
x = "hogehoge";

でもいいですが。<>はキャストみたいなものです。ちなみに、こういったことの実例はTypeScriptのソースをダウンロードしてきて、\src\harness\external\json2.ts に、json2.jsをtsに変換した例が見れます。ほんの2, 3箇所anyの注釈を入れているだけで、ほぼほぼそのままですね。実際のところ、↑みたいなゆるふわキャストなんて、たとえJSといえど多用してるわけがないので、作業的な手間はあまりありません。やることは、コンパイルエラーの出た箇所をポチポチとモグラたたきするだけなので、楽ちん。

実際にやってみる

理屈上はそうですが、実際やってみるとどうなんでしょうねえ、ということで、linq.jsでやってみました。(なお、linq.jsの型定義自体はlinq.jsのTypeScript対応とTypeScript雑感で手付けですでにやってあります)。まず.tsにしてコンパイルかけてみると、赤波線が全体に出てきてわけわからんオワタ!エラー90件!

で、まあ、こういう場合は問題は基底部分にあります。

(function (root, undefined) {
// 中略
})(this);

問題なのはundefinedです。function(root, undefined) として定義しているのに、(this)ということで、呼んでないから。冷静に見てみれば、ただたんにメソッド呼ぶ引数が足りないぜ、って言ってるだけですな。ちなみにこのコード自体は、undefinedは代入可能な代物で破壊されている可能性があるから(あるわけないけど!)、安全なundefinedを作ろう、という古臭いイディオムです。

エラーが90件もあってわけわかりませんが、一番最初のエラーが「Supplied parameters do not match any signature of call target」なので、やっぱり冷静に見てみれば、ちゃんと教えてくれていた、と。TypeScript優しいのね。

なのでundefinedを抜けば真っ赤っ赤はなくなります。OK。だがまだエラーは続く。というかエラー件数は89件になっただけである。

お次はEnumeratorがないぞ!というエラー。

if (typeof Enumerator !== Types.Undefined) {
if (typeof Windows === Types.Object && typeof obj.first === Types.Function) {

このEnumeratorはIEのみに存在するオブジェクトで、Windows Script Hostで列挙するのに使ったり使わなかったりする、今では知らない人のほうが遥かに多いであろう謎オブジェクトです。Windowsのほうも同様に、Windows8用アプリケーションにしか存在しません。さて、これへの対処は、定義ファイルのない外部ライブラリを使う際と同じで、anyでdeclareします。ファイルの先頭に

declare var Enumerator;
declare var Windows

と書いておけばOK。しかしまだまだエラーは続くよ!該当箇所はここ。

var Enumerable = function (getEnumerator) {
    this.getEnumerator = getEnumerator;
};
 
// このUtilsで赤線
Enumerable.Utils = {}; // container

このEnumerableが意図するところはコンストラクタです。new Enumerable()するためのものです。で、JavaScriptでは関数にもオブジェクトを生やせますが、TypeScriptでは生やせません。対処方法はまあ、面倒くさいのでEnumerableをanyにしましょう。

var Enumerable: any = function (getEnumerator) {
    this.getEnumerator = getEnumerator;
};

これだけで割と一気に解決します!89件あったエラーが残りほんの数件に!any最強!dynamic! で、linq.jsでは同じようにOrderedEnumerableとArrayEnumerableというものが存在するので、同様にanyにしておきます。

そんなわけで、なんとなくわかったと思いますが、ようするにエラーの出てるところを片っ端からanyにしていくだけです。ただしルート階層に近いものを優先的にany化すると、その下にぶら下がってるものは全部解決するので、意外とそんな手間じゃありません。

あとは一番下にAMD対応もどきのとこがあるのですが、これはそもそも微妙なのでまるごと削除して解決(てきとー)。で、対応はほんとこれだけです。あっという間だし簡単ですなあ。TypeScriptのJavaScriptとの互換性は本物だ!

declarationsオプション

で、ここからが本題であって本題ではないのですが、TypeScriptはtsc -declarationsとオプションをつけてコンパイルすると、d.tsを吐いてくれます。ちゃんと型定義されたtsファイルならちゃんとしあd.tsを吐いてくれます。役立ちです。

で、人間欲が出るもので、もしこれを、↑のように修正した.tsにかませてやるとどうなる?もし、たとえanyであっても定義テンプレを吐いてくれたら、そこから注釈入れてくだけですむわけで、随分と楽になりますよね?面倒くさい型定義よさようなら。

というわけで、こいつをdeclarationsオプションをつけてコンパイルしましょう。

tsc linq.js.ts -declarations

期待のlinq.js.d.tsの結果は

var Enumerator;
var Windows;

になります(笑)。はい、関数で丸ごと括った部分が消滅してしまいました。クソが。今回は定義ファイルが欲しいだけなので、関数で括る部分を除去して再度コンパイルすると

var Enumerator;
var Windows;
var Functions: { Identity: (x: any) => any; True: () => bool; Blank: () => void; };
var Types: { Boolean: string; Number: string; String: string; Object: string; Undefined: string; Function: string; };
var Utils: { createLambda: (expression: any) => any; isIEnumerable: (obj: any) => bool; defineProperty: (target: any,methodName: any,value: any) => void; compare: (a: any,b: any) => number; dispose: (obj: any) => void; };
var State: { Before: number; Running: number; After: number; };
var IEnumerator: (initialize: any,tryGetNext: any,dispose: any) => void;
var Yielder: () => void;
var Enumerable: any;
var OrderedEnumerable: any;
var SortContext: any;
var DisposableEnumerable: (getEnumerator: any,dispose: any) => void;
var ArrayEnumerable: any;
var WhereEnumerable: (source: any,predicate: any) => void;
var WhereSelectEnumerable: (source: any,predicate: any,selector: any) => void;
var Dictionary;
var Lookup: (dictionary: any) => void;
var Grouping: (groupKey: any,elements: any) => void;

外に出したくないもの(Yielderとか)は、まあ、あとで別途削除すればいいんですが、しかしそもそも肝心のEnumerableメソッドが全部出てないぞ! 理由としては、ようするにanyつけちゃったから。うーん、これじゃ実用度ゼロですね。

そもそもfunctionで定義したクラス(をコンストラクタとして使いたい)というのを、それがコンストラクタなのか関数なのかをどうやって区別するんだ?って話ですし、無理ですなー。(ファイル全てをなめてthis.してるのはクラスとか.prototype = hogehogeしてるのはクラスとか、曖昧な判定はできるでしょうけれど、それは危険ですしね)。

夢は見ちゃダメ。でもMicrosoftならきっといつかはやってくれるはず!(実際、GWT用に空気読んでJavaScriptからJavaの定義を吐いてくれるものは存在するとか)

まとめ

プレーンなJavaScriptはほぼほぼTypeScriptになります。素晴らしい互換性です!一方、型定義自動生成のほうは無理でした。地道に頑張りましょう。

あ、そうそう、今回の記事で言いたいのは別に表題通りの話じゃあないです。既存JSは既存JSとして使えばいいので、わざわざTypeScript化する必要なんて全然ありません。いえ、あります、ちゃんと型付けするならば。でも、今回のようにanyを付けて回る程度の話は全くの無意味です。じゃあどうでもいいかといえば、やっぱりそうじゃなくて、TypeScriptとJavaScriptの互換性というのはどういうものなのか、というとこは位置づけ的には大事ですからね、漠然とじゃあなく抑えておきたいところ。

linq.jsのTypeScript対応とTypeScript雑感

MicrosoftからTypeScriptという新言語が発表されました。驚くべきは、あのC#のAnders Hejlsbergが関わっている!これはもう触るしかない。そしてこれはコンパイル後にJavaScriptになる言語(CoffeeとかJSXとかみたいな)なわけで、じゃあlinq.jsを対応させるしかない!というわけで、させました。

// TypeScript
Enumerable.range(1, 10)
    .where(x => x % 2 == 0)
    .select(x => x * x)
    .writeLine();
 
// コンパイル後
Enumerable.range(1, 10).where(function (x) {
    return x % 2 == 0;
}).select(function (x) {
    return x * x;
}).writeLine();

ひゃっはー、もうfunction() { return }とはオサラバだ!そしてこの記述性と最強のコレクション操作であるLINQが合わさると最強に見える。

に型定義ファイルは同梱してありますので、是非是非お試しを。NuGetのlinq.js -Preでも入ります。NPMは予定はありますが、まだです、すみません。

TypeScriptについて

型安全なCoffee Script、といった印象ですね。基本的にはCoffee Scriptに近いと思います。JavaScriptにプラスアルファな構文を採用することで、既存のJSライブラリとの繋がりを良くすることと、綺麗なJavaScriptを吐くことに重きが置かれている。TypeScriptは、比較的素直にJavaScriptに読み解くことが出来て、独自のコード生成は、現状はほぼほぼ無いし、意図的に省かれているようです(例えば非同期にたいしてasync構文を入れたりすると、大量のコード生成が入り、出力されるJavaScriptが機械的に汚れがち)。

そういった点、機能面では、TypeScriptには物足りなさを感じるところが多いかもしれません。じゃあJavaScriptに対する強みってどこなんだよ!といったら、一つはJavaScriptの冗長な記述性の補正(class,module, arrow function, Object.create/definePropertyとかも最低だしね)。もう一つは、無理なく自然に馴染んだ型付け。

型はないよりあったほうがいい。でも、型を付けるのがあまりにも苦痛だったら?ちょっとしたコードを書くのにも型!型!型!と押し付けられたら?そりゃあ、嫌だ。というわけで、型推論によって、比較的スムースに書けるようになっています。

型推論の性質というか範囲というか強さというかは、C#と非常に近いというかC#とまるで一緒なので、C#erならサクッと馴染めます。もっと強力な型推論のある言語に馴染んでいる人だと、え、ここで効かないの?みたいな違和感はあるかもですが。

また、さすがはMicrosoftというかAnders Hejlsbergというか、入力補完のことを念頭に置いた言語設計になっているので、IDEとの相性が非常に良い。そして最初からVisual StudioによるIDE環境が用意されていることで、型のある利点の一つであるリアルタイムエラー通知や入力補完をたっぷり満喫できます。さらに、それはTypeScript PlaygroundによってWeb上でも体感できます。というか、もはやPlaygroundはWeb IDEとでも言うべき驚異的な動き!

また、Windowsだけではなく、最初からSublime, Vim, Emacsの対応ファイルが用意されているというところからも、Windowsに限らず幅広く請求したい、という表れだと思います。そして実際、言語はプラットフォーム中立なわけです(最終的にはJavaScriptだしね!)。

Structural Subtyping

TypeScriptの最も面白いところは、ここです。C#とかのインターフェイスとLLのダックタイピングの中間、みたいな。実にゆるふわなJavaScriptと絶妙に合っててイイ!というかそもそも私はStructural Subtypingって名前だけでぜーんぜん分かってなかったのですが、TypeScriptだと自然と馴染めます。ほむ、どういうことか、というと、一例を。

union(second: any[], compareSelector?: (element: any) => any): Enumerable;
union(second: Enumerable, compareSelector?: (element: any) => any): Enumerable;
union(second: { length: number;[index: number]: any; }, compareSelector?: (element: any) => any): Enumerable;

これはlinq.jsの型定義の一つでunion、和集合を生成するためのメソッドです。なので、元シーケンスと、対象シーケンス(second)が対象になるわけですが、じゃあシーケンスって何?と。列挙できればいいので、配列もそうだし、Enumerableもそう。そして、JavaScriptに特有の存在として、配列みたいだけど配列じゃあないもの(lengthを持っていてインデクサでアクセスできる)、例えばDOMのNodeListとか、もそう。

で、そういった「lengthを持っていてインデクサでアクセスできる」という型の定義が{ length: number;[index: number]: any; }。これにより、DOMのNodeListやjQuery([0]とかでHTMLElementが取れる)など、配列みたいだけど配列じゃないもの全てが型安全に定義されました。やったね!

もしC#だったら、対象はインターフェイスで指定するしかないので、IEnumerable<T>を実装していないクソコレクションクラスが存在したら、それは列挙不能になってしまいます。片っ端からオーバーロードを作るのは不可能だし、かといってdynamic secondなどとしてしまってはアレ。

とはいえ、基本的にC#では最初から全てのシーケンスはIEnumerable<T>を実装している、という前提が成り立っているので、問題はおこらない。でも、JavaScriptは違う。配列みたいだけど配列じゃあないもの、が跋扈してる。でも、そこをanyとして何でも受け入れられるようにしたら型安全じゃあない。安全にしたい。そこをStructural Subtypingが華麗に解決してくれました!惚れた……。

TypeScriptはJavaScriptか?

JavaScriptコードはそのままTypeScriptだ!ということにYesと言えるかというと、イエスでもあり、しかし割とノーです。私がこの話を聞いて、最初に思ったのは、既存JSコード、つまりライブラリ類もそのままで動くのかな?と。答えはNOです。JS固有の、実行時に切った貼ったして構造作っていくの、ああいうのTypeScriptだと軒並みコンパイルエラーになるので、ダメです。ほとんどのライブラリが絶滅でしょう。

と、勘違いしていたのですが(yharaさん指摘ありがとうございます!)

declare var $; // jQuery
declare var _; // underscore
declare var Enumerable; // linq.js

とかって定義すると、これはそれぞれ $:any, _:any, Enumerable:any という扱いになって、以降はどんなチェーンを繋げてもエラーが起こらない、つまりライブラリが正常に読み込めたかのようになります。

ただ、型チェックや入力補完が効かなくなるので、TypeScript用の型注釈ファイルはあったほうがいいですね。有名ライブラリはともあれ、無名プラグインとかは自前で型注釈書かなければならないかもり。手書きだとかったるいので、自動生成である程度テンプレート吐き出してくれないと、面倒くさい。この辺はMicrosoftだしやってくれるんじゃないかなあ、という淡い期待を抱いていますが……。

とはいえ、ちょっとしたコンパクトなプラグインを使ったり、ライブラリ使うとしても一部分だけだしー、などというのに、わざわざ型定義も馬鹿らしいわけで、さくっと動的な感じにdeclareできちゃう、というのは素晴らしい話。

そんなわけで、JavaScript→TypeScriptの相互運用性としては、繋がりはかなり良好。勿論、jQueryなどもスムースに扱うことができます。これは、文法がJavaScriptプラスアルファで構築されているがことの利点です。そしてTypeScript→JavaScriptは、というと、素直なJavaScriptを吐いてくれることもあり、良好です。TypeScriptで作られた生成物は、TypeScriptだけに閉じません。

JavaScriptを中間言語とする選択肢が増えた。JavaScriptを介することで他の言語とも自由に繋がる。ここには、Webの互換性、中立性を崩す要素は一切ありません。独自言語による囲い込みとかではありません。素直に歓迎できるはずです。ただし、言語としてはあくまでTypeScriptはTypeScriptです。そこだけは、誤解しないほうがいいと思います。文法的に、ES6を若干取り入れているとはいえ、違う言語です。将来的にもTypeScriptはEcmaScriptにならないでしょうし、EcmaScriptはTypeScriptにはならないでしょう。TypeScriptはEcmaScript6のただの代替なのではない、別の価値ある言語です。

変な期待をして、これJavaScriptじゃないじゃん、とかって難癖つけたりは、あまり良くないですね。

TypeScriptとVisual Studio

別にMicrosoft環境に閉じる言語ではないので、EmacsでもVimでもいいですが、やはりVisual Studioが第一な点は少なからずあります。LinuxでもIDEで書きたい?きっとJetBrainsがWebStormに搭載してくれるはずです!(実際、Voteはかなり集まってました)

ともあれ、Visual Studioです。専用拡張のインストールはTypeScriptのサイトから出来ます。プロジェクトテンプレートが何故かC#のところにあって気づきにくいことに注意!それともう一つ、Web Essentialsを入れましょう。元々Coffee ScriptとLESSに対応していたのですが、今回TypeScriptにも対応してくれました。Web Essentialsを入れることで、保存時のコンパイル(通常の拡張だとビルド時のみ)と、ウィンドウ分割での出力後のJS表示、それとSourceMapファイルの出力を行ってくれます。

勿論、IntelliSenseはフルに効くしエラーはリアルタイムでがんがん通知してくれます。TypeScript Playgroundと違うのは、エラーがあるとJSに変換してくれないところですね。まあ、それは正しい挙動なのでいいです。Playgroundで中途半端なエラーのある状態でもガンガン変更表示してくれるのは、それはそれで便利なので、それもまたいいです。

ちなみに、TypeScript Playgroundでは赤波線が出ている状態は、一応、JSを出力してくれてますが、それはコンパイルエラーの状態で完全な出力がされていないと思って良いです。つまり、本来的には動いてないわけです。この動いていない出力を指して、(現状Firefoxにしか乗ってない)JavaScriptへの互換が不完全とかって難癖つけたりするのは、ほんと良くないですね……。

SourceMap

Web Essentialsの吐いてくれるSourceMapとは何ぞや、というと、これはTypeScriptのままデバッグができます。コンパイル時にJSを吐いてくれる系言語の欠点として、デバッガを使ったデバッグが困難、というのが挙げられますがSourceMapを使うとそれも解決、します。

現状、対応ブラウザはChromeと、まあ、他は知らないのですが、とりあえずChromeは対応しています。IE10(とVS2012内蔵デバッガ)も対応してくれると嬉しいなあ。Chromeのデバッガの不満点としては、ブレークポイントが行単位でしか貼れないことですね。ラムダ式の内側に貼れないと、特にLINQのような一行ラムダを多用するものではデバッグがとても不便でして。この辺、改善されていってくれると嬉しい話。

vs JavaScript(のIntelliSense)

実は、VisualStudio 2012のJavaScriptはかなりサポートが手厚く、裏で常にコードを実行して補完候補を出してくれたりします。

なので、純粋なIntelliSenseの効きだけでいうと、TypeScriptはJavaScriptに負けているかもしれない!如何せん、特にlinq.jsではシーケンスの要素がanyになってしまいますからね。JavaScript(を裏で動かして解釈する)ならば、ここも補完効いてしまうという。最近のJavaScript IDEは進化しすぎで恐ろしい……。

ジェネリクス

仕様書にも明言されていますが、正式リリースまでには搭載する予定があるそうです(ちなみに現在は0.8)。ジェネリクスが乗っかるとlinq.jsがすっごくパワフルになるんですよ。如何せん、今はシーケンスの要素の型が全てany扱いで補完が全く効かなくてTypeSafeでもなんでもないのですが、ここが型付けされると完璧なIntelliSense生活!C#並というかむしろC#超えるぐらいの勢いでパーフェクトなLINQ to Objects!なので、相当に待ち遠しいです。

Compiler as a Serviceの未来

TypeScriptのコンパイラはTypeScriptで書かれてます。これ、別にかっこつけとかでもなんでもなく、非常に重要な意味を持ちます。で、いきなり分かりやすく成果物として出してくれているのがTypeScript Playground。構文解析がJavaScriptで可能だから、Web上で全て完結するIDEが作れる。C#も次のバージョンではC#コンパイラがC#で書かれるという計画があります。そのことがもたらす価値の一部分は、TypeScriptが教えてくれます。いや、むしろブラウザ上で全て完結というのは、C#以上の魅力がありますね、正直……。

結論

TypeScriptは、良い言語だと本当に本当に思います。私は、素のJavaScriptも別にそこまで嫌いではないのですけれど、やっぱ、違うなあ、と。なので今後は積極的に使っていきたいところです(CSSもLESSで!)。

言語設計者が同じということもありますが、特にC#erには絶対馴染むと思うので、(linq.jsとセットで)今までJavaScriptとは無縁だった人も手を出して欲しいですね。きっと気に入りますし、視点が変わります。勿論、ネイティブJSerも是非是非触ってみるといいと思います!というか触ってほしいです。

あ、あと、軽く流しましたがVisual StudioユーザーならWeb Essentialsも必ず入れておきましょう。これがあるのとないのとでは、TypeScriptの使い勝手全然違ってくるので、TypeScript試すならば必須です。

Search/Archive

Category

Profile


Yoshifumi Kawai
Microsoft MVP for .NET(C#)

April 2011
|
March 2017

Twitter:@neuecc
GitHub:neuecc
ils@neue.cc