Skip to content

Typescript

Posted on:September 23, 2022 at 03:22 PM

Introduction

Downlevelling

Running a ts file

The primitive types

Complex types: arrays and objects

Any type

let person: {
  name: string,
  age: number,
};

TS and functions

Anonymous functions

// No type annotations here, but TypeScript can spot the bug
const names = ["Alice", "Bob", "Eve"];

// Contextual typing also applies to arrow functions
names.forEach(s => {
  console.log(s.toUppercase());
  // Property 'toUppercase' does not exist on type 'string'.
  // Did you mean 'toUpperCase'?
});

Object Types

// The parameter's type annotation is an object type
// You can use , or ; to separate the properties,
// and the last separator is optional either way.
function printCoord(pt: { x: number, y: number }) {
  console.log("The coordinate's x value is " + pt.x);
  console.log("The coordinate's y value is " + pt.y);
}
printCoord({ x: 3, y: 7 });

Accessing properties in javascript

Union Types

Working with union types

Finding the types of a variable

Type Aliases

type Point = {
  x: number,
  y: number,
};

type ID = number | string;

interface Point {
  x: number;
  y: number;
}

Interfaces

Type aliases

type person = {
  name: string,
  age: number,
};

Differences Between Type Aliases and Interfaces

Extending an interface

interface Animal {
  name: string;
}

interface Bear extends Animal {
  honey: boolean;
}

// getBear() gives Bear type
const bear = getBear();
bear.name;
bear.honey;

extending a type via intersections

type Animal = {
  name: string,
};

type Bear = Animal & {
  honey: boolean,
};

// getBear() gives Bear type
const bear = getBear();
bear.name;
bear.honey;

Adding new fields to an existing interface

interface Window {
  title: string;
}

interface Window {
  ts: TypeScriptAPI;
}

const src = 'const a = "Hello World"';
window.ts.transpileModule(src, {});

A type cannot be changed after being created

type Window = {
  title: string,
};

type Window = {
  ts: TypeScriptAPI,
};

// Error: Duplicate identifier 'Window'.

Type Assertions

// Ts knows that this will return some kind of HTMLElement,
// but you know that your page will always have an
// HTMLCanvasElement with a given ID
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;

// You can also use the angle-bracket syntax
// (except if the code is in a .tsx file)
const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");

Literal Types

function printText(s: string, alignment: "left" | "right" | "center") {
  // ...
}

function compare(a: string, b: string): -1 | 0 | 1 {
  return a === b ? 0 : a > b ? 1 : -1;
}

interface Options {
  width: number;
}

function configure(x: Options | "auto") {
  // ...
}

Literal inference

// Argument of type 'string' is not assignable to parameter
// of type '"GET" | "POST"'.
const req = { url: "https://example.com", method: "GET" };
handleRequest(req.url, req.method);

// Solution 1:
const req = { url: "https://example.com", method: "GET" as "GET" };
// Solution 2
handleRequest(req.url, req.method as "GET");
// Solution 3:
const req = { url: "https://example.com", method: "GET" } as const

null and undefined

Non-null Assertion Operator (Postfix !)

Enums

Less Common Primitives

bigint

// Creating a bigint via the BigInt function
const oneHundred: bigint = BigInt(100);

// Creating a BigInt via the literal syntax
const anotherHundred: bigint = 100n;

symbol

const firstName = Symbol("name");
const secondName = Symbol("name");

if (firstName === secondName) {
  // This condition will always return 'false'
  // since the types 'typeof firstName' and
  // 'typeof secondName' have no overlap.
  // Can't ever happen
}

Narrowing

Different constructs TypeScript understands for narrowing

typeof typegaurd

Truthiness narrowing

// both of these result in 'true'
Boolean("hello"); // type: boolean, value: true
!!"world"; // type: true, value: true

function printAll(strs: string | string[] | null) {
  // null and array type removed in this conditional
  if (strs && typeof strs === "object") {
    for (const s of strs) {
      console.log(s);
    }
  } else if (typeof strs === "string") {
    console.log(strs);
  }
}

Equality narrowing

function example(x: string | number, y: string | boolean) {
  if (x === y) {
    // We can now call any 'string' method on 'x' or 'y'.
    // (method) String.toUpperCase(): string
    x.toUpperCase();

    // (method) String.toLowerCase(): string
    y.toLowerCase();
  } else {
    // (parameter) x: string | number
    console.log(x);

    // (parameter) y: string | boolean
    console.log(y);
  }
}

The in operator narrowing

type Fish = { swim: () => void };
type Bird = { fly: () => void };
type Human = { swim?: () => void; fly?: () => void };

function move(animal: Fish | Bird | Human) {
  // Notice that "human" type show up in
  // both sides of the "in" check
  if ("swim" in animal) {
  // (parameter) animal: Fish | Human
    animal;
  } else {
   // (parameter) animal: Bird | Human
    animal;
  }
}

instanceof narrowing

function logValue(x: Date | string) {
  if (x instanceof Date) {
    // (parameter) x: Date
    console.log(x.toUTCString());
  } else {
    // (parameter) x: string
    console.log(x.toUpperCase());
  }
}

Assignments

// type is "let x: string | number"
let x = Math.random() < 0.5 ? 10 : "hello world!";

// Narrowed to let x: number
x = 1;

// Narrowed to let x: string
x = "goodbye!";

// Error: Type 'boolean' is not assignable to
// type 'string | number'.
x = true;

Control flow analysis

function padLeft(padding: number | string, input: string) {
  if (typeof padding === "number") {
    // If padding is number then this function
    // will return
    return " ".repeat(padding) + input;
  }
  // This code is not 'reachable' if padding
  // has number as one of it's types.
  // Ts analyzed and removed 'number' from
  // type of padding in this branch of code.
  return padding + input;
}

Using type predicates

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

// Both calls to 'swim' and 'fly' are now okay.
let pet = getSmallPet();

if (isFish(pet)) {
  // pet is a Fish
  pet.swim();
} else {
  // pet is not a Fish, only other
  // option is Bird type
  pet.fly();
}

// You may use the type guard isFish to filter an
// array of 'Fish | Bird' and obtain an 'array of Fish'
const zoo: (Fish | Bird)[] =
    [getSmallPet(), getSmallPet(), getSmallPet()];
const underWater1: Fish[] = zoo.filter(isFish);
// or, equivalently
const underWater2: Fish[] = zoo.filter(isFish) as Fish[];

// The predicate may need repeating for more complex examples
const underWater3: Fish[] = zoo.filter((pet): pet is Fish => {
  if (pet.name === "sharkey") return false;
  return isFish(pet);
});

Discriminated unions

interface Shape {
  kind: "circle" | "square";
  radius?: number;
  sideLength?: number;
}

function getArea(shape: Shape) {
  if (shape.kind === "circle") {
    // Ts doesn't know that circle type has radius
    // Object is possibly 'undefined'.
    return Math.PI * shape.radius ** 2;
  }
}

// Solution to above is "discriminated union"
interface Circle {
  kind: "circle";
  radius: number;
}

interface Square {
  kind: "square";
  sideLength: number;
}

// Shape is a dsicriminated union
type Shape = Circle | Square;

function getArea(shape: Shape) {
  if (shape.kind === "circle") {
    // (parameter) shape: Circle
    return Math.PI * shape.radius ** 2;
  }
}

The never type

Exhaustiveness checking using never

// Adding a new member to the Shape union,
// will cause a TypeScript error
interface Triangle {
  kind: "triangle";
  sideLength: number;
}

type Shape = Circle | Square | Triangle;

function getArea(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    default:
      // Type 'Triangle' is not assignable to type 'never'.
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck;
  }
}

function and function types

// Note that the parameter name is required.
// You can also use type aliases to name
// a function type
function greeter(fn: (a: string) => void) {
  fn("Hello, World");
}

function printToConsole(s: string) {
  console.log(s);
}

greeter(printToConsole);

Call Signatures

type DescribableFunction = {
  description: string,
  // Note the syntax, use ':' between the parameter
  // list and the return type rather than '=>'
  // Call signature
  (someArg: number): boolean,
};
function doSomething(fn: DescribableFunction) {
  console.log(fn.description + " returned " + fn(6));
}

// You can combine call and construct signatures
// in the same type arbitrarily. Suitable for objects
// like Date which can be called with or without 'new'
interface CallOrConstruct {
  new(s: string): Date;
  (n?: number): number;
}

Construct Signatures

type SomeConstructor = {
  // Construct Signature
  new(s: string): SomeObject,
};
function fn(ctor: SomeConstructor) {
  return new ctor("hello");
}

Generic Functions

function firstElement<Type>(arr: Type[]): Type | undefined {
  return arr[0];
}

// Note that we didn’t have to specify Type in this sample
// s is of type 'string'
const s = firstElement(["a", "b", "c"]);
// n is of type 'number'
const n = firstElement([1, 2, 3]);
// u is of type undefined
const u = firstElement([]);

Constraints

// Ts can infer the return type of generic functions also
// Cnstraint on Type to have a length property of type number
function longest<Type extends { length: number }>(a: Type, b: Type) {
  if (a.length >= b.length) {
    return a;
  } else {
    return b;
  }
}

// longerArray is of type 'number[]'
const longerArray = longest([1, 2], [1, 2, 3]);
// longerString is of type 'alice' | 'bob'
const longerString = longest("alice", "bob");
// Error! Numbers don't have a 'length' property
const notOK = longest(10, 100);
Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.

Specifying Type Arguments in generics

function combine<Type>(arr1: Type[], arr2: Type[]): Type[] {
  return arr1.concat(arr2);
}
// Type 'string' is not assignable to type 'number'.
// Ts unable to infer the arg type
const arr = combine([1, 2, 3], ["hello"]);

// Correct way to call the generic function
// by specifying the type
const arr = (combine < string) | (number > ([1, 2, 3], ["hello"]));

Guidelines for Writing Good Generic Functions

Optional Parameters

// Use of '?' to specify optional arguments
function f(x?: number) {
  // ...
}
f(); // OK
f(10); // OK

Optional Parameters in Callbacks

Function Overloads

// overload signature#1
function makeDate(timestamp: number): Date;
// overload signature#2
function makeDate(m: number, d: number, y: number): Date;
// implementation signature
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
  if (d !== undefined && y !== undefined) {
    return new Date(y, mOrTimestamp, d);
  } else {
    return new Date(mOrTimestamp);
  }
}
const d1 = makeDate(12345678);
const d2 = makeDate(5, 5, 5);
// No overload expects 2 arguments, but overloads
// do exist that expect either 1 or 3 arguments.
const d3 = makeDate(1, 3);

Declaring this in a Function

Other Types to Know About

// The inferred return type is void
function noop() {
  return;
}
function fail(msg: string): never {
  throw new Error(msg);
}

Rest Parameters and Arguments

function multiply(n: number, ...m: number[]) {
  return m.map(x => n * x);
}
// 'a' gets value [10, 20, 30, 40]
const a = multiply(10, 1, 2, 3, 4);
const arr2 = [4, 5, 6];
arr1.push(...arr2);

// Inferred type is number[] -- "an array with zero or more numbers",
// not specifically two numbers
const args = [8, 5]
// A spread argument must either have a tuple type
// or be passed to a rest parameter.
const angle = Math.atan2(...args)

// Fix for above scenario
// Inferred as 2-length tuple
const args = [8, 5] as const;
// OK
const angle = Math.atan2(...args);

Parameter Destructuring

// Parameter Destructuring of arguments passed to
// the function
type ABC = { a: number, b: number, c: number };
function sum({ a, b, c }: ABC) {
  console.log(a + b + c);
}
sum({ a: 10, b: 3, c: 9 });

Assignability of Functions

type voidFunc = () => void;

// Return value will be ingnored
const f1: voidFunc = () => {
  return true;
};
// Return value will be ingnored
const f2: voidFunc = () => true;
// Return value will be ingnored
const f3: voidFunc = function () {
  return true;
};

// Following variables will be of type void
const v1 = f1();
const v2 = f2();
const v3 = f3();

// literal function definition has a void return type
function f2(): void {
  // @ts-expect-error
  return true;
}

// literal function definition has a void return type
const f3 = function (): void {
  // @ts-expect-error
  return true;
};

Object types

// anonymous object
function greet(person: { name: string, age: number }) {
  return "Hello " + person.name;
}

Property Modifiers

Optional Properties

// In an object destructuring pattern, 'shape: Shape'
// means grab the property shape and redefine it
// locally as a variable named Shape
function draw({ shape: Shape, xPos: number = 100 /*...*/ }) {
  // Cannot find name 'shape'. Did you mean 'Shape'?
  render(shape);
  // Cannot find name 'xPos'.
  render(xPos);

readonly Properties

interface SomeType {
  readonly prop: string;
}

function doSomething(obj: SomeType) {
  // We can read from 'obj.prop'.
  console.log(`prop has the value '${obj.prop}'.`);

  // But we can't re-assign it.
  // Cannot assign to 'prop' because it is a
  // read-only property.
  obj.prop = "hello";
}

Index Signatures

interface StringArray {
  [index: number]: string;
}

const myArray: StringArray = getStringArray();
// const secondItem: string
const secondItem = myArray[1];

Extending Types

interface BasicAddress {
  name?: string;
  street: string;
  city: string;
  country: string;
  postalCode: string;
}
// Add the new fields that are unique to 'AddressWithUnit'
interface AddressWithUnit extends BasicAddress {
  unit: string;
}

interface Colorful {
  color: string;
}

interface Circle {
  radius: number;
}

// extending from multiple interfaces
interface ColorfulCircle extends Colorful, Circle {}

const cc: ColorfulCircle = {
  color: "red",
  radius: 42,
};

Intersection Types

interface Colorful {
  color: string;
}
interface Circle {
  radius: number;
}

type ColorfulCircle = Colorful & Circle;

Interfaces vs. Intersections

Generic Object Types

interface Box<Type> {
  contents: Type;
}

let box: Box<string>;

interface Apple {
  // ....
}

// Same as '{ contents: Apple }'.
type AppleBox = Box<Apple>;

// type aliases can also be generic
type Box<Type> = {
  contents: Type,
};

Array Type

ReadonlyArray Type

// Error: 'ReadonlyArray' only refers to a type,
but is being used as a value here.
new ReadonlyArray("red", "green", "blue");
// Correct way to initialize a readonly array
const roArray: ReadonlyArray<string> = ["red", "green", "blue"];

let x: readonly string[] = [];
let y: string[] = [];

x = y;
// Error: The type 'readonly string[]' is 'readonly'
// and cannot be assigned to the mutable type 'string[]'.
y = x;

Tuple Types

// tuple type of string and number whose 0 index
// contains a string and 1 index contains a number.
type StringNumberPair = [string, number];

type Either2dOr3d = [number, number, number?];

function setCoordinate(coord: Either2dOr3d) {
  // const z: number | undefined
  const [x, y, z] = coord;
  // (property) length: 2 | 3
  console.log(`Provided coordinates had ${coord.length} dimensions`);
}

// first two elements are string and number respectively,
// but which may have any number of booleans following.
type StringNumberBooleans = [string, number, ...boolean[]];
// first element is string and then any number of booleans
// and ending with a number.
type StringBooleansNumber = [string, ...boolean[], number];
// tarting elements are any number of booleans and ending
// with a string then a number.
type BooleansStringNumber = [...boolean[], string, number];

const a: StringNumberBooleans = ["hello", 1];
const b: StringNumberBooleans = ["beautiful", 2, true];
const c: StringNumberBooleans = ["world", 3, true, false, true, false, true];

// Useful when you don’t want to introduce intermediate variables/params.
function readButtonInput(...args: [string, number, ...boolean[]]) {
  const [name, version, ...input] = args;
  // ...
}

// Equivalent to above
function readButtonInput(name: string, version: number, ...input: boolean[]) {
  // ...
}

readonly Tuple Types

Creating Types from Types

Generics

// Genric function
function identity<Type>(arg: Type): Type {
  return arg;
}
// Method#1 to call a generic function.
let output = `identity<string>('myString')`;
// Method#2 ro call a generic funtion.
let output = identity("myString");

Generic Types

// Interface with generic function
interface GenericIdentityFn {
  // call signature
  <Type>(arg: Type): Type;
}

function identity<Type>(arg: Type): Type {
  return arg;
}

let myIdentity: GenericIdentityFn = identity;

// Genric interface
interface GenericIdentityFn<Type> {
  (arg: Type): Type;
}

function identity<Type>(arg: Type): Type {
  return arg;
}

let myIdentity: GenericIdentityFn<number> = identity;

Generic Classes

class GenericNumber<NumType> {
  zeroValue: NumType;
  // call signature
  add: (x: NumType, y: NumType) => NumType;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
  return x + y;
};

Generic Constraints

// Adding a contraint to generic type.
interface Lengthwise {
  length: number;
}

function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
  // Now we know it has a .length property, so no more error
  console.log(arg.length);
  return arg;
}

Using Type Parameters in Generic Constraints

function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
  return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a");
getProperty(x, "m");
Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.

Using Class Types in Generics

function create<Type>(c: { new(): Type }): Type {
  return new c();
}

Keyof Type Operator

type Point = { x: number; y: number };
// type P = keyof Point
type P = keyof Point;

AutoComplete in VSCode

type vs interface

Optional function arguments

Array types

Casting in TS

Set generic

Map generic

Record type

Record<string, string>
// Or
{
  [id:string]: string;
}

Narrowing down Union types

Typing errors in try-catch

....
catch(e) {
  if (e instanceof Error) {
    return e.message;
  }
}

Inheriting interface properties

Combining types to create new types

Selectively Construct Types from Other Types

Typing Functions

Typing Async Functions