Type Inference in TypeScript
TypeScript can infer (and then check) the type of a variable based on a few simple rules. Because these rules are simple you can train your brain to recognize safe / unsafe code (it happened for me and my team mates quite quickly).
The types flowing is just how I imagine in my brain the flow of type information.
Definition
Types of a variable are inferred by definition.
let foo = 123; // foo is a `number`
let bar = "Hello"; // bar is a `string`
foo = bar; // Error: cannot assign `string` to a `number`
This is an example of types flowing from right to left.
Return
The return type is inferred by the return statements e.g. the following function is inferred to return a number
.
function add(a: number, b: number) {
return a + b;
}
This is an example of types flowing bottom out.
Assignment
The type of the function parameters / return can also be inferred by assignment e.g. here we say that foo
is an Adder
, that makes the type of a
and b
to infer as number
.
type Adder = (a: number, b: number) => number;
let foo: Adder = (a, b) => a + b;
This fact can be demonstrated by the below code which raises an error as you would hope:
type Adder = (a: number, b: number) => number;
let foo: Adder = (a, b) => {
a = "hello"; // Error: cannot assign `string` to a `number`
return a + b;
}
This is an example of types flowing from left to right.
The same assignment style type inference works if you create a function for a callback argument. After all an argument -> parameter
is just another form of variable assignment.
type Adder = (a: number, b: number) => number;
function iTakeAnAdder(adder: Adder) {
return adder(1, 2);
}
iTakeAnAdder((a, b) => {
// a = "hello"; // Would Error: cannot assign `string` to a `number`
return a + b;
})
Structuring
These simple rules also work in the presence of structuring (object literal creation). For example in the following case the type of foo
is inferred to be {a:number, b:number}
let foo = {
a: 123,
b: 456
};
// foo.a = "hello"; // Would Error: cannot assign `string` to a `number`
Similarly for arrays:
const bar = [1,2,3];
// bar[0] = "hello"; // Would error: cannot assign `string` to a `number`
And ofcourse any nesting:
let foo = {
bar: [1, 3, 4]
};
foo.bar[0] = 'hello'; // Would error: cannot assign `string` to a `number`
Destructuring
And of course, they also work with destructuring, both objects:
let foo = {
a: 123,
b: 456
};
let {a} = foo;
// a = "hello"; // Would Error: cannot assign `string` to a `number`
and arrays:
const bar = [1, 2];
let [a, b] = bar;
// a = "hello"; // Would Error: cannot assign `string` to a `number`
And if the function parameter can be inferred, so can its destructured properties. For example here we destructure the argument into its a
/b
members.
type Adder = (numbers: { a: number, b: number }) => number;
function iTakeAnAdder(adder: Adder) {
return adder({ a: 1, b: 2 });
}
iTakeAnAdder(({a, b}) => { // Types of `a` and `b` are inferred
// a = "hello"; // Would Error: cannot assign `string` to a `number`
return a + b;
})
Type Guards
We have already seen how Type Guards help change and narrow down types (particularly in the case of unions). Type guards are just another form of type inference for a variable in a block.
Warnings
Be careful around parameters
Types do not flow into the function parameters if it cannot be inferred from an assignment. e.g. in the following case the compiler does not to know the type of foo
so it cannot infer the type of a
or b
const foo = (a,b) => { /* do something */ };
However if foo
was typed the function parameters type can be inferred (a
,b
are both inferred to be number below).
type TwoNumberFunction = (a: number, b: number) => void;
const foo: TwoNumberFunction = (a, b) => { /* do something */ };
Be careful around return
Although TypeScript can generally infer the return type of a function, it might not be what you expect. e.g. here function foo
has a return type of any
function foo(a: number, b: number) {
return a + addOne(b);
}
// Some external function in a library someone wrote in JavaScript
function addOne(a) {
return a + 1;
}
This is because the return type is impacted by the poor type definition for addOne
(a
is any
so the return of addOne
is any
so the return of foo
is any
).
I find it simplest to always be explicit about function / returns. After all these annotations are a theorem and the function body is the proof.
There are other cases that one can imagine, but the good news is that there is a compiler flag that can help catch such bugs.
noImplicitAny
There is a boolean compiler flag noImplicitAny
where the compiler will actually raise an error if it cannot infer the type of a variable (and therefore can only have it as an implicit any
type). You can then
- either say that yes I want it to be an
any
by explicitly adding an: any
type annotation - help the compiler out by adding a few more correct annotations.