プログラミングTypeScript(オライリー)の練習問題4.5の自分なりの実装

流石にもう逃げてちゃダメだな、ということで最近プログラミングTypeScript ――スケールするJavaScriptアプリケーション開発を読みながらTypeScriptを勉強しています。

正直な所、TypeScriptの型システムは複雑怪奇すぎて、本当にコレを使いこなしているプロジェクトなんて有るんだろうか?と思ってしまいます。 逆に、もしこの型システムを完璧に使いこなしているプロジェクトが有るとするなら、自分はそのプロジェクトに参画することは不可能です。

多分この感覚は勉強を続けても変わることは無いんじゃないかと思っています。

と、愚痴はこのぐらいにしておいて、タイトル通り第4章の最後の練習問題の一部を自分なりに実装してみました。

4.5練習問題の4番目

前項までに説明されたcall関数で、第1引数に渡す関数fの第2引数はstring固定にせよ、というものです。 以下がそのcall関数です。

test("4.2.5.2 制限付きポリモーフィズムを使って可変長引数をモデル化する", () => {
    function call<T extends unknown[], R>(
        f: (...args: T) => R,
        ...args: T
        ): R {
        return f(...args)
    }

    function fill(length: number, value: string): string[] {
        return Array.from({length}, () => value)
    }

    function fill2(length: number, value: number): string[] {
        return Array.from({length}, () => value.toString())
    }

    expect(call(fill, 3, 'a')).toEqual(['a', 'a', 'a'])
    expect(call(fill2, 3, 99)).toEqual(['99', '99', '99'])
})

コレを指定通り変更したものが以下のとおりです。

test("4.5 練習問題の4番目", () => {
    // 元々宣言していた型Tは、unknwon型の配列。
    // なので、普通に以下のように配列の型を宣言してあげればOKっぽい
    type A = [number, string, ...unknown[]]
    function call<T extends A, R>(
        f: (...args: T) => R,
        ...args: T
        ): R {
        return f(...args)
    }

    function fill(length: number, value: string): string[] {
        return Array.from({length}, () => value)
    }
    function fill2(length: number, value: number): string[] {
        return Array.from({length}, () => value.toString())
    }
    function fill3(length: number, value1: string, value2: string): string[] {
        return Array.from({length}, () => value1 + value2)
    }

    expect(call(fill, 3, 'a')).toEqual(['a', 'a', 'a'])

    // 以下は意図した通りコンパイル時点でエラー。
    // expect(call(fill2, 3, 99)).toEqual(['99', '99', '99'])

    // ちなみに以下もコンパイルエラー
    // このことからも、callに渡す第1引数の関数のシグネチャからちゃんと型推論されていることが分かる。
    // expect(call(fill3, 3, 'a')).toEqual(['a', 'a', 'a'])
    // こっちは当然OK.
    expect(call(fill3, 3, 'a','b')).toEqual(['ab', 'ab', 'ab'])
})

4.5 練習問題の5番目

型安全なアサーション関数isを実装せよ、というものです。 自分の実装は以下のようになりました。

test("4.5 練習問題の5番目 1", () => {
    function is<T>(a: T, b: T): boolean {
        return a === b
    }

    expect(is('string', 'otherstring')).toEqual(false)
    expect(is(true, false)).toEqual(false)
    expect(is(42, 42)).toEqual(true)

    // 異なる型同士の比較はコンパイルエラー
    // expect(is(10, 'foo')).toEqual(false)
})

任意の数の引数を受け取れるように変更する

is関数を修正して、任意の数の引数を受け取れるように修正せよ、とのことです。 サンプルではis([1], [1,2], [1,2,3]) === falseが実行できるようにせよとなっています。

長くなりますが、以下が自分の実装です。

test("4.5 練習問題の5番目 2", () => {
    function is<T>(...values: T[][]): boolean {
        if (values.length === 1) {
            return true
        }

        // そもそも要素数同じですかね?をチェック
        const sizes = new Set(values.map(v => v.length)).size
        if (sizes !== 1) {
            return false
        }

        const [head, ...tail] = values
        const isIdentical = (v: T[]) => {
            for (let i = 0; i < v.length; i++) {
                if (head[i] !== v[i]) {
                    return false
                }
            }
            return true
        }
        return tail.reduce((prev:boolean, cur: T[]) => {
            if (prev) {
                return isIdentical(cur)
            } else {
                return false
            }
        }, true)
    }

    expect(is([1], [1,2], [1,2,3])).toEqual(false)
    expect(is([1], [1], [1])).toEqual(true)
    expect(is([1,2], [1,2], [1,2])).toEqual(true)

    // 要素数が違えば当然false
    expect(is([1,2], [1,2,3], [1,2])).toEqual(false)
    expect(is([1,2], [1,2], [1,2,3])).toEqual(false)
    expect(is([1,2,3], [1,2], [1,2])).toEqual(false)
    expect(is([1,2,3], [1,2], [1,2])).toEqual(false)

    // 要素数は同じでも値が異なれば当然ダメ
    expect(is([1,2], [1,2], [1,3])).toEqual(false)
    expect(is([1,3], [1,2], [1,2])).toEqual(false)
    expect(is([1,2], [1,3], [1,2])).toEqual(false)
    // 以下は要素の型が異なるのでコンパイルエラー
    // expect(is(1, [1,2], [1,2])).toEqual(false)
    // expect(is([1,2], [1,3], ['a', 'a'])).toEqual(false)
})

正直、配列同士の比較ではもっと良い実装が有ると思います。 更にいうと、そもそも配列の中身がどんな構造(型)になるかは不明なので、is関数の中で決め打ちせずに、is関数の引数自体に比較関数を渡すべきだと思います。 が、前述の通りサンプルとしてis([1], [1,2], [1,2,3]) === falseとなっていたので、isのシグネチャを変えずに上記のような実装になりました。

公開日:2021/10/18

TypeScript

About me

ドイツの現地企業でWeb Developer/System Administratorとして働いているアラフォーおじさんです。

プログラミングとかコンピュータに関する事がメインですが、日常的なメモとか雑多なことも書きます。

Links :
目次

4.5練習問題の4番目


4.5 練習問題の5番目


任意の数の引数を受け取れるように変更する