fp-ts
fp-ts copied to clipboard
Typed flow
🚀 Feature request
Hi, thanks for great lib! What do you think about additional overloads for flow like this?
Current Behavior
// flow may be used only in typed context
const optionResult2 = pipe(
(X.prop),
flow(
O.fromNullable,
O.map(flow(inc, toString, alert)),
O.getOrElseW(constUndefined)
)
);
Desired Behavior
// want use like so, but its impossibe in current typescript
// can't use flow out of typed context
const result = flow(
O.fromNullable, // Type 'Some<unknown>' is not assignable to type 'Option<number>'.
O.map(flow(inc, toString)),
O.getOrElseW(constUndefined)
);
Suggested Solution
// add several signatures like this
declare function flow<A extends ReadonlyArray<unknown>>(): <B, ..., PreEnd, End>(
ab: (...a: A) => B,
...
end: (c: PreEnd) => End
) => (...a: A) => End;
// usage
const result = flow<[undefined | number]>()(
O.fromNullable,
O.map(flow(inc, toString)),
O.getOrElseW(constUndefined)
);
result("") // Argument of type '""' is not assignable to parameter of type 'number | undefined'.ts(2345)
result(2) // some("3")
result(undefined) // none
const result = flow()( // - without generic params - fallback to the previous behaviour
O.fromNullable, // Type 'Some<unknown>' is not assignable to type 'Option<number>'
O.map(flow(inc, toString, alert)),
O.getOrElseW(constUndefined)
);
Who does this impact? Who is this for?
Its' allow to create well typed flow out of typed context
Describe alternatives you've considered
Additional context
Your environment
Software | Version(s) |
---|---|
fp-ts | latest |
TypeScript | 4.3.5 |
In the interim, I tend to write mine like this
const extractValue = (e:ChangeEvent<HTMLInputEvent>) => e.target.value
declare const transformValue: <A>(a:a) => MyType
declare const updateState: (t:MyType) => void
const onInputChange: (e:ChangeEvent<HTMLInputElement>) => void = flow(
extractValue,
transformValue,
updateState
)
I.e., I provide the type for the function that the flow
is being used to create.
@KhraksMamtsov In your example, TypeScript is not able to infer the right type from the usage (because it works from left to right). TypeScript needs a little help and you can fix that by annotating the type of result
(like in the example made by @kylegoetz).
Your proposed solution needs annotating the types too and I don't think it would improve the DX that much.
Manually typed functions may has some inaccuracy unlike auto inferred ones. For example:
const toUpperCase = <S extends string>(x: S): Uppercase<S> => x.toUpperCase() as any;
const alrt = <S extends string>(x: S): `${S}!!!` => `${x}!!!`;
const toStr = <N extends number>(x: N): `${N}` => x.toString() as any;
type Union= 1 | 2 | 3 | 4 | 5;
type ResultType = "1!!!" | "2!!!" | "3!!!" | "4!!!" | "5!!!";
declare const arg: Union;
const testResult = toUpperCase(alrt(toStr(arg))); // "1!!!" | "2!!!" | "3!!!" | "4!!!" | "5!!!"
const composition = flow(toStr, alrt, toUpperCase);
// Type '<N extends number>(x: N) => Uppercase<`${N}!!!`>' is not assignable to type 'string'.
const weakTypedResult = (n: Union): string => composition;
// Type '<N extends number>(x: N) => Uppercase<`${N}!!!`>' is not assignable to type 'ResultType'.
const weakTypedResult1 = (n: Union): ResultType => composition;
// Type 'string' is not assignable to type 'R'.
const weakTypedResult2 = <R extends string,>(n: Union): R => composition;
// infer stong type automatically
// (a_0: Union) => "1!!!" | "2!!!" | "3!!!" | "4!!!" | "5!!!"
const goodTypedComposition = fw<[Union]>()(
toStr,
toUpperCase,
alrt
);
// good types, but too long to write
const result: (x: Union) => O.Option<"1!!!" | "2!!!" | "3!!!" | "4!!!" | "5!!!"> = flow(
O.fromNullable,
O.map(composition),
);
// too wide manually typed result ("6!!!") (error?)
const wideTypedResult: (x: Union) => O.Option<"1!!!" | "2!!!" | "3!!!" | "4!!!" | "5!!!" | "6!!!"> = flow(
O.fromNullable,
O.map(composition),
);
// weak types
const weaklyTypedResult: (a_0: Union) => O.Option<string> = flow(
O.fromNullable,
O.map(composition),
);
// good typed auto inferred result
// (a_0: Union) => O.Option<"1!!!" | "2!!!" | "3!!!" | "4!!!" | "5!!!">
const goodTypedResult = fw<[Union]>()(
O.fromNullable,
O.map(composition),
);
const deliberateSafeTypedResult1: (x: 1) => FP.option.Option<string> = goodTypedResult;
const deliberateSafeTypedResult2: (x: 2) => void = goodTypedResult;
in any case, the new overload is:
- backward compatible (until this moment where is no overload for
flow
without arguments) - adds new behaviour with autoinferring without changing the old one
Your proposed solution needs annotating the types too and I don't think it would improve the DX that much. DX improvements:
- no need explicitly annotate result type because of auto inferring
- manually annotated result may contains inaccuracy
- no need type long unions, need to type only first arg:
(x: Union) => O.Option<"1!!!" | "2!!!" | "3!!!" | "4!!!" | "5!!!" | "6!!!">
->flow<[Union]>()(...)
@gcanti what you think?
I am also trying to solve this in my code. Is there any update on this issue?