Typescript: Type Inference on function arguments
yossarian
Posted on July 18, 2021
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)
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 })
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 })
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 })
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 })
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' })
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' })
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' }])
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 }])
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
});
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!
});
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'});
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
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
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()
});
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)
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.
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
August 31, 2024