- Modern Typescript with Examples Cheat Sheet
- Typing Objects
- Mapped Types - Getting Types from Data
- Immutability
- Strict Mode
- Types
- Generics
- Discriminated Unions
- Optional Chaining
- Nullish Coalescing
- Assertion Functions
Object
is the type of all instances of class Object
.
- It describes functionality that is common to all JavaScript objects
- It includes primitive values
const obj1 = {};
obj1 instanceof Object; // true
obj1.toString === Object.prototype.toString; // true
function fn(x: Object) {}
fn("foo"); // OK
object
is the type of all non-primitive values.
function fn(x: object) {}
fn("foo"); // Error: "foo" is a primitive
interface ExampleInterface {
myProperty: boolean; // Property signature
myMethod(x: string): void; // Method signature, 'x' for documentation only
[prop: string]: any; // Index signature
(x: number): string; // Call signature
new (x: string): ExampleInstance; // Construct signature
readonly modifierOne: string; // readonly modifier
modifierTwo?: string; // optional modifier
}
Helps to describe Arrays or objects that are used as dictionaries.
- If there are both an index signature and property and/or method signatures in an interface, then the type of the index property value must also be a supertype of the type of the property value and/or method
interface I1 {
[key: string]: boolean;
// Property 'myProp' of type 'number' is not assignable to string index type 'boolean'
myProp: number;
// Property 'myMethod' of type '() => string' is not assignable to string index type 'boolean'
myMethod(): string;
}
interface I2 {
[key: string]: number;
myProp: number; // NO errors
}
Enables interfaces to describe functions, this
is the optional calling context
of the function in this example:
interface ClickListener {
(this: Window, e: MouseEvent): void;
}
const myListener: ClickListener = e => {
console.log("mouse clicked!", e);
};
addEventListener("click", myListener);
Enables describing classes and constructor functions. A class has two types:
- The type of the static side
- The type of the instance side
The constructor sits in the static side, when a class implements an interface, only the instance side of the class is checked.
interface ClockInterface {
tick(): void;
}
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
// Using Class Expression
const ClockA: ClockConstructor = class Clock implements ClockInterface {
constructor(h: number, m: number) {}
tick() {}
};
let clockClassExpression = new ClockA(18, 11);
// Using Class Declaration with a Constructor Function
class ClockB implements ClockInterface {
constructor(h: number, m: number) {}
tick() {}
}
function createClock(
ctor: ClockConstructor,
hour: number,
minute: number
): ClockInterface {
return new ctor(hour, minute);
}
let clockClassDeclaration = createClock(ClockB, 12, 17);
Typically used in the signature of a higher-order function.
type MyFunctionType = (name: string) => number;
-
Engineers can’t just think of interfaces as “objects that have exactly a set of properties” or “objects that have at least a set of properties”. In-line object arguments receive an additional level of validation that doesn’t apply when they’re passed as variables.
-
TypeScript is a structurally typed language. To create a
Dog
you don’t need to explicitly extend theDog
interface, any object with abreed
property that is of typestring
can be used as aDog
:
interface Dog {
breed: string;
}
function printDog(dog: Dog) {
console.log("Dog: " + dog.breed);
}
const ginger = {
breed: "Airedale",
age: 3
};
printDog(ginger); // excess properties are OK!
printDog({
breed: "Airedale",
age: 3
});
// Excess properties are NOT OK!!
// Argument of type '{ breed: string; age: number; }' is not assignable...
const data = {
value: 123,
text: "text",
subData: {
value: false
}
};
type Data = typeof data;
// type Data = {
// value: number;
// text: string;
// subData: {
// value: boolean;
}
const data = ["text 1", "text 2"] as const;
type Data = typeof data[number]; // "text 1" | "text 2"
const locales = [
{
locale: "se",
language: "Swedish"
},
{
locale: "en",
language: "English"
}
] as const;
type Locale = typeof locales[number]["locale"]; // "se" | "en"
const currencySymbols = {
GBP: "£",
USD: "$",
EUR: "€"
};
type CurrencySymbol = keyof typeof currencySymbols; // "GBP" | "USD" | "EUR"
interface HasPhoneNumber {
name: string;
phone: number;
}
interface HasEmail {
name: string;
email: string;
}
interface CommunicationMethods {
email: HasEmail;
phone: HasPhoneNumber;
fax: { fax: number };
}
function contact<K extends keyof CommunicationMethods>(
method: K,
contact: CommunicationMethods[K] // 💡turning key into value -- a *mapped type*
) {
//...
}
contact("email", { name: "foo", email: "[email protected]" });
contact("phone", { name: "foo", phone: 3213332222 });
contact("fax", { fax: 1231 });
// // we can get all values by mapping through all keys
type AllCommKeys = keyof CommunicationMethods;
type AllCommValues = CommunicationMethods[keyof CommunicationMethods];
Properties marked with readonly
can only be assigned to during initialization
or from within a constructor of the same class.
type Point = {
readonly x: number;
readonly y: number;
};
const origin: Point = { x: 0, y: 0 }; // OK
origin.x = 100; // Error
function moveX(p: Point, offset: number): Point {
p.x += offset; // Error
return p;
}
function moveX(p: Point, offset: number): Point {
// OK
return {
x: p.x + offset,
y: p.y
};
}
Gettable area property is implicitly read-only because there’s no setter:
class Circle {
readonly radius: number;
constructor(radius: number) {
this.radius = radius;
}
get area() {
return Math.PI * this.radius ** 2;
}
}
const array: readonly string[];
const tuple: readonly [string, string];
number
becomes number literal
// Type '10'
let x = 10 as const;
- array literals become
readonly
tuples
// Type 'readonly [10, 20]'
let y = [10, 20] as const;
- object literals get
readonly
properties - no literal types in that expression should be widened (e.g. no going from
"hello"
tostring
)
// Type '{ readonly text: "hello" }'
let z = { text: "hello" } as const;
- ⛔
const
contexts don’t immediately convert an expression to be fully immutable.
let arr = [1, 2, 3, 4];
let foo = {
name: "foo",
contents: arr
} as const;
foo.name = "bar"; // Error!
foo.contents = []; // Error!
foo.contents.push(5); // ...works!
strict: true /* Enable all strict type-checking options. */
is equivalent to enabling all of the strict mode family options:
noImplicitAny: true /* Raise error on expressions and declarations with an implied 'any' type */,
strictNullChecks: true /* Enable strict null checks */,
strictFunctionTypes: true /* Enable strict checking of function types */,
strictBindCallApply: true /* Enable strict 'bind', 'call', and 'apply' methods on functions */,
strictPropertyInitialization: true /* Enable strict checking of property initialization in classes */,
noImplicitThis: true /* Raise error on 'this' expressions with an implied 'any' type */,
alwaysStrict: true /* Parse in strict mode and emit "use strict" for each source file */
You can then turn off individual strict mode family checks as needed.
In strict null checking mode, null
and undefined
are no longer assignable to
every type.
let name: string;
name = "Marius"; // OK
name = null; // Error
name = undefined; // Error
let name: string | null;
name = "Marius"; // OK
name = null; // OK
name = undefined; // Error
Optional parameter ?
automatically adds | undefined
type User = {
firstName: string;
lastName?: string; // same as `string | undefined`
};
- In JavaScript, every function parameter is optional, when left off their value
is
undefined
. - We can get this functionality in TypeScript by adding a
?
to the end of parameters we want to be optional. This is different from adding| undefined
which requires the parameter to be explicitly passed asundefined
function fn1(x: number | undefined): void {
x;
}
function fn2(x?: number): void {
x;
}
fn1(); // Error
fn2(); // OK
fn1(undefined); // OK
fn2(undefined); // OK
Type guard needed to check if Object is possibly null
:
function getLength(s: string | null) {
// Error: Object is possibly 'null'.
return s.length;
}
function getLength(s: string | null) {
if (s === null) {
return 0;
}
return s.length;
}
// JS's truthiness semantics support type guards in conditional expressions
function getLength(s: string | null) {
return s ? s.length : 0;
}
function doSomething(callback?: () => void) {
// Error: Object is possibly 'undefined'.
callback();
}
function doSomething(callback?: () => void) {
if (typeof callback === "function") {
callback();
}
}
The
call()
method calls a function with a giventhis
value and arguments provided individually, whileapply()
accepts a single array of arguments.The
bind()
method creates a new function that, when called, has itsthis
keyword set to the provided value.
When set, TypeScript will check that the built-in methods of functions call
,
bind
, and apply
are invoked with correct argument for the underlying
function:
// With strictBindCallApply on
function fn(x: string) {
return parseInt(x);
}
const n1 = fn.call(undefined, "10"); // OK
const n2 = fn.call(undefined, false); // Argument of type 'false' is not assignable to parameter of type 'string'.
Verify that each instance property declared in a class either:
- Has an explicit initializer, or
- Is definitely assigned to in the constructor
// Error
class User {
// Type error: Property 'username' has no initializer
// and is not definitely assigned in the constructor
username: string;
}
// OK
class User {
username = "n/a";
}
const user = new User();
const username = user.username.toLowerCase();
// OK
class User {
constructor(public username: string) {}
}
const user = new User("mariusschulz");
const username = user.username.toLowerCase();
- Has a type that includes undefined
class User {
username: string | undefined;
}
const user = new User();
// Whenever we want to use the username property as a string, though, we first have to make sure that it actually holds a string and not the value undefined
const username =
typeof user.username === "string" ? user.username.toLowerCase() : "n/a";
unknown
is the type-safe counterpart of the any
type: we have to do some
form of checking before performing most operations on values of type unknown
.
type Result =
| { success: true; value: unknown }
| { success: false; error: Error };
function tryDeserializeLocalStorageItem(key: string): Result {
const item = localStorage.getItem(key);
if (item === null) {
// The item does not exist, thus return an error result
return {
success: false,
error: new Error(`Item with key "${key}" does not exist`)
};
}
let value: unknown;
try {
value = JSON.parse(item);
} catch (error) {
// The item is not valid JSON, thus return an error result
return {
success: false,
error
};
}
// Everything's fine, thus return a success result
return {
success: true,
value
};
}
never
represents the type of values that never occur. It is used in the
following two places:
- As the return type of functions that never return
- As the type of variables under type guards that are never true
never
can be used in control flow analysis:
function controlFlowAnalysisWithNever(value: string | number) {
if (typeof value === "string") {
value; // Type string
} else if (typeof value === "number") {
value; // Type number
} else {
value; // Type never
}
}
Generics enable you to create reusable code components that work with a number of types instead of a single type.
function identity<T>(arg: T): T {
return arg;
}
let output = identity<string>("myString"); // type of output will be 'string'
let output = identity("myString"); // The compiler sets the value of `T` based on the type of the argument we pass in
No value arguments are needed in this case:
function makePair<F, S>() {
let pair: { first: F; second: S };
function getPair() {
return pair;
}
function setPair(x: F, y: S) {
pair = {
first: x,
second: y
};
}
return { getPair, setPair };
}
// Creates a (number, string) pair
const { getPair, setPair } = makePair<number, string>();
// Must pass (number, string)
setPair(1, "y");
// Input a function `<T extends (...args: any[]) => any>`
// Output a function with same params and return type `:(...funcArgs: Parameters<T>) => ReturnType<T>`
function logDuration<T extends (...args: any[]) => any>(func: T) {
const funcName = func.name;
// Return a new function that tracks how long the original took
return (...args: Parameters<T>): ReturnType<T> => {
console.time(funcName);
const results = func(...args);
console.timeEnd(funcName);
return results;
};
}
function addNumbers(a: number, b: number): number {
return a + b;
}
// Hover over is `addNumbersWithLogging: (a: number, b: number) => number`
const addNumbersWithLogging = logDuration(addNumbers);
addNumbersWithLogging(5, 3);
A data structure used to hold a value that could take on several different, but fixed, types.
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
radius: number;
}
interface Triangle {
kind: "triangle";
whatever: number;
}
type Shape = Square | Rectangle | Circle | Triangle;
function assertNever(x: never): never {
throw new Error("Unexpected object: " + x);
}
function area(s: Shape) {
switch (s.kind) {
case "square":
return s.size * s.size;
case "rectangle":
return s.height * s.width;
case "circle":
return Math.PI * s.radius ** 2;
default:
return assertNever(s); // Argument of type 'Triangle' is not assignable to parameter of type 'never'.
}
}
Album where the artist, and the artists biography might not be present in the data.
type AlbumAPIResponse = {
title: string;
artist?: {
name: string;
bio?: string;
previousAlbums?: string[];
};
};
// Instead of:
const maybeArtistBio = album.artist && album.artist.bio;
// ?. acts differently than the &&s since && will act differently on "falsy" values (e.g. an empty string, 0, NaN, and false).
const artistBio = album?.artist?.bio;
// optional chaining also works with the [] operators when accessing elements
const maybeArtistBioElement = album?.["artist"]?.["bio"];
const maybeFirstPreviousAlbum = album?.artist?.previousAlbums?.[0];
Optional chaining on an optional function:
interface OptionalFunction {
bar?: () => number;
}
const foo: OptionalFunction = {};
const bat = foo.bar?.(); // number | undefined
Value foo
will be used when it’s “present”; but when it’s null
or
undefined
, calculate bar()
in its place.
let x = foo ?? bar();
// instead of
let x = foo !== null && foo !== undefined ? foo : bar();
It can replace uses of ||
when trying to use a default value, and avoids bugs.
When localStorage.volume
is set to 0
, the page will set the volume to 0.5
which is unintended. ??
avoids some unintended behaviour from 0
, NaN
and
""
being treated as falsy values.
function initializeAudio() {
let volume = localStorage.volume || 0.5; // Potential bug
}
Assertions in JavaScript are often used to guard against improper types being passed in.
function yell(str) {
assert(typeof str === "string");
return str.toUppercase();
// Oops! We misspelled 'toUpperCase'.
// Would be great if TypeScript still caught this!
}
function yell(str) {
if (typeof str !== "string") {
throw new TypeError("str should have been a string.");
}
// Error caught!
return str.toUppercase();
}
function assert(condition: any, msg?: string): asserts condition {
if (!condition) {
throw new AssertionError(msg);
}
}
function yell(str) {
assert(typeof str === "string");
return str.toUppercase();
// ~~~~~~~~~~~
// error: Property 'toUppercase' does not exist on type 'string'.
// Did you mean 'toUpperCase'?
}
Assertion Function Style 2 - Tell Typescript That a Specific Variable or Property Has a Different Type
Very similar to writing type predicate signatures.
function assertIsString(val: any): asserts val is string {
if (typeof val !== "string") {
throw new AssertionError("Not a string!");
}
}
function yell(str: any) {
assertIsString(str);
// Now TypeScript knows that 'str' is a 'string'.
return str.toUppercase();
// ~~~~~~~~~~~
// error: Property 'toUppercase' does not exist on type 'string'.
// Did you mean 'toUpperCase'?
}
Mike North - TypeScript 3 Fundamentals v2