编写可同时同步和异步工作的类型安全代码的最佳方法是什么?

时间:2019-09-09 01:40:42

标签: typescript async-await

我正在建立一个数据库,如果后端是内存中的,我想同步工作;如果后端是写入外部存储的,我要异步工作。

棘手的是,我不想两次写所有相同的逻辑-一次写同步版本,一次写异步版本。

基本上,我希望能够将参数传递给API的同步版本或API的异步版本的函数,并使其返回同步响应或异步响应。

interface DatabaseAPI {
    get: {
        request: { id: string }
        response: object | undefined
    }
    set: {
        request: { id: string; value: object }
        response: void
    }
}

type SyncDatabaseAPI = {
    [API in keyof DatabaseAPI]: (
        args: DatabaseAPI[API]["request"]
    ) => DatabaseAPI[API]["response"]
}

type AsyncDatabaseAPI = {
    [API in keyof DatabaseAPI]: (
        args: DatabaseAPI[API]["request"]
    ) => Promise<DatabaseAPI[API]["response"]>
}

我一直在使用的一些想法:

(1)我尝试了Monad方法将所有内容包装在Task中。

export class Task<T> {
    constructor(public value: T | Promise<T>) {}

    map<V>(fn: (value: T) => V): Task<V> {
        if (this.value instanceof Promise) {
            return new Task(this.value.then(fn))
        } else {
            return new Task(fn(this.value))
        }
    }

    chain<V>(fn: (value: T) => Task<V>): Task<V> {
        if (this.value instanceof Promise) {
            // .then will unwrap the promise task (i think)
            return new Task(this.value.then(fn))
        } else {
            return new Task(fn(this.value).value)
        }
    }
}

这里令人讨厌的是,您无法确定任务是同步还是异步。因此,我将其分为两种不同的类型:

class SyncTask<T> {
    constructor(public value: T) {}

    map<V>(fn: (value: T) => V): SyncTask<V> {
        return new SyncTask(fn(this.value))
    }

    chain<V>(fn: (value: T) => SyncTask<V>): SyncTask<V> {
        return new SyncTask(fn(this.value).value)
    }
}

class AsyncTask<T> {
    constructor(public value: Promise<T>) {}

    map<V>(fn: (value: T) => V): AsyncTask<V> {
        if (this.value instanceof Promise) {
            return new AsyncTask(this.value.then(fn))
        } else {
            return new AsyncTask(Promise.resolve(fn(this.value)))
        }
    }

    chain<V>(fn: (value: T) => AsyncTask<V>): AsyncTask<V> {
        if (this.value instanceof Promise) {
            return new AsyncTask(this.value.then(result => fn(result).value))
        } else {
            return new AsyncTask(fn(this.value).value)
        }
    }
}

但是由于某种原因,类型系统对此不满意:

function logic(db: SyncDatabaseAPI | AsyncDatabaseAPI) {
    db.get({ id: "a" }).chain(result => db.set({ id: "b", value: result }))
}                       ^^^^^

> This expression is not callable.
>   Each member of the union type '(<V>(fn: (value: object) => SyncTask<V>) => SyncTask<V>) | (<V>(fn: (value: Promise<object>) => AsyncTask<V>) => AsyncTask<V>)' has signatures, but none of those signatures are compatible with each other.ts(2349)

我觉得这种方法行不通。

(2)我尝试的下一种方法是使用代数效应(good explanation here)。

问题在于,即使使用Typescript 3.6生成器,类型仍然无法正确推断

type APIName = keyof DatabaseAPI

type APIRequest<N extends APIName = APIName> = DatabaseAPI[N]["request"]

type APIResponse<N extends APIName = APIName> = DatabaseAPI[N]["response"]

// Generator yields Messages that the handler will process
type APIMessage<N extends APIName = APIName> = {
    name: N
    request: APIRequest<N>
}

function* logic(): Generator<APIMessage, any, APIResponse> {
    const result = yield { name: "get", request: { id: "a" } }
    // result is inferred as: void | object
}

这很烦人,似乎只有TypeScript才能使这种方法起作用。

(3)我做了一个library,它用Polyfills Promise解决了Promise与TypeScript es5生成器运行时同步工作的问题。您可以在此处看到代码,但是我基本上只是选择了Promise / A +并摆脱了setImmediate

此解决方案有效,但感觉确实很不稳定,可能不是一个好主意。

(4)我考虑过使用回调,但这会带来一系列不同的问题(例如,厄运金字塔)。

请让我知道是否还有其他值得探讨的想法。我理想地希望单子方法可以起作用,但是类型系统可能不够强大。我也不反对某种代码生成器方法,但是我对那些工具不熟悉。

0 个答案:

没有答案