简体   繁体   中英

Typescript generic infer of nested function

I have these ts function:

const fnGeneric = <V,I>(fn:<U>(param:U) => V, param:I) => fn(param);
const fn = (some:string) =>  some;
const result = fnGeneric(fn,5);

but result ends with static type error :

Argument of type '(some: string) => string' is not assignable to parameter of type '(param: U) => string'. Types of parameters 'some' and 'param' are incompatible. Type 'U' is not assignable to type 'string

What is wrong with this pattern? I think U should infer I type as number, but I have some blank space here.

I think your definition is off, the types will not be inferred automatically to fit your intent. This is the definition i would use:

const fnGeneric = <V,U>(fn: (param:U) => V, param: U) => fn(param);
const fn = (some: string) =>  some;
const result = fnGeneric(fn, 5);

This uses the same type parameter U both in the function and the second argument, ensuring their compatibility. Of course the call will be invalid then, because fn only accepts strings as argument.

The reason for this becomes more clear if you look at the type signature of fnGeneric as it would appear in a .d.ts file.

declare const fnGeneric: <V,I>(fn:<U>(param:U) => V, param:I) => V

Does not relate U and I in any way. Here is what the type checker can infer from this signature:

  1. fnGeneric is a function defined for all types V , and I , receiving 2 parameters fn and param and returning the type V
  2. fn is a function defined for all types U receiving 1 parameter param , of type U and returning V
  3. param is of type I

This type-checks fine, because fn is a function that can turn any type into a V , so passing it an I in the definition fn(param) is perfectly acceptable.

The problems start when you try to call this function. In order to call fnGeneric , you need to give is a function that can take any type , and turn it into the type you want to get out of fnGeneric . This is impossible! Passing in a const fn: (some: string) => string like in your example doesn't work, becase fn doesn't accept any type, it only accepts strings. The signature would need to be const fn: <U>(some: U) => string . Hence the error:

Argument of type '(some: string) => string' is not assignable to parameter of type '(param: U) => string'. Types of parameters 'some' and 'param' are incompatible. Type 'U' is not assignable to type 'string

Additionally, based on your example fnGeneric(fn, 5) , you are trying to pass a number to a function that takes a string by coercing fn to be a function that takes any type of argument. Here is a code snippet that type checks:

const fnGeneric = <A>(fn: <B>(param: B) => B, param: A): A => fn(param);
const fn = <A>(some: A): A => some;
const result: number = fnGeneric(fn, 5); // result = 5

fn in this example is commonly known as the identity function - which returns the parameter it is given, and it is the only valid implementaion (without arbitrary type casting) of a function with the signature <A>(a: A) => A . By extension, fnGeneric is a function that takes the identity function and a parameter of any type, and returns the application of the identity function to that parameter.

What is wrong with this pattern?

fnGeneric as defined doesn't make any sense, it would be impossible to write a program that type checks that satisfies the signature. The only way to make the types work simply makes it redundant. There aren't any cases where it would be preferable to call fnGeneric(identity, x) instead of just identity(x) (or for that matter, simply the expression x ). They are all completely equivalent.

i can give u more realistic scenario, i made simplier version of fn, there is real:

export const inject = <I,V>(fn:<U>(input?:U) => V, resolveWithPayload: boolean, resolveArgs?: I) => <R>(payload:R):R => { resolveWithPayload ? fn(payload) : resolveArgs ? fn(resolveArgs) : fn(); return payload; };

const fn = (value:number):number => {
    propertyToMutate = value;
    return propertyToMutate;
}

const res =_fish.inject(fn,false,60)(50);

but calling it ends with :

Argument of type '(value: number) => number' is not assignable to parameter of type '(input?: U) => number'. Types of parameters 'value' and 'input' are incompatible. Type 'U' is not assignable to type 'number'.

if i improve code by your way:

export const inject = <I,V,U>(fn:(input?:U) => V, resolveWithPayload: boolean, resolveArgs?: I) => <R>(payload:R):R => { resolveWithPayload ? fn(payload) : resolveArgs ? fn(resolveArgs) : fn(); return payload; };

it ends by type definition error in inject itself like:

TS2345: Argument of type 'R' is not assignable to parameter of type 'U'.

TS2345: Argument of type 'I' is not assignable to parameter of type 'U'.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM