Type Inference in TypeScript

    Types of a variable are inferred by definition.

    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.

    1. function add(a: number, b: number) {
    2. return a + b;
    3. }

    This is an example of types flowing bottom out.

    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.

    1. type Adder = (a: number, b: number) => number;
    2. let foo: Adder = (a, b) => a + b;

    This fact can be demonstrated by the below code which raises an error as you would hope:

    1. type Adder = (a: number, b: number) => number;
    2. a = "hello"; // Error: cannot assign `string` to a `number`
    3. return a + b;
    4. }

    The same assignment style type inference works if you create a function for a callback argument. After all an argument -> parameteris just another form of variable assignment.

    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}

    1. let foo = {
    2. a: 123,
    3. b: 456
    4. };

    Similarly for arrays:

    1. const bar = [1,2,3];
    2. // bar[0] = "hello"; // Would error: cannot assign `string` to a `number`

    And of course any nesting:

    1. let foo = {
    2. bar: [1, 3, 4]
    3. };
    4. foo.bar[0] = 'hello'; // Would error: cannot assign `string` to a `number`

    And of course, they also work with destructuring, both objects:

    and arrays:

    1. const bar = [1, 2];
    2. let [a, b] = bar;

    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.

    1. type Adder = (numbers: { a: number, b: number }) => number;
    2. function iTakeAnAdder(adder: Adder) {
    3. return adder({ a: 1, b: 2 });
    4. }
    5. iTakeAnAdder(({a, b}) => { // Types of `a` and `b` are inferred
    6. // a = "hello"; // Would Error: cannot assign `string` to a `number`
    7. return a + b;
    8. })

    Type Guards

    Types do not flow into the function parameters if it cannot be inferred from an assignment. For example in the following case the compiler does not know the type of foo so it cannot infer the type of a or b.

    1. const foo = (a,b) => { /* do something */ };

    However if was typed the function parameters type can be inferred (a,b are both inferred to be of type number in the example below).

    Be careful around return

    Although TypeScript can generally infer the return type of a function, it might not be what you expect. For example here function foo has a return type of any.

    1. function foo(a: number, b: number) {
    2. return a + addOne(b);
    3. }
    4. // Some external function in a library someone wrote in JavaScript
    5. function addOne(a) {
    6. return a + 1;
    7. }

    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

    The flag noImplicitAny instructs the compiler to 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

    • help the compiler out by adding a few more correct annotations.