How can an inferred parameter type be used in return type position without outright duplication?

59 views Asked by At

I'm trying to figure out how to return an inferred parameter type for the purposes of a chaining API. Here is a simplified example:

type NoInfer<T> = [T][T extends unknown ? 0 : never]

interface Alpha<Foo extends string> {
  foo: Foo
  bar: `Depends on ${NoInfer<Foo>}`
}

interface Chain<ACC> {
  bravo: <T extends { [K in keyof T]: string }>(xs: { [K in keyof T]: Alpha<T[K]> }) => void 
  // instead of void, return Chain<[***inferred type of xs***, ...ACC>     -------------^^^^
}

In the above, bravo parameter type works well but the ability to build up type context via a chain is missing. How would the inferred type of xs on every call to bravo be captured in the return type and passed recursively to the generic type Chain?

Playground link

Note I have discovered that making the return type be { [K in keyof T]: Alpha<T[K]> } seems to work. TS seems to have behaviour such that the given input (parameter arguments) will determine what this type in return position becomes.

However, the outright duplication is at best quite hard to read, maintain, etc. Is there a solution that doesn't go down this path?

1

There are 1 answers

0
jcalz On BEST ANSWER

What you really want is to create inline type aliases, as described in microsoft/TypeScript#30979. Unfortunately that feature is not part of the language. If it were, then maybe I could tell you to write

// ⚠☠ THE FOLLOWING CODE IS NOT VALID TS; DON'T TRY IT ☠⚠
interface Chain<A extends unknown[]> {
  bravo: <T extends { [K in keyof T]: string }>(     
    xs: type U = {[K in keyof T]: Alpha<T[K]>}
  ) => Chain<[U, ...A]>
}

and move on. Without such a feature, all I can give you are workarounds.


The most generally applicable workaround is to just create a utility type; that is, regular type alias external to the place you need it:

type MapAlpha<T extends { [K in keyof T]: string }> =
  { [K in keyof T]: Alpha<T[K]> }

And then use it multiple times where you need it:

interface Chain<A extends unknown[]> {
  bravo: <T extends { [K in keyof T]: string }>(
    xs: MapAlpha<T>) => Chain<[MapAlpha<T>, ...A]>
}

This requires a little more code than you might want, but it's obvious what you're doing. Also, if the type is something you actually do need multiple times, it might be worthwhile to give it a meaningful name (MapAlpha for something that maps Alpha over an object type seems reasonable to me).


As a second approach: if there's a value of the type you want in scope, you can use the typeof operator to refer to its type. This will work even if the value is a function parameter:

interface Chain<A extends unknown[]> {
  bravo: <T extends { [K in keyof T]: string }>(
    xs: { [K in keyof T]: Alpha<T[K]> }
  ) => Chain<[typeof xs, ...A]>
}

Here typeof xs is the same as { [K in keyof T]: Alpha<T[K]> }.

This is terser, but might be a little more confusing to readers. But the main issue is that it's not always possible to find a value directly with the type you want, and you might have to fall back to a utility type definition.


There are sometimes other workarounds involving using extra type parameters in a wider scope to act as an inline type alias, but for the example as given I couldn't come up with anything I'd want to suggest. I mean, the following sort of works:

interface Chain<A extends unknown[]> {
  bravo: <T extends { [K in keyof T]: string }, U>(
    xs: { [K in keyof T]: Alpha<T[K]> } & U) => Chain<[U, ...A]>
}

But it relies on U being inferred from xs separately from T, and the types are therefore not necessarily identical. I'd almost always recommend creating a utility type before playing games with inference like this.

Playground link to code