我一直在尝试为从API提取的数据创建一种Monadic包装器。我希望它具有4种形状:
这是我的实现的摘要。
type Data<A> = Failed<A> | Loaded<A> | Loading<A>
export class Loaded<A> {
readonly kind: "Loaded" = "Loaded"
constructor(public readonly value: A) {}
map<B>(f: (a: A) => B): Data<B> {
return loaded(f(this.value))
}
chain<B>(f: (a: A) => Data<B>): Data<B> {
return f(this.value)
}
flatMap<B>(f: (a: A) => Data<B[]>): Data<B>[] {
const result = f(this.value)
switch(result.kind) {
case 'Failed':
return []
case 'Loading':
return []
case 'Loaded':
const elements = result.value
const loadedElements = elements.map(loaded)
return loadedElements
}
}
/* ... some type guards ... */
public match<O1, O2, O3>({
loading,
loaded,
failed,
}: {
loading: (percent: number) => O1,
loaded: (value: A) => O2,
failed: (error: any) => O3,
}):O2 {
return loaded(this.value)
}
}
export class Failed<A> {
readonly kind: "Failed" = "Failed"
constructor(public readonly error: any = undefined) {}
map<B>(f: (a: A) => B): Data<B> {
return failed(this.error)
}
chain<B>(f: (a: A) => Data<B>): Data<B> {
return failed(this.error)
}
flatMap<B>(f: (a: A) => Data<B[]>): Data<B>[] {
return []
}
/* ... some type guards ... */
public match<O1, O2, O3>({
loading,
loaded,
failed,
}: {
loading: (percent: number) => O1,
loaded: (value: A) => O2,
failed: (error: any) => O3,
}):O3 {
return failed(this.error)
}
}
export class Loading<A> {
readonly kind: "Loading" = "Loading"
constructor(public readonly percent: number = 0) {}
map<B>(f: (a: A) => B): Data<B> {
return loading()
}
chain<B>(f: (a: A) => Data<B>): Data<B> {
return loading()
}
flatMap<B>(f: (a: A) => Data<B[]>): Data<B>[] {
return []
}
/* ... some type guards ... */
public match<O1, O2, O3>({
loading,
loaded,
failed,
}: {
loading: (percent: number) => O1,
loaded: (value: A) => O2,
failed: (error: any) => O3,
}):O1 {
return loading(this.percent)
}
}
// helper functions
const failed = <A>(error?: any):Data<A> => new Failed<A>(error)
const loaded = <A>(value: A):Data<A> => new Loaded<A>(value)
const loading = <A>():Data<A> => new Loading<A>()
const maybe = <A>(value?: A):Data<A> => value === undefined ? failed() : loaded(value)
我已经测试了map,flatMap和chain方法,它们似乎按预期工作(类型和运行时行为均如此)
我想拥有一个match
函数,该函数根据数据的变体形式运行一个函数。因此,如果monad处于failed
状态,它将运行failed
回调,如果loaded
然后运行loaded
函数,等等...
我确保使该函数具有4个泛型输出O1, O2, O3, O4
并显式注释返回类型(尽管打字稿应该能够很容易地推断出它。)
问题出现在这里:
const data = maybe(3)
const x = data.match({
loaded: () => 'string',
loading: () => [],
failed: () => 3,
})
x // <-- content is 'string' but when type says number
当它应该知道数据的类型为Loaded
时,就推断出x是错误的,说它是数字类型。还是我错了?
我该如何做?
还请让我知道在Typescript中是否有一种更好的方法可以在不牺牲类型安全性的情况下构建这样的monad(可能甚至改进它,为什么不这样做!)
答案 0 :(得分:0)
查看可能是数据,匹配项的错误类型
const maybe: <A>(value?: A) => Data<A>
const data: Data<number>
(method) match<any[], string, number>({ loading, loaded, failed, }: { loading: (percent: number) => any[]; loaded: (value: number) => string; failed: (error: any) => number; }): number
您应该将可验证的“数据”定义为已加载<>
const data2 = new Loaded(3);
// const x2: string
const x2 = data2.match({
loaded: () => 'string',
loading: () => [],
failed: () => 3,
})
答案 1 :(得分:0)
“何时应该知道数据的类型为已加载”。
不应该这样,因为maybe
的返回类型是Data<A>
,所以data
的推断类型是Data<number>
。但是,我希望推断出的类型是3种情况下的返回类型的并集:number | string | any[]
,或者可能会报告错误。相反,它似乎是从第一个选项中选择返回类型(如果更改为type Data<A> = Loading<A> | Failed<A> | Loaded<A>
,则会看到x
的类型更改)。我看不出这种行为有充分的理由,甚至可能是一个错误。
按照有区别的工会文档中的说明使用kind
可以解决此问题:
function match<A, O1, O2, O3>(data: Data<A>, matcher: {
loading: (percent: number) => O1,
loaded: (value: A) => O2,
failed: (error: any) => O3,
}) {
switch (data.kind) {
case "Failed": return data.match(matcher)
case "Loaded": return data.match(matcher)
case "Loading": return data.match(matcher)
}
}
const y = match(data, {
loaded: () => 'string',
loading: () => [],
failed: () => 3,
})
y
的类型如预期的那样在on the playground中显示为string | number | any[]
。