Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
user753 opened this issue Apr 23, 2022 · 9 comments
Closed

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

user753 opened this issue Apr 23, 2022 · 9 comments

Comments

@user753
Copy link

user753 commented Apr 23, 2022

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?

@craigphicks
Copy link

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

type F1<T> = (item:T)=>{foo:T}
type Z = ReturnType<F1<number>>

@user753
Copy link
Author

user753 commented Apr 24, 2022

@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
}

@harry0000
Copy link

@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 }));

@user753
Copy link
Author

user753 commented Apr 24, 2022

@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)

@harry0000
Copy link

harry0000 commented Apr 25, 2022

@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>

@user753
Copy link
Author

user753 commented Apr 25, 2022

@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}

@harry0000
Copy link

harry0000 commented Apr 25, 2022

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)

@user753
Copy link
Author

user753 commented Apr 25, 2022

@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.

@harry0000
Copy link

harry0000 commented Apr 25, 2022

@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.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants