Typescript: Type Inference on function arguments

captainyossarian

yossarian

Posted on July 18, 2021

Typescript: Type Inference on function arguments

Sorry, no picture :)

In this article I will show you simple and very useful examples of type inference in TypeScript.

Part #1

Imagine you want to infer not just number type but a literal representation.
Consider next example:



const foo = <T,>(a: T) => a

// const foo: <42>(a: 42) => 42
foo(42)


Enter fullscreen mode Exit fullscreen mode

T generic parameter was infered to 42 which is perfectly fine.

Now, try to pass an object: {a: 42}:



const foo = <T,>(a: T) => a

// const foo: <{ a: number; }> (a: { a: number; }) => { a: number; }
foo({ a: 42 })


Enter fullscreen mode Exit fullscreen mode

This is not really what we want. We want to infer {a: 42}, not just {a: number}.

You have at least two options how to do it.
First, you can just annotate your object as immutable value. I mean as const.



foo({ a: 42 })


Enter fullscreen mode Exit fullscreen mode

It works, but sometimes you are not allowed to use immutable values.
Second option is much better.
You can add extra generic yo annotate the value.



const foo = <Value, T extends { a: Value }>(a: T) => a

// const foo: <{ a: number; }> (a: { a: number; }) => { a: number; }
foo({ a: 42 })


Enter fullscreen mode Exit fullscreen mode

As you might havev noticed it still does not work. In order to make it work you need apply extra restrictions to Value generic.



const foo = <Value extends number, T extends { a: Value }>(a: T) => a

// const foo: <{ a: 42; }> (a: { a: 42; }) => { a:42; }
foo({ a: 42 })


Enter fullscreen mode Exit fullscreen mode

Now, it works as expected.
I know that you did not like last example. What if you want to pass an object with multiple keys. According to my example, you need to annotate each key then.

Consider next example:



const foo = <
  Key extends PropertyKey,
  Value extends number | string,
  T extends Record<Key, Value>
>(a: T) => a


// const foo: <PropertyKey, string | number, { a: 42; b: "hello";}> 
foo({ a: 42, b: 'hello' })


Enter fullscreen mode Exit fullscreen mode

Now, I don't like this example, because my values are restricted to string and number types.
Instead of using string | number as a value type, we can use Json type.




type Json =
  | null
  | string
  | number
  | boolean
  | Array<JSON>
  | {
    [prop: string]: Json
  }

const foo = <
  Key extends PropertyKey,
  Value extends Json,
  T extends Record<Key, Value>
>(a: T) => a

// const foo: <PropertyKey, Json, { a: 42; b: "hello"; }
foo({ a: 42, b: 'hello' })


Enter fullscreen mode Exit fullscreen mode

If you want to infer an Array instead of Record, you can do this:




type Json =
  | null
  | string
  | number
  | boolean
  | Array<JSON>
  | {
    [prop: string]: Json
  }

const foo = <
  Key extends PropertyKey,
  Value extends Json,
  T extends Record<Key, Value>[]
>(a: T) => a

// const foo: <PropertyKey, Json, { a: 42; b: "hello"; }
foo([{ a: 42, b: 'hello' }])


Enter fullscreen mode Exit fullscreen mode

If your array consists of homogeneous data, you can use variadic tuples:



const foo = <
  V extends number,
  A extends { a: V }[]
>(a: [...A]) => a

foo([{ a: 1 }])


Enter fullscreen mode Exit fullscreen mode

You may ask me, why I even need to infer literal type?
Because sometimes we want to validate our arguments. See my
previous article or my blog.
Imagine that you want to disallow value if it equals 1.
Try to implement this validation rule.

Part 2
What about functions ?
Consider next example:



const fn = <T,>(
  arg: {
    a: (a_arg: number) => T;
    b: (b_arg: T) => void
  }
) => null;

fn({
  a: (arg1) => ({ num: 0 }),
  b: (arg2 /** unknown */) => {
    arg2.num;
  }, // Error
});


Enter fullscreen mode Exit fullscreen mode

It is obvious that argument of b method/callback/function should have return type of a. But, TS infers it as unknown.

Here you can find good explanation.

My question is based on this question and answer

Let's say we have next code:

const myFn = <T,>(p: {
  a: (n: number) => T
  b: (o: T) => void,
}) => {
  // ...
}


myFn({
  a: () => ({ n: 0 }), // Parameter of a is ignored

To make it work, you should just add extra generic:



const myFn = <T,>(arg: {
  a: (a_arg: number) => T;
  b: <U extends T>(b_arg: U) => void;
}) => {
  // ...
};

myFn({
  a: (a) => ({ num: 0 }),
  b: (b_arg) => {
    b_arg.num;
  }, // Works!
});


Enter fullscreen mode Exit fullscreen mode

So, if you don't know how to infer smth, always start with adding extra generics.

If an extra generic does not help you, try to add default generic value.

Consider this example (my favourite):



class Store<T> {
  itemCreator<U>(
    generate: (item: Omit<T, keyof U>) => U
  ): (item: Omit<T, keyof U>) => Omit<T, keyof U> & U {
    return item => ({...item, ...generate(item)});
  }
}

type Person = {
  id: string;
  name: string;
  email: string;
  age?: number;
};

const create = new Store<Person>()
  .itemCreator(() => ({id: 'ID', extra: 42}));

const person = create({name: 'John', email: 'john.doe@foo.com'});


Enter fullscreen mode Exit fullscreen mode

Seems, it works perfectly fine. Now, try to add an argument into itemCreator callback.



const create = new Store<Person>()
  .itemCreator((a) => ({id: 'ID', extra: 42}));

const person = create({name: 'John', email: 'john.doe@foo.com'}); // error



Enter fullscreen mode Exit fullscreen mode

This example drives me crazy.
In order to fix it, you just need to move Omit<T, keyof U> outside the function:




class Store<T> {

    itemCreator<U>(
      // here I have used extra generic with default value
        generate: <P = Omit<T, keyof U>>(item: P) => U
    ): (item: Omit<T, keyof U>) => Omit<T, keyof U> & U {
        return item => ({ ...item, ...generate(item) });
    }
}


type Person = {
  id: string;
  name: string;
  email: string;
  age?: number;
};

const create = new Store<Person>()
  .itemCreator((a) => ({id: 'ID', extra: 42}));

const person = create({name: 'John', email: 'john.doe@foo.com'}); // ok



Enter fullscreen mode Exit fullscreen mode

In order to understand function types, you should be aware of TypeScript contextual typing and context sensitive functions.

Can't correctly infer generic interface type when it's behind a function #25092

TypeScript Version: 2.9

Search Terms: function parameter inference

Code

interface MyInterface<T> {
    retrieveGeneric: (parameter: string) => T,
    operateWithGeneric: (generic: T) => string
}

const inferTypeFn = <T>(generic: MyInterface<T>) => generic;

// inferred type for myGeneric = MyInterface<{}>, `generic.toFixed()` marked as error (as {} doesn't have .toFixed())
const myGeneric = inferTypeFn({
    retrieveGeneric: parameter => 5,
    operateWithGeneric: generic => generic.toFixed()
});

// inferred type for myGeneric = MyInterface<number>, everything OK
const myWorkingGeneric = inferTypeFn({
    retrieveGeneric: (parameter: string) => 5,
    operateWithGeneric: generic => generic.toFixed()
});
Enter fullscreen mode Exit fullscreen mode

Expected behavior: myGeneric has every type correctly inferred, parameter is a string, generic is a number.

Actual behavior: it doesn't infer the correct type for generic parameter unless you manually specify the type of parameter (which it already had the right type)

Playground Link:
https://www.typescriptlang.org/play/#src=interface%20MyInterface%3CT%3E%20%7B%0D%0A%20%20%20%20retrieveGeneric%3A%20(parameter%3A%20string)%20%3D%3E%20T%2C%0D%0A%20%20%20%20operateWithGeneric%3A%20(generic%3A%20T)%20%3D%3E%20string%0D%0A%7D%0D%0A%0D%0Aconst%20inferTypeFn%20%3D%20%3CT%3E(generic%3A%20MyInterface%3CT%3E)%20%3D%3E%20generic%3B%0D%0A%0D%0A%2F%2F%20inferred%20type%20for%20myGeneric%20%3D%20MyInterface%3C%7B%7D%3E%2C%20%60generic.toFixed()%60%20marked%20as%20error%20(as%20%7B%7D%20doesn't%20have%20.toFixed())%0D%0Aconst%20myGeneric%20%3D%20inferTypeFn(%7B%0D%0A%20%20%20%20retrieveGeneric%3A%20parameter%20%3D%3E%205%2C%0D%0A%20%20%20%20operateWithGeneric%3A%20generic%20%3D%3E%20generic.toFixed()%0D%0A%7D)%3B%0D%0A%0D%0A%2F%2F%20inferred%20type%20for%20myGeneric%20%3D%20MyInterface%3Cnumber%3E%2C%20everything%20OK%0D%0Aconst%20myWorkingGeneric%20%3D%20inferTypeFn(%7B%0D%0A%20%20%20%20retrieveGeneric%3A%20(parameter%3A%20string)%20%3D%3E%205%2C%0D%0A%20%20%20%20operateWithGeneric%3A%20generic%20%3D%3E%20generic.toFixed()%0D%0A%7D)%3B%0D%0A%0D%0A

YOu can find the links inside linked SO question/answer.

Summary
1) If you don't know how to infer something, add extra generic
2) If it still does not work, try to add default values
There is high probability that such approach will help you.

Thanks.

💖 💪 🙅 🚩
captainyossarian
yossarian

Posted on July 18, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related