Typescript выводит неправильный возврат метода? - PullRequest
1 голос
/ 12 июня 2019

Я пытался создать своего рода монадическую оболочку для данных, извлекаемых из 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, не жертвуя безопасностью типов (возможно, даже улучшая ее, почему бы и нет!)

Ответы [ 2 ]

0 голосов
/ 12 июня 2019

"когда он должен знать, что данные имеют тип Loaded."

Не должно быть, потому что ваш тип возврата для 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 показан на игровой площадке как string | number | any[], как и ожидалось.

0 голосов
/ 12 июня 2019

Посмотрите на инерционные типы для возможно, данных, совпадения

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

Вы должны определить проверяемые "данные" как Loaded <>

const data2 = new Loaded(3);
// const x2: string
const x2 = data2.match({
  loaded: () => 'string',
  loading: () => [],
  failed: () => 3,
})

Детская площадка

...