Я пытался создать своего рода монадическую оболочку для данных, извлекаемых из API. Я хочу, чтобы он имел 4 фигуры:
- Начальная буква (я еще не писал)
- Загрузка
- Ошибка
- Loaded
Это фрагмент моей реализации.
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
, которая запускает функцию в зависимости от того, какой это вариант данных. Поэтому, если монада находится в состоянии 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
. Или я не прав?
Как я могу сделать эту работу?
Также , пожалуйста, , дайте мне знать, если есть лучший способ построения такой монады в Typescript, не жертвуя безопасностью типов (возможно, даже улучшая ее, почему бы и нет!)