Grokking TypeScript
Vote

0% completed

​

Implicit and Explicit Type Setting

Explicit Type Setting

Implicit Type Setting

Where Inference Has No Value to Use

Parameters Are Not Inferred From Calls

Which Type Setting Should You Use?

Key Takeaways

These two declarations give the compiler the same result:

let price: number = 20; let quantity = 3;

Both variables have the type number. In the first line you stated the type yourself, which is called explicit typing. In the second line the compiler worked out the type from the value, which is called implicit typing or type inference. The question for this lesson is when each form is the better choice.

Explicit Type Setting

To set a type explicitly, write a colon and the type after the name. This written type is called a type annotation:

let age: number = 25; let username: string = "John"; let isLoggedIn: boolean = true;

The compiler then enforces that type on the variable, so an accidental mismatch becomes an error. Explicit types are most useful on function parameters and return values:

function greetUser(customer: string): string { return `Hello, ${customer}!`; } console.log(greetUser("Alice")); // Allowed console.log(greetUser(123)); // Error
greet.ts(6,23): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.

By stating the input and output types, you tell every caller what the function needs. The compiler also checks the function body against the return type that you declared.

Implicit Type Setting

When a variable has an initial value, the compiler infers its type from that value:

let count = 10; // Inferred as number let message = "Hello"; // Inferred as string let isActive = false; // Inferred as boolean

An inferred type is not weaker than an annotated one. It gives the same checks, with less to write:

let count = 10; count = "ten";
count.ts(2,1): error TS2322: Type 'string' is not assignable to type 'number'.

Where Inference Has No Value to Use

Inference needs something to read, so it behaves differently when there is no initial value. A variable declared without a value or an annotation starts with the type any, which means that the compiler does not check it. In strict mode, however, the compiler then follows the variable through your code and uses the type of the last value assigned:

let data; data = 42; data.toFixed(2); // Allowed: data is a number here data = "Hello"; // Allowed: the type changes again data.toUpperCase(); // Allowed: data is a string here data.toFixed(2); // Error
data.ts(6,6): error TS2551: Property 'toFixed' does not exist on type 'string'. Did you mean 'fixed'?

The picture below follows data through these lines. Notice that the assignments themselves are never errors, because the compiler has no declared type to compare them with. Only the last use of data fails.

The type of data changes from any to number to string at each assignment, and the last call to toFixed is an error.
The type of data changes from any to number to string at each assignment, and the last call to toFixed is an error.

The compiler follows the variable through the surrounding code, including if branches and loops. It cannot follow the variable into another function, because it cannot know which assignment ran before that function was called, so it reports an error:

let later; function readLater() { return later; } later = 1;
later.ts(1,5): error TS7034: Variable 'later' implicitly has type 'any' in some locations where its type cannot be determined. later.ts(4,10): error TS7005: Variable 'later' implicitly has an 'any' type.

These protections depend on strict mode. With --strict false, the data example above compiles with no errors, and its last line crashes at runtime with TypeError: data.toFixed is not a function. To avoid the problem entirely, give the variable a type or an initial value. A declared type also makes the compiler check that you assign a value before you read it:

let total: number; console.log(total);
total.ts(2,13): error TS2454: Variable 'total' is used before being assigned.

Parameters Are Not Inferred From Calls

A function parameter usually has no initial value, and the compiler does not guess its type from the calls to the function. In strict mode, a parameter with neither a type nor a default value is therefore an error:

function greetUser(customer) { return `Hello, ${customer}!`; }
greet.ts(1,20): error TS7006: Parameter 'customer' implicitly has an 'any' type.

The chapter on functions covers the two exceptions: a default value, which gives the parameter its type, and a callback passed to another function, whose parameter types come from that function.

Which Type Setting Should You Use?

Use explicit types on function parameters, on return values, and on objects with complex structures. Most parameters require them, and the other two gain clarity from them. Also use them when a variable receives its value later, because the compiler has nothing to infer from at the declaration.

Use implicit types when the assigned value is clear and self-explanatory, because the compiler can usually determine the correct type. An annotation there only repeats what the value already says:

let city: string = "New York"; // The annotation adds nothing

Instead, let the compiler infer the type:

let city = "New York"; // Inferred as string

Key Takeaways

  • An inferred type gets the same checks as an annotated type.
  • Without an initial value or an annotation, a variable starts as any, and strict mode then follows its assignments.
  • Parameters are never inferred from calls, so most of them need annotations in strict mode.
  • Annotate function boundaries, and let the compiler infer local variables that have clear values.

Reading Progress

0%


Vote for new content

On This Page

Explicit Type Setting

Implicit Type Setting

Where Inference Has No Value to Use

Parameters Are Not Inferred From Calls

Which Type Setting Should You Use?

Key Takeaways