Skip to content

Incorrent return type inference when using infer with generic function #48820

Closed

Description

@user753

Bug Report

type ApplyFn<Fn extends (item: any) => any, T> =
  Fn extends (item: T) => infer R ? R : never

type X = ApplyFn<(x: number) => string, number>

type X is string

type Fn = <T>(item: T) => {foo: T}
type Y = ApplyFn<Fn, string>

type Y is {foo: unknown} but I have expected {foo: string}

🔎 Search Terms

infer generic function return type

⏯ Playground Link

Playground link with relevant code

Is there any workaround to solve this problem?

Activity

  1. craigphicks commented on Apr 23, 2022

    @craigphicks

    Probably not the level of generality you were hoping for but -

    type F1<T> = (item:T)=>{foo:T}
    type Z = ReturnType<F1<number>>
    
  2. user753 commented on Apr 24, 2022

    @user753
    Author

    Craig P Hicks (@craigphicks) thanks. But I don't think it can help me. I want to create map function for tuples

    type ApplyFn<Fn extends (item: any) => any, T> =
      Fn extends (item: T) => infer R ? R : never
    
    type Mapper<Tuple extends ReadonlyArray<any>, Fn extends (item: any) => any> =  {
      [Index in keyof Tuple]: ApplyFn<Fn, Tuple[Index]>
    }
    
    function tupleMap<T extends ReadonlyArray<any>,
      Fn extends (item: any) => any>(a: T, fn: Fn): Mapper<T, Fn> {
      return a.map(fn) as any
    }
  3. harry0000 commented on Apr 24, 2022

    @harry0000

    user753

    playground

    function tupleMap<T extends unknown, F extends (item: T) => any>(
      tuple: ReadonlyArray<T>,
      f: F
    ): ReadonlyArray<ReturnType<F>> {
      return tuple.map(f);
    }
    
    const map      = tupleMap([1, 2, 3], (n) => ({ foo: n }));
    const constMap = tupleMap([1, 2, 3] as const, (n) => ({ foo: n }));
  4. user753 commented on Apr 24, 2022

    @user753
    Author

    harry (@harry0000) thanks, but I want it to work like this

    const tuple = ["foo", "bar"] as const
    const convert = <T>(x: T): {item: T} => ({item: x})
    const convertedTuple: [{item: "foo"}, {item: "bar"}] = tupleMap(tuple, convert)
  5. harry0000 commented on Apr 25, 2022

    @harry0000

    user753 I recently answered the exact same question on Reddit, and my conclusion is no different than the answer to that question.

    https://www.reddit.com/r/typescript/comments/u7qcdv/is_it_possible_to_map_an_array_literal_to_another/

    You can create such a type definition, but the type information of the order of the constant array will be lost when you use Array.prototype.map(), so you will have to cast it with as in the end.

    playground

    const tuple = ["foo", "bar"] as const;
    
    type TupleMap<T extends ReadonlyArray<unknown>, R extends Array<unknown> = []> =
      T extends readonly [infer TH, ...infer TT]
        ? TupleMap<TT, TH extends string | number ? [...R, { item: `${TH}` }] : R>
        : R
    
    type FooBar = TupleMap<typeof tuple>
    // [{item: "foo"}, {item: "bar"}]
    
    const convertedTuple: FooBar = [
      { item: tuple[0] },
      { item: tuple[1] }
    ];
    
    const dont: FooBarItem = tuple.map(s => ({ item: s }));
    // Type '{ item: "foo" | "bar"; }[]' is not assignable to type '[{ item: "foo"; }, { item: "bar"; }]'.
    //  Target requires 2 element(s) but source may have fewer.(2322)

    And TS does not have Higher Kinded Types, you cannot write as below:

    #1213
    #44875

    type Item<T extends string | number> = { item: `${T}` }
    
    type TupleMap<T extends ReadonlyArray<unknown>, C extends Convert<_ extends string | number>, R extends Array<any> = []> =
      T extends readonly [infer TH, ...infer TT]
        ? TupleMap<TT, TH extends string | number ? [...R, C<TH>] : R>
        : R
    
    type FooBarItem = TupleMap<typeof tuple, Item>
    type Fn<T extends string | number> = (item: T) => any
    
    type ItemMapper<T extends string | number> = (item: T) => { item: `${T}` }
    
    type TupleMap<T extends ReadonlyArray<unknown>, F extends Fn, R extends Array<any> = []> =
      T extends readonly [infer TH, ...infer TT]
        ? TupleMap<TT, TH extends string | number ? [...R, ReturnType<F<TH>>] : R>
        : R
    
    type MappedFooBar = TupleMap<typeof tuple, ItemMapper>
  6. user753 commented on Apr 25, 2022

    @user753
    Author

    harry (@harry0000) Could you explain why we need Higher Kinded Types in this case?
    Why it's not possible to infer correct type?

    type ApplyFn<Fn extends (item: any) => any, T> =
      Fn extends (item: T) => infer R ? R : never
    type Fn = <T>(item: T) => {foo: T}
    type Y = ApplyFn<Fn, string>

    The compiler knows that Fn type is <T>(item: T) => {foo: T} and when it extends (item: T) => infer R it could substitute string into it and get a correct type (item: string) => {foo: string}

  7. harry0000 commented on Apr 25, 2022

    @harry0000

    The compiler knows that Fn type is <T>(item: T) => {foo: T}

    This definition of Fn is the definition of a function that takes any type T as an argument without any restrictions by extends. Therefore, it must be guaranteed to execute no matter what type is passed as an argument based on its own constraints (definition). T is inferred to be unknown within the type definition, but this is unknown because we do not know what is actually passed to the function. Also, it actually can take any types to the function argument.

    https://www.typescriptlang.org/docs/handbook/2/generics.html#generic-constraints

    declare const f: Fn;
    
    f();
    // Expected 1 arguments, but got 0.(2554)
    // const f: <unknown>(item: unknown) => {
    //   foo: unknown;
    // }
    
    type P = Parameters<Fn> // type P = [item: unknown]
    type R = ReturnType<Fn> // type R = { foo: unknown; }

    What you are actually trying to do is pass string to T in Fn, but writing Fn extends (item: string) => infer R does not pass string to T in Fn, so infer R returns { foo: unknown; }.

    type Fn = <T>(item: T) => { foo: T }
    
    // Since Fn can be executed even if a string type is passed, this "inheritance relationship" is valid.
    type Y = Fn extends (item: string) => infer R ? R : never;
    // type Y = { foo: unknown; }

    Since the inheritance relation check only checks whether the inheritance relation is satisfied, in order to determine T in Fn, a type parameter must be passed to Fn, which is not possible because there are no Higher Kinded Types.

    type Fn<T> = (item: T) => { foo: T }

    Edit:

    Why did you mistakenly think that you could pass any type to the type parameter of a generic function by checking the inheritance relationship with extends? From your experience with other programming languages? (just my interesting)

  8. user753 commented on Apr 25, 2022

    @user753
    Author

    harry (@harry0000) Thanks. Now I get it. The <T> was on the wrong side of the type and it should be

    type Fn<T> = (item: T) => {foo: T}
    

    And it is possible in some way to solve ApplyFn problem

    type ApplyFn<Fn extends (item: A) => R, T, A = any, R = any> =
      Fn extends (item: T) => infer R ? R : never
     
    type Y = ApplyFn<Fn<string>, string> 

    Do you know by any chance if it is possible to write tupleMap with HKT emulation? Like
    https://github.com/gcanti/fp-ts/blob/master/src/HKT.ts

    Why did you mistakenly think that you could pass any type to the type parameter of a generic function by checking the inheritance relationship with extends? From your experience with other programming languages? (just my interesting)

    I thought type Fn<T> = (item: T) => {foo: T} is the same as type Fn = <T>(item: T) => {foo: T} but I'm not sure why and I am still not sure why it is not.

  9. harry0000 commented on Apr 25, 2022

    @harry0000

    user753

    Do you know by any chance if it is possible to write tupleMap with HKT emulation?

    Unfortunately, no.

    If you use fp-ts, you can simply write the following, but as I mentioned before, the type information of the tuple order will be lost, so you will have to cast it with as in the end.

    playground

    import * as ROA from "fp-ts/lib/ReadonlyArray";
    
    const tuple = ["foo", "bar", "buz"] as const;
    
    const identity = <T>(i: T) => i;
    const ids = ROA.map(identity)(tuple);
    // const ids: readonly ("foo" | "bar" | "buz")[]
    
    const itemMapper = <T>(item: T) => ({ item });
    const items = ROA.map(itemMapper)(tuple);
    // const items: readonly {item: "foo" | "bar" | "buz"}[]

    Edit:

    Maybe I should have written this workaround from the beginning? Generic functions delay determining the type until the argument is passed, and currying can further delay that determination.

    playground

    const map: <A, B>(f: (a: A) => B) => (fa: ReadonlyArray<A>) => ReadonlyArray<B> = (f) => {
      return (fa) => fa.map(f);
    }
    
    const items = map(itemMapper)(tuple);
    // const items: readonly {item: "foo" | "bar" | "buz"}[]
    
    const m = map(itemMapper);
    // const m: <T>(fa: readonly T[]) => readonly { item: T; }[]
    
    type P = Parameters<typeof m> // type P = [fa: readonly unknown[]]
    type R = ReturnType<typeof m> // type R = readonly { item: unknown; }[]

    However, if no arguments have been passed yet (the type has not yet been determined: including references in the type definition), it will be unknown (See also f(), type P, and type R in the previous answer.)

  10. locked as resolved and limited conversation to collaborators on Oct 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions