Skip to content

Infer type in conditional cannot unify generics #22617

Open

Description

TypeScript Version: 2.8.0-dev.20180315

Search Terms:
infer conditional unify generic function

Code

type Apply1<T, V> = T extends (item: V) => infer R ? R : never;
type A1 = Apply1< (item: string) => string[] , string>;
type B1 = Apply1< <U>(item: U) => U[], string>;

type Apply2<T, V> = T extends (item: V, obj: infer R) => any ? R : never;
type A2 = Apply2< (item: string, obj: string[]) => any , string>;
type B2 = Apply2< <U>(item: U, obj: U[]) => any , string>;

type Apply3<T, V> = T extends (item: V, obj: infer R) => infer R ? R : never;
type A3 = Apply3< (item: string, obj: string[]) => string[] , string>;
type B3 = Apply3< <U>(item: U, obj: U[]) => U[] , string>;
{
	"compilerOptions": {
		"allowJs": true,
		"target": "es6",
		"module": "commonjs",
		"outDir": "dest",
		"strictNullChecks": true,
		"jsx": "preserve",
		"strictFunctionTypes": true
	}
}

Expected behavior:
All A and B types are inferred to string[]. For types B, the type parameter U should be unified with string.

Actual behavior:
All B types are inferred to {}[].

Related Issues:
#22615 (different problem, but similar input)

Activity

  1. ahejlsberg commented on Mar 21, 2018

    @ahejlsberg
    Member

    Repeating my comments from #22615 as they apply equally here:

    It is effectively a design limitation. We have the concept of instantiating a generic function type in the context of a non-generic function type (a form of unification), but we currently don't do that in conditional types. Instead, function type parameters are erased to their constraints and we infer from those. In the example above, that causes us to infer {} for the infer R type parameter.

    The proper fix for this would be have type inference perform instantiation of a source type in the context of a target type when the source type is a generic function type.

  2. SimonMeskens commented on Apr 20, 2018

    @SimonMeskens

    It's a design limitation, but Suggestion and Need proposal were added instead of closing. Does this mean there's a slim chance this could land eventually? This specific use case of a generic input type mapped onto the output is really common and this issue is stopping quite a few use cases from being typed properly.

  3. DuncanWalter commented on Aug 11, 2018

    @DuncanWalter

    Any new thoughts/insights? IMHO, now that v3.0 is out, this issue has become more interesting. It looks like this issue is the cause of this behavior:

    type BindConsumer<T, M extends <A>(a: A) => any> = 
      M extends ((a: T) => infer U)
        ? U
        : never
    
    const a: BindConsumer<number, <U>(a: U) => { boxed: U }> = {
      boxed: 2,  // infers the type: { boxed: {} }
    }

    If (strong if) this is a bug/potential feature, then fixing it will allow for all sorts of neat types like:

    // Can be used for function declarations like zip and even conditional mapping.
    // mapped types on tuples could also make this work.
    type MapTuple<Tuple extends any[], Mapping extends <T>(a: T) => any> = never // long and tedious
    
    // Good for all manner of composition/piping.
    // Folds the tuple's types into one by inferring the return of Join.
    // mapped types on tuples probably isn't good enough for this?
    type FoldTuple<Tuple extends any[], Join extends <T, U>(a: T, b: U) => any, Seed> = never // longer and more tedious

    Though, any type made this way can be expressed, albeit more verbosely.

  4. fazouane-marouane commented on Feb 4, 2019

    @fazouane-marouane

    Hi,

    I'll add a simple example related to this:

    type GenericReturnType<T, TInput> = T extends (_: TInput) => infer TResult? TResult: never;
    
    // Gives { x: {}; }
    type Box = <T>(_: T)=> { value: T };
    type _test1 = GenericReturnType<Box, number>;
    
    // Gives {x: number; }
    type AltBox<T> = (_: T)=> { value: T };
    type _test2 = GenericReturnType<AltBox<number> | AltBox<string>, number>;
  5. thomasmikava commented on Jul 18, 2020

    @thomasmikava

    Anders Hejlsberg (@ahejlsberg) The fix for this was planned to be in TypeScript 2.9 milestone, but then it has been removed (judging from #22615 history). Is there any plan to include this in the milestones of 4.1, 4.2 or higher versions? Since it might have many use cases for higher order functions

  6. thomasmikava commented on Jul 18, 2020

    @thomasmikava

    One of the use cases that can be achieved by fixing this limitation:

    const getRef = <T extends any>(val: T) => {
        return {
            current: val,
        };
    };
    
    const getWrapInValueFn = <Fn extends (arg: any) => any>(fn: Fn) => {
        return <A extends Parameters<Fn>[0]>(
            arg: A
        ): { value: Fn extends (arg: A) => infer R ? R : never } => {
            return {
                value: fn(arg),
            };
        };
    };
    const wrapInValue = getWrapInValueFn(getRef);
    const data = wrapInValue(5); // infsers { value: { current: unknown; }; } instead of { value: { current: number; }; }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Needs ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.SuggestionAn idea for TypeScript

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions