Implicit typing
- types get inferred where ever possible, e.g. in variable assignment, return statement of function, object properties, array elements, etc.
- don’t need to provide types for everything, but safer so can not later accidentally change them, and use them for documentation
let age = 42;
age.toUpperCase();
- the declared type determines the types that are allowed to be assigned, even if in meantime had a more specific type
let x = Math.random() < 0.5 ? 42 : "world";
x = 1;
x = "goodbye!";
- variable declarations with
const
infer literal type, i.e. const x = 42
is inferred to be of type 42
not of type number
❗️
- variable declarations with
let
and var
infer general type, i.e. let x = 42
is inferred to be of type number
not of type 42
❗️
let x = 42;
let y: 42 = 42;
x = 21;
y = 21;
readonly
properties infer the literal type, similarly to const
variable declarations
Structural typing
- type compatibility is checked using duck typing (“If it walks like a duck and quacks like a duck, it is a duck!“)
- an object type is compatible with another, as long as it implements at least all properties of the other
interface Named {
name: string;
}
function sayHi(obj: Named) {
console.log(`Hi, my name is ${obj.name}.`);
}
const p = { name: "Peter", age: 42 };
sayHi(p);
interface Named {
name: string;
}
class Person {
name: string;
age: number;
constructor(name, age) {
this.name = name;
this.age = age;
}
}
const q: Named = new Person("Peter", 42);
- excess property checking: object literals aren’t allowed to have more properties than specified when assigning them directly to variables or passing them as arguments to functions, often source of bugs, can allow by assigning it to new variable with type inference and using that variable
interface Named {
name: string;
}
function sayHi(obj: Named) {
console.log(`Hi, my name is ${obj.name}.`);
}
sayHi({ name: "Peter", age: 42 });
const p = { name: "Peter", age: 42 };
sayHi(p);
interface Named {
name: string;
}
const q: Named = { name: "Peter", age: 42 };
const p = { name: "Peter", age: 42 };
const q: Named = p;
- if all properties are optional, must at least implement one, i.e. not none
interface Named {
gender?: string;
}
const p = { name: "Peter", age: 42 };
const q: Named = p;
- a function type is compatible with another, as long as the other implements at least all parameter types of it and the other type’s return type is a subtype of its return type, the parameter names can be different
let f = (a: number) => ({name: "Peter", age: 42});
let g = (x: number, y: string) => ({name: "Peter"});
g = f;
f = g;
- function parameter bivariance: parameter types of function types are compatible, as long as one is assignable to the other, using
strictFunctionTypes
flag only if parameter type of source is assignable to parameter type of target
interface Named {
name: string;
}
interface Person {
name: string;
age: number;
}
let f = (a: Named) => {};
let g = (b: Person) => {};
g = f;
f = g;
- classes are compatible, if their instance object types are compatible, i.e. static properties and the constructor are ignored ❗️
- beware: classes containing private or protected instance properties are not compatible, only if those instance properties originate from the same class
Control Flow Analysis
- TS narrows down types to more specific types when it can
- in if statements, switch statements, loops, conditional ternaries, etc.
- using type guards in condition, e.g.
typeof
, instanceof
, equality operator, truthiness, see Type operators
- beware: TS doesn’t catch all edge cases of JS, needs to still understand JS ❗️
- can check if type is
never
to not forget a code path, e.g. a missing case in switch statement, if…else statements, etc.
function assertNever(object: never): never {
throw new Error("You forgot to handle a control flow path.")
}
type Shape = { kind: "circle" } | { kind: "rectangle" } | { kind: "square" };
declare let shape: Shape;
switch (shape.kind) {
case "circle":
break;
case "rectangle":
break;
default:
assertNever(shape);
}
Resources