Проблема на самом деле довольно проста.Решение не обязательно так. Я очистил ваши типы , чтобы выявить проблему:
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>
.