fp-ts icon indicating copy to clipboard operation
fp-ts copied to clipboard

Typed flow

Open KhraksMamtsov opened this issue 3 years ago • 5 comments

🚀 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

KhraksMamtsov avatar Jul 08 '21 16:07 KhraksMamtsov

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.

kylegoetz avatar Jul 11 '21 00:07 kylegoetz

@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.

DenisFrezzato avatar Jul 11 '21 07:07 DenisFrezzato

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]>()(...)

KhraksMamtsov avatar Jul 11 '21 10:07 KhraksMamtsov

@gcanti what you think?

KhraksMamtsov avatar Jul 22 '21 06:07 KhraksMamtsov

I am also trying to solve this in my code. Is there any update on this issue?

kaladivo avatar Mar 08 '23 17:03 kaladivo