Типовая подпись для функциональной монады проверки в TypeScript - PullRequest
1 голос
/ 23 сентября 2019

Я написал простую функциональную монаду проверки в машинописи.Чтобы проверить поведение, я сочиняю 3 функции и хочу изучить результат.Но ts-компилятор показывает мне либо ошибку для типов, либо я получаю неверный конечный результат, когда я проверяю (наведите курсор) на мою переменную, где хранится функция compose.

Я попытался поиграть с типами подписи на всех функциях.Но появляется как минимум одна ошибка типа.

Вы можете проверить детская площадка , и вы увидите ошибку на cMap


import { compose } from 'ramda'


export interface Success<T> {
    readonly _tag: string,
    readonly _value: T
}
export interface Failure<T> {
    readonly _tag: string,
    readonly _value: T
}
export type Validate<T> = Success<T> | Failure<T>

export const failure = <T>(value: T): Failure<T> => ({ _tag: 'Failure', _value: value });

export const success = <T>(value: T): Success<T> => ({ _tag: 'Success', _value: value });

export const fromNullable = <T>(value: T): Success<T>|Failure<T> => (value == null) ? failure(value) : success(value);

export const isFailure = <T>(ma: Validate<T|T[]>): boolean => (ma._tag === 'Failure') ? true : false;

export const cMap: <T1,T2>(fn: (a: T1) => T2) => (ma: Success<T1>|Failure<T1[]>) => Success<T1>|Failure<T1[]>  = 
    (fn) => (ma) => isFailure(ma) ? ma : fromNullable(fn(ma._value));

export const cChain: <T1,T2>(fn: (a: T1) => T2) => (ma: Validate<T1>) => T2 | Failure<T1> = 
    (fn) => (ma) => isFailure(ma) ? ma : fn(ma._value);


const email = 'test@test.de'

const isEmail = (value: string): Success<string> | Failure<Array<string>> =>
// dummy implementation
(value.length > 3) ? success(value) : failure(['Email is not valid']) 

const findUser = (value: {email: string}) => {
    return Promise.resolve(value)
}

const toObject = (email: string) =>{
    return {email}
 }

const user = compose(
        cChain(findUser),
        cMap(toObject),
        isEmail
        )(email);


console.log(user)

1 Ответ

2 голосов
/ 23 сентября 2019

Проблема на самом деле довольно проста.Решение не обязательно так. Я очистил ваши типы , чтобы выявить проблему:

const cMap: <T1,T2>(fn: (a: T1) => T2) => (ma: Validate<T1>) => Success<T2>|Failure<T1>|Failure<T2>  = 
    (fn) => (ma) => isFailure(ma) ? ma : fromNullable(fn(ma._value));

Проблема в том, что cMap имеет три возможных типов возврата: это уже может быть ошибкой(Сбой), он может преуспеть (Успех) или, что критически, может произойти сбой при fromNullable вызове .Если вы не учитываете все три возможности, компилятор будет справедливо жаловаться на то, что ваша подпись неполная.

При правильных типах на месте, когда вы начинаете создавать свою композицию, вы получаете

const f = cChain(findUser);

То, что вам скажет компилятор, имеет подпись

const f: (ma: Validate<{
    email: string;
}>) => Promise<{
    email: string;
}> | Failure<{
    email: string;
}>

и

const m = cMap(toObject);

, которая имеет подпись

const m: (ma: Validate<string>) => Failure<string> | Success<{
    email: string;
}> | Failure<{
    email: string;
}>

И теперь это совершенно яснопочему они не собираются вместе, результатом цепочки может быть Failure<string>, а функция отображения ожидает Validate<{ email: string }>.Именно поэтому я спросил в комментариях, действительно ли сбой должен быть универсальным: по крайней мере из ваших примеров его свойство value всегда будет нулевым или неопределенным, кроме случаев, когда вы явно сконструировали его со строкой.

Если выЕсли вы просто не можете принять необязательное строковое сообщение, все становится намного менее беспорядочным :

interface Success<T> {
    readonly _tag: string,
    readonly _value: T
}

// I've used a class here because it's very easy to
// satisfy the typechecker with instanceof. You don't
// *have* to use a class, it's just easier.
class Failure {
  _tag: string
  _value: string
  constructor (value?: any) {
    this._tag = 'Failure';
    if (typeof value === 'string') {
      this._value = value;
    } else {
      this._value = 'undefined or null or unknown value';
    }
  }
}

type Validate<T> = Success<T> | Failure

export const success = <T>(value: T): Success<T> => ({ _tag: 'Success', _value: value });

export const fromNullable = <T>(value: T): Validate<T> => (value == null) ? new Failure() : success(value);

export const isFailure = <T>(ma: Validate<T>): boolean => (ma._tag === 'Failure') ? true : false;

export const cMap: <T1,T2>(fn: (a: T1) => T2) => (ma: Validate<T1>) => Validate<T2>  = 
    (fn) => (ma) => ma instanceof Failure ? ma : fromNullable(fn(ma._value));

export const cChain: <T1,T2>(fn: (a: T1) => T2) => (ma: Validate<T1>) => T2 | Failure = 
    (fn) => (ma) => ma instanceof Failure ? ma : fn(ma._value);


const email = 'test@test.de'

const isEmail = (value: string): Success<string> | Failure =>
// dummy implementation
(value.length > 3) ? success(value) : new Failure('Email is not valid')

const findUser = (value: {email: string}) => {
    return Promise.resolve(value)
}

const toObject = (email: string) =>{
    return {email}
}

const f = cChain(findUser);
const m = cMap(toObject);

const c1 = pipe(isEmail, m);
const c2 = pipe(m, f);

const user = pipe(
    isEmail,
    cMap(toObject),
    cChain(findUser)
)(email);

И теперь вы можете видеть, что пользователь имеет ожидаемый тип Promise<{ email: string }>|Failure.Одна последняя, ​​обычно цепочка IIRC должна иметь вид m => ma ~> (a -> mb) -> mb , и ваша команда возвращает простой b в конце вместо Validate<b>.

...