Typescript Definitions - nested functions

2019-07-23 04:44发布

问题:

As listed below, let's say we have:

  1. a function r that combines multiple tasks together
  2. and a function o that returns a shape like when(cb).map(cb)

every callback passed to when or map should always take these 3 arguments: S, A, C where S and C are defined in r and A is defined in o.

here is a link to the typescript playground which also shows the error I am getting.

My question is: How can I get a typesafe declaration?

type Task<S, A, C> = <AA extends A>(s: S, a: AA, c: C) => any;
type Fn<S, A, C> = (s: S, a: A, c: C) => any;

const r = <S, C>() => ({
  tasks: (...tasks: Task<S, any, C>[]) => null,
});

const o = <T, A = { prop: T }>(type: T) => ({
  when: <S, C>(fp: Fn<S, A, C>) => ({
    map: <SS extends S, CC extends C>(fn: Fn<SS, A, CC>): Task<SS, A, CC> => (
      (s: SS, a: A, c: CC): any => (
        fp(s, a, c) ? fn(s, a, c) : s
      )
    ),
  }),
});

const result = r<2, 7>().tasks(
  o(44) // expect: cb(2, 44, 7)
    .when((s, a, c) => s + a.prop + c)
    .map((s, a, c) => s + a.prop + c),

  o(78) // expect: cb(2, 78, 7)
    .when((s, a, c) => s + a.prop + c)
    .map((s, a, c) => s + a.prop + c),

  // etc...
  // callback provided to `.map` is typesafe,
  // callback provided to `.when` is not,
);

As you can see, the callback provided to when is not typesafe: Params S and C are lost.

回答1:

Hmm, among other problems, it looks like you want some contextual type inference that the language doesn't provide for. Here is how I would recommend typing things:

type Fn<S, A, C> = (s: S, a: A, c: C) => any;

// allow tasks to be an array of Fn<S, any, C> 
const r = <S, C>() => ({
  tasks: <FF extends Fn<S, any, C>[]>(...tasks: FF) => null,
});
// it is concerning that S and C must be explicitly specified
// when calling r(); not sure how you will hook that up to runtime

// only make types generic if they need to be, 
// and declare close to their uses if you want them to be inferred
const o = <T>(type: T) => ({
  when: <S, C>(fp: Fn<S, { prop: T }, C>) => ({
    map: (fn: Fn<S, { prop: T }, C>) => (s: S, a: { prop: T }, c: C): any =>
      fp(s, a, c) ? fn(s, a, c) : s,
  }),
});

const result = r<2, 7>().tasks(
  o(44) // expect: db(2, 44, 7)
    // you need to annotate the s and c parameters in when().
    // The compiler does not try to infer S and C in o(44).when() contextually 
    // from the contextual parameter type of r<2.7>().tasks().
    .when((s: 2, a, c: 7) => s + a.prop + c)
    .map((s, a, c) => s + a.prop + c),

  o(78) // expect: db(2, 78, 7)        
    // alternatively you can specify S and C manually:
    .when<2, 7>((s, a, c) => s + a.prop + c)
    .map((s, a, c) => s + a.prop + c),
  // etc...
);

You've changed your definitions since I worked on this, so the following might not match up exactly with what you posted. In short:

  • Make r().tasks() take an array (possibly a tuple) of Fn<S, any, C> values, so the fact that the second task's A is not the same as the first won't cause an error.

  • Don't have a generic T and also a generic A = {prop: T}. I'm guessing A is not meant to be independent from T and you are trying to use a default type parameter to represent some kind of assignment, but it doesn't really work that way. Instead, just use T and then replace all instances of A with {prop: T}.

  • Only have as many generic types as you need and as close to the desired inference place as possible. I've moved S and C to o().when.

  • Finally, contextual typing from the parameter of r<2.7>().tasks() to the values of S and C in o().when() does not occur. The compiler probably doesn't even try to do it, since the inference would have to happen across multiple levels of function call. The only way to deal with that seems to be to re-specify S and C, either by annotating the s and c parameters of the callback passed to o().when(), or by calling o().when<2,7>().

Hope that helps point you in the right direction. Good luck!