我正在建立一个数据库,如果后端是内存中的,我想同步工作;如果后端是写入外部存储的,我要异步工作。
棘手的是,我不想两次写所有相同的逻辑-一次写同步版本,一次写异步版本。
基本上,我希望能够将参数传递给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)我考虑过使用回调,但这会带来一系列不同的问题(例如,厄运金字塔)。
请让我知道是否还有其他值得探讨的想法。我理想地希望单子方法可以起作用,但是类型系统可能不够强大。我也不反对某种代码生成器方法,但是我对那些工具不熟悉。