microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.01k stars 12.48k forks source link

Preserve pass-through parameter type in generic function return type #31529

Closed jelbourn closed 3 months ago

jelbourn commented 5 years ago

Search Terms

generic, propagate, multiple, preserve, argument, parameter, pass-through

Suggestion

When a generic function expresses its return type in terms of an argument constraint, the originally given argument type should be preserved/passed-through when the function is called.

Example

interface HasId {
  id: string;
}

interface SpecialArray<D> {
  getSpecialItem(): D;
}

function go<T extends Array<D>, D>(x: T): SpecialArray<D> {
  return x as any;
}

const v: Array<HasId> = [{id: 'one'}];

// We expect a return type of `SpecialArray<HasId>` but instead we
// get `SpecialArray<{}>`.
const w = go(v);

([playground](https://www.typescriptlang.org/play/#src=%0D%0Ainterface%20HasId%20%7B%0D%0A%20%20id%3A%20string%3B%0D%0A%7D%0D%0A%0D%0Ainterface%20SpecialArray%3CD%3E%20%7B%0D%0A%20%20getSpecialItem()%3A%20D%3B%0D%0A%7D%0D%0A%0D%0Afunction%20go%3CT%20extends%20Array%3CD%3E%2C%20D%3E(x%3A%20T)%3A%20SpecialArray%3CD%3E%20%7B%0D%0A%20%20return%20x%20as%20any%3B%0D%0A%7D%0D%0A%0D%0Aconst%20v%3A%20Array%3CHasId%3E%20%3D%20%5B%7Bid%3A%20'one'%7D%5D%3B%0D%0A%0D%0A%2F%2F%20We%20expect%20a%20return%20type%20of%20%60SpecialArray%3CHasId%3E%60%20but%20instead%20we%0D%0A%2F%2F%20get%20%60SpecialArray%3C%7B%7D%3E%60.%0D%0Aconst%20w%20%3D%20go(v)%3B))

Use Cases

My primary use-case for this has been compositional mixins. I want to be able to take some class with a type like HasItems<SomeItem> and augment it with a mixin to add a behavior HasActiveItem<D>, where the mixin has recognizes the SomeItem passed in and propagates it through to satisfy subsequent mixin calls.

This can be done today by omitting one of the generics from the function signature and duplicating those bits of typing in both the parameters and the return type; the remaining generic only captures the "Item" part of the signature.

Checklist

dragomirtitian commented 5 years ago

Typescript will not infer type parameters if they are not used in the parameters of the function. Other type parameters are not (as far as I know) an inference site for type parameters.

There are several ways of getting the code above to work. You could just use D and remove T:


interface HasId {
  id: string;
}

interface SpecialArray<D> {
  getSpecialItem(): D;
}

function go<D>(x: D[]): SpecialArray<D> {
  return x as any;
}

const v: Array<HasId> = [{id: 'one'}];

const w = go(v); //SpecialArray<HasId>

Or use T and use a conditional type to extract the item type:

function go<T extends any[]>(x: T): SpecialArray<T extends Array<infer D> ? D : never> {
  return x as any;
}

const v: Array<HasId> = [{id: 'one'}];

const w = go(v); //SpecialArray<HasId>

While it would be nice if your code would work as is, since there are already ways to achieve the same thing I'm not sure the extra complexity to infer one type parameter from the other is justified.

RyanCavanaugh commented 5 years ago

@dragomirtitian 's analysis is correct.

Without use cases that require new functionality, there's not really a suggestion here. Generic functions are already inferred in many places where appropriate, but the function described in OP doesn't need that.