Typescript: Type Inference on function arguments
Tags: typescript, javascript, tutorial
Published: 2021-07-18 11:34:53

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:
```typescript
const foo = <T,>(a: T) => a

// const foo: <42>(a: 42) => 42
foo(42)
```
`T` generic parameter was infered to `42` which is perfectly fine.

Now, try to pass an object: `{a: 42}`:
```typescript
const foo = <T,>(a: T) => a

// const foo: <{ a: number; }> (a: { a: number; }) => { a: number; }
foo({ a: 42 })
```
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`.
```typescript
foo({ a: 42 })
```
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.
```typescript
const foo = <Value, T extends { a: Value }>(a: T) => a

// const foo: <{ a: number; }> (a: { a: number; }) => { a: number; }
foo({ a: 42 })
```
As you might havev noticed it still does not work. In order to make it work you need apply extra restrictions to `Value` generic.
```typescript
const foo = <Value extends number, T extends { a: Value }>(a: T) => a

// const foo: <{ a: 42; }> (a: { a: 42; }) => { a:42; }
foo({ a: 42 })
```
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:
```typescript
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' })
```
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.
```typescript

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' })
```
If you want to infer an Array instead of Record, you can do this:
```typescript

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' }])
```

If your array consists of homogeneous data, you can use variadic tuples:
```typescript
const foo = <
  V extends number,
  A extends { a: V }[]
>(a: [...A]) => a

foo([{ a: 1 }])
```

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:
```typescript
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
});
```
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.
{% stackoverflow 65550146 %}


To make it work, you should just add extra generic:
```typescript
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!
});
```
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):
```typescript
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'});
```
Seems, it works perfectly fine. Now, try to add an argument into `itemCreator` callback.
```typescript
const create = new Store<Person>()
  .itemCreator((a) => ({id: 'ID', extra: 42}));

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

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

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

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

{% github https://github.com/microsoft/TypeScript/issues/25092 %}

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.