Typescript overview
Use typescript to add static typing to your codebase
- it highlights unexpected behavior in code through static typing and compilation errors, for example :
const message = "Hello World!";
// run a built-in method
message.toLowerCase();
// throws TypeError: message is not a function
message();
- ECMA's
typeof
andinstanceof
checks are too limitative to prevent such errors from happening at runtime, thus the need to identify them at compile time - it is not really a language, rather a linter on steroids that enforces strict static typing through code and dependencies
- to some extent, it was designed to strong arm developers into using VSCode since TypeScript is fully integrated in it
- VERY IMPORTANT: much of the time, you will know better than TypeScript.
- when creating a variable and assigning a value, the value type becomes the variable type (inferred types : avoid and be explicit instead)
- object types can be described with the
interface
keyword (type signature definition) and objects can be extended from it, a la java
interface User {
name: string;
id: number;
};
const user: User = {
name: "Hayes",
id: 0
};
- if an extended object does not match its
interface
properties names / types the compiler will throw an error -
interface
properties can be defined as optional using?:
(note that doing so addsundefined
to the property accessor signature) :
interface User {
name: string;
id?: number;
};
// compiler doesn't throw
const user: User = {
name: "Hayes"
};
-
interface
can be used to constrain functions returned values types (be it constructor functions or standard functions) - the following ECMA primitive types are supported :
boolean
,bigint
,null
,number
,string
,symbol
, andundefined
- TS adds supports the additional following primitive types :
type | description |
---|---|
any |
anything is allowed |
all further type checking is disabled, and it is assumed that you know the environment better than TypeScript. | |
unknown |
stricter version of any in the sense that that type definition will only accept a typed value |
never |
type representing something that cannot happen, usage is to constrain the code flow and identify possible errors |
no value ever can be assigned to a never type: compiler will throw an error if the code makes it possible |
|
void |
similar to never , but representing something that can only be assigned undefined (not even null ) |
- unions :
- used to constrain possible types for a value or a variable :
// using a composing types on the possible values
type PositiveOddNumbersUnderTen = 1 | 3 | 5 | 7 | 9;
// using a composing type on the accepted arguments
function getLength(obj: string | string[]) {
return obj.length;
}
// using a generic on the returned type
function wrapInArray(obj: string | string[]): Array<string> {
if (typeof obj === "string") {
return [obj];
}
return obj;
}
✔️ Note : TypeScript will only allow operations that are valid for every type (ie. methods that are only available on string can't be used on the union string | number
). The solution to this is code narrowing :
function printId(id: number | string) {
if (typeof id === "string") {
// in this branch, id is of type 'string'
console.log(id.toUpperCase());
} else {
// here, id is of type 'number'
console.log(id);
}
}
✔️ Note : It might be confusing that a union of types appears to have the intersection of those types’ properties. This is not an accident - the name union comes from type theory. The union number | string
is composed by taking the union of the values from each type. Notice that given two sets with corresponding facts about each set, only the intersection of those facts applies to the union of the sets themselves.
- generics
- use the
Type
keyword to declare an alias for the given type - types definition are allowed to use variables / interfaces / other types by using the
<Type>
syntax a la java to describe and constrain types - this adds flexibility to derive different object signatures from the same
interface
- use the
// constrain array values to be strings only
type StringArray = Array<string>;
// constrain array values to match interface or object signature
type ObjectWithNameArray = Array<{ name: string }>;
// create a flexible interface
interface Backpack<Type> {
add: (obj: Type) => void;
get: () => Type;
}
// the constant's "add" method will only accept string values, compiler will throw an error otherwise ...
declare const backpack: Backpack<string>;
- the type checking leverages object's "shapes" a la V8 inline cache
- an object's shape is its signature : number and type and order of definition of its properties
- multiple objects with the same shape will implicitly be considered as being of the same type, without the compiler throwing
- the above applies to TS interfaces as well as to ECMA object literals or class instances
- once again, avoid relying on this and be as explicit as possible. it's more interesting to organize the whole codebase around types
// signature / shape / interface definition
interface Point {
x: number;
y: number;
}
// function with constrained argument
function logPoint(p: Point): void {
console.log(`${p.x}, ${p.y}`);
}
// explicit
const p1: Point = { x: 12, y: 26 };
logPoint(p1);
// shape subset, implicit, compiler accepts
const p2 = { x: 12, y: 26, z: 89 };
logPoint(p2);
// shape subset, explicit, compiler throws
const rect: Point = { x: 33, y: 3, width: 30, height: 80 };
logPoint(rect);
// different shape, compiler throws
const color = { hex: "#187ABF" };
logPoint(color);
// class declaration matching the interface
class VirtualPoint {
x: number;
y: number;
constructor(x: number, y: number) { this.x = x; this.y = y;}
}
// compiler doesn't throw (DO NOT RELY ON THIS, CONFUSING)
const newVPoint = new VirtualPoint(13, 56);
logPoint(newVPoint);
- class expressions are not supported TypeScript is unable to detect types/shapes created through class expressions. This mandates the use of class declarations
-
values returned from built-in objects methods have a signature too and the code processing said values has to handle that signature properly, for example :
-
Map.prototype.get()
andArray.prototype.at()
return the composing typeunknown | undefined
- TypeScript assumes that
undefined
will be returned at some point, so the code won't compile unlessundefined
is handled
-
- nullish coalescing operator and optional chaining can be used to handle such cases (beware of operator evaluation order)
-
destructuring variables assignments have to be typed as well, when destructuring into an array it is necessary to use a
Tuple
type - use the
declaration
compiler option to create a*.d.ts
type declarations file at compile time - this file is mandatory for the module exports to be used in another file
- since strict type checking is enabled by default, relevant declaration files have to be installed as dev dependencies :
npm install --save-dev @types/node @types/express
- ESLint rules are resolved according to the declaration order of extended configurations in
.eslintrc
"extends" property - when more than one config enable the same rule, the rule from the last extended config takes precedence over the earliest ones
- review and fine tune the following TS linter rules :
no-inferrable-types