How to describe return type of function which is result of call passed generic function

Viewed 154

The question is how to describe the return type of function bar which should be the result of execution of generic function fn.

function foo(fn<T>) /*return →*/ function bar<B> /*return →*/ call fn<B>()

Example:

function foo<T extends <X>(...args: any) => any>(fn: T) {
    return function bar<B>(): ReturnType<typeof fn> /***/ {
        return fn<B>({ /*config*/ });
    }
}

{
    const f1 = <T>() => Promise.resolve(({/*input*/ } as T));
    const x1 = foo(f1)
    const y1 = x1<{ a: 'A' }>()/**Promsise<unknown>❌*/.then(data => data.a); // should be Promise<{ a: 'A'}> ✅
    const y12 = x1<{ b: 'B' }>()/**Promsise<unknown>❌*/.then(data => data.b); // should be Promise<{ b: 'B'}> ✅
}

{
    const f2 = <T>() => ({/*input*/ } as T);
    const x2 = foo(f2)
    const y2 = x2<{ a: 'A' }>()/**unknown❌*/.value // should be { a: 'A'} ✅
    const y21 = x2<{ b: 'B' }>()/**unknown❌*/.value // should be { b: 'B'} ✅
}

Thanks

Update 1 :

TypeScript related issues

3 Answers

Interesting question, since I've experimented a lot with coding techniques in this area as a platform engineer, so I thought I'd share some suggestions.

I have found that generics on their own do not provide a full solution, or the most readable code. When dealing with API calls it can help to formulate requirements and provide layers with different responsibilities. Here is an example approach that I use in my own apps - expressed via React + Axios.

REQUIREMENTS

  • React views must get a typesafe experience when dealing with API payloads
  • Reliability and retries must be handled by shared classes used in multiple apps, eg to retry API calls, implement circuit breaker or whatever
  • Code must be simple and readable

CODE SNIPPETS

  • React view model uses typed API entities without casting and the UI renders this in a type safe manner. All views and view models get this experience.

  • Fetch class implements low level stuff and could be shared between multiple apps. At one point I used generics here and the code wss much less readable.

  • Service agent class. A simple adapter with methods that serve views in the most natural way. There are casts here but they are only in this layer.

  • In other places limited generics are used, eg to call axios.get<T>. However, using generics in many places with separate request and response types quickly gets messy.

CONCLUSION

Sometimes layering and separation of concerns provides the cleanest solution, rather than using generics too much. Out of interest the design is portable to any language, and I also use it for mobile apps:

Of course, my code could be improved, eg to validate better before casting - perhaps service agents could manage that. But all developers in a software company would understand my code, which I think is the most important outcome.

Providing this answer but not sure this is what you are expecting as your initial question is missing some info..

function foo<T extends (...args: any) => any>(fn: T):()=>ReturnType<typeof fn> {
  return function bar(): ReturnType<typeof fn> 
   {
      return fn({ /*config*/ });
  }
}


  const f1 = () => Promise.resolve(({ value: 'value' } ));
  const x1 = foo(f1)
  const y1 = x1().then(data => data.value); // type of Promise<string> as value is string 

  const f2 = () => Promise.resolve(({ value:1234 } ));
  const x2 = foo(f1)
  const y2 = x2().then(data => data.value); // type of Promise<number> as value is string 

I don't think this way is idiomatic for typescript:

function foo<T extends <X>(...args: any) => any>(fn: T) {
  return function bar<B>(): ReturnType<typeof fn> /***/ {
    return fn<B>({ /*config*/ });
  }
}

X generic parameter is unused. It should be removed.

Consider this example:

const fn = <T,>(): T => [1, 2, 3] as any as T
const result_0 = fn<number[]>() // number[]
const result_1 = fn<number[] & { tag: 42 }>() // number[]
result_1.tag // 42 but undefined in runtime

This is very unsafe to use generic type which is not related to any argument. I would even say that it is a bad practice in TypeScript. Also you should never use explicit generic arguments during function call< like here x1<{ a: 'A' }>(). Generic { a: 'A' } is not related to any argumenty.

However, there is a amsll expection. You should use explicit generic if you are passing empty array to a function like here:

const array = <T,>(list: T[]) => list
array([]) // never
array<number>([]) // number

AFAIK, this is only one case I know where you are forced to provide explicit generic. Maybe there are other exceptions but I don't know about them. If you are interested in function argument inference you can check my article.

To summarize, this line x1<{ b: 'B' }>() is unsafe as well as using as T in <T>() => Promise.resolve(({/*input*/ } as T)).

ALso, if you want to express type of any function, you should use this annotation (...args: any[]) => any instead of this (...args: any) => any because ...args is an array.

Let's go back to the original example. In order to make it safer, I would write it as follow:


type Fn = (...args: any[]) => any

function foo<Callback extends Fn>(fn: Callback) {
  return function bar<Config>(config: Config): ReturnType<typeof fn> /***/ {
    return fn(config);
  }
}

We can do even better. Since you want to infer only return type, we can use appropriate generic parameter:


type Fn<Return> = (...args: any[]) => Return

const foo = <Return,>(fn: Fn<Return>) => <Config,>(config: Config) => fn(config);

// data argument should have explicit type annotation
const f1 = (data: { a: 'A' }) => Promise.resolve((data));

const x1 = foo(f1)

x1(42).then(elem => {
  elem.a // "A"
})

As you might have noticed, it is possible to reduce function annotation. Also, I have not used explicit ReturnType<typeof fn> because typescript is able to infer it.

Related