Skip to content

Instantly share code, notes, and snippets.

@ekas
Last active December 8, 2022 02:14
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save ekas/5a094a1b5f78298489fc6beee4654259 to your computer and use it in GitHub Desktop.
Save ekas/5a094a1b5f78298489fc6beee4654259 to your computer and use it in GitHub Desktop.
Let's Gear Up for TypeScript

🚀 🚀 Let's Gear Up for TypeScript 🎉 🎉

TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. Any browser. Any host. Any OS. Open source.

Basic Types

  • Boolean
let isDone: boolean = false;
  • Number
let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;
  • String
let color: string = "blue";
color = 'red';
let fullName: string = `Bob Bobbington`;
let age: number = 37;
let sentence: string = `Hello, my name is ${ fullName }. I'll be ${ age + 1 } years old next month.`;

This is equivalent to declaring sentence like so:

let sentence: string = "Hello, my name is " + fullName + ".\n\n" + "I'll be " + (age + 1) + " years old next month.";
  • Array
let list: number[] = [1, 2, 3];

The second way uses a generic array type, Array<elemType>:

let list: Array<number> = [1, 2, 3];
  • Tuple
let x: [string, number];

x = ["hello", 10]; //✅

x = [10, "hello"]; //❌

Accessing an element outside the set of known indices fails with an error:

x[3] = "world"; //❌

console.log(x[5].toString()); //❌
  • Enum

An enum is a way of giving more friendly names to sets of numeric values.

enum Color {
  Red,
  Green,
  Blue,
}
let c: Color = Color.Green;
  • Any

If we want to opt-out of type checking and let the values pass through compile-time checks. To do so, we label these with the any type.

let notSure: any = 4;
notSure = "maybe a string instead";

notSure = false; //✅
let list: any[] = [1, true, "free"];

list[1] = 100;  //✅
  • Void

void is a little like the opposite of any: the absence of having any type at all.

function warnUser(): void {
  console.log("This is my warning message");
}
let unusable: void = undefined;
unusable = null; //✅ if `--strictNullChecks` is not given
  • Null and Undefined

In TypeScript, both undefined and null actually have their own types named undefined and null respectively. Much like void, they’re not extremely useful on their own:

// Not much else we can assign to these variables!
let u: undefined = undefined;
let n: null = null;

n = null  //✅

n = "abc" //❌
  • Never & Object

Interfaces

This is sometimes called “duck typing” or “structural subtyping”. In TypeScript, interfaces fill the role of naming these types, and are a powerful way of defining contracts within your code as well as contracts with code outside of your project.

function printLabel(labeledObj: { label: string }) {
  console.log(labeledObj.label);
}

let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);

Use of interface keyword

interface LabeledValue {
  label: string;
}

function printLabel(labeledObj: LabeledValue) {
  console.log(labeledObj.label);
}

let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);

Optional Properties

interface SquareConfig {
  color?: string;
  width?: number;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
  let newSquare = { color: "white", area: 100 };
  if (config.clor) { //❌
    newSquare.color = config.clor; //❌
  }
  if (config.width) {
    newSquare.area = config.width * config.width;
  }
  return newSquare;
}

let mySquare = createSquare({ color: "black" });

ReadOnly Properties

interface Point {
  readonly x: number;
  readonly y: number;
}
let p1: Point = { x: 10, y: 20 };

p1.x = 5; //❌

TypeScript comes with a ReadonlyArray<T> type that is the same as Array<T> with all mutating methods removed.

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;

ro[0] = 12; //❌

ro.push(5); //❌

ro.length = 100; //❌

a = ro; //❌

On the last line of the snippet you can see that even assigning the entire ReadonlyArray back to a normal array is illegal. You can still override it with a type assertion.

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;

a = ro as number[];

The easiest way to remember whether to use readonly or const is to ask whether you’re using it on a variable or a property. Variables use const whereas properties use readonly.

interface SquareConfig {
  color?: string;
  width?: number;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
  return { color: config.color || "red", area: config.width || 20 };
}

let mySquare = createSquare({ colour: "red", width: 100 }); //❌

Notice the given argument to createSquare is spelled colour instead of color. In plain JavaScript, this sort of thing fails silently.

type assertion

let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);

Now, the compiler won’t give you an error.

interface SquareConfig {
  color?: string;
  width?: number;
  [propName: string]: any;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
  return { color: config.color || "red", area: config.width || 20 };
}

let mySquare = createSquare({ colour: "red", width: 100 });

Function Types

Interfaces are capable of describing the wide range of shapes that JavaScript objects can take. In addition to describing an object with properties, interfaces are also capable of describing function types.

interface SearchFunc {
  (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;

mySearch = function (src: string, sub: string): boolean {
  let result = src.search(sub);
  return result > -1;
};

or

interface SearchFunc {
  (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;

mySearch = function (src, sub) {
  let result = src.search(sub);
  return result > -1;
};

But not

let mySearch: SearchFunc;

mySearch = function (src, sub) {
  let result = src.search(sub);

  return "string"; //❌
};

Indexable Types

We can also describe types that we can “index into” like a[10], or ageMap["daniel"]. TypeScript only allows two types for indexes (the keys): string and number.

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

let myArray: StringArray;
myArray = ["Bob", "Fred"];

Above, we have a StringArray interface that has an index signature. This index signature states that when a StringArray is indexed with a number, it will return a string.

interface States {
  [state: string]: boolean;
}

let s: States = {'enabled': true, 'maximized':false};
interface NumberOrStringDictionary {
  [index: string]: number | string;

  length: number; // ✅, length is a number

  name: string; // ✅, name is a string
}

However, properties of different types are acceptable if the index signature is a union of the property types.

interface ReadonlyStringArray {
  readonly [index: number]: string;
}

let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; //❌ It is readonly

Extending Interfaces

Like classes, interfaces can extend each other. This allows you to copy the members of one interface into another, which gives you more flexibility in how you separate your interfaces into reusable components.

interface Shape {
  color: string;
}

interface Square extends Shape {
  sideLength: number;
}

let square = {} as Square;
square.color = "blue";
square.sideLength = 10;

square.borderWidth = 5; //❌

An interface can extend multiple interfaces, creating a combination of all of the interfaces.

interface Shape {
  color: string;
}

interface PenStroke {
  penWidth: number;
}

interface Square extends Shape, PenStroke {
  sideLength: number;
}

Hybrid Types

As we mentioned earlier, interfaces can describe the rich types present in real world JavaScript. Because of JavaScript’s dynamic and flexible nature, you may occasionally encounter an object that works as a combination of some of the types described above.

One such example is an object that acts as both a function and an object.

interface Counter {
  (start: number): string;
  interval: number;
  reset(): void;
}

function getCounter(): Counter {
  let counter = function (start: number) {} as Counter;
  counter.interval = 123;
  counter.reset = function () {};
  return counter;
}

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;

Unions Types

Occasionally, you’ll run into a library that expects a parameter to be either a number or a string.

/**
 * Takes a string and adds "padding" to the left.
 * If 'padding' is a string, then 'padding' is appended to the left side.
 * If 'padding' is a number, then that number of spaces is added to the left side.
 */
function padLeft(value: string, padding: string | number) {
  if (typeof padding === "number") {
    return Array(padding + 1).join(" ") + value;
  }
  if (typeof padding === "string") {
    return padding + value;
  }
  //throw new Error(`Expected string or number, got '${padding}'.`);
  //No more needed
}

Union with common types

interface Bird {
  fly(): void;
  layEggs(): void;
}

interface Fish {
  swim(): void;
  layEggs(): void;
}

declare function getSmallPet(): Fish | Bird;

let pet = getSmallPet();

pet.layEggs(); //✅

pet.swim();  //❌

Discriminating Unions

type NetworkLoadingState = {
  state: "loading";
};

type NetworkFailedState = {
  state: "failed";
  code: number;
};

type NetworkSuccessState = {
  state: "success";
  response: {
    title: string;
    duration: number;
    summary: string;
  };
};

// Create a type which represents only one of the above types
// but you aren't sure which it is yet.
type NetworkState = | NetworkLoadingState | NetworkFailedState | NetworkSuccessState;

You can achieve this

function networkStatus(state: NetworkState): string {

  state.code; //❌ unsafe to use at this point

  switch (state.state) {
    case "loading":
      return "Downloading...";
    case "failed":
      return `Error ${state.code} downloading`; //✅
    case "success":
      return `Downloaded ${state.response.title} - ${state.response.summary}`;
  }
}

Intersection Types

interface ErrorHandling {
  success: boolean;
  error?: { message: string };
}

interface ArtworksData {
  artworks: { title: string }[];
}

interface ArtistsData {
  artists: { name: string }[];
}

// These interfaces are composed to have
// consistent error handling, and their own data.

type ArtworksResponse = ArtworksData & ErrorHandling;
type ArtistsResponse = ArtistsData & ErrorHandling;

const handleArtistsResponse = (response: ArtistsResponse) => {
  if (response.error) {
    console.error(response.error.message);
    return;
  }

  console.log(response.artists);
};

Literal Types

A literal is a more concrete sub-type of a collective type.

String Literal Types

In practice string literal types combine nicely with union types, type guards, and type aliases. You can use these features together to get enum-like behavior with strings.

type Easing = "ease-in" | "ease-out" | "ease-in-out";

class UIElement {
  animate(dx: number, dy: number, easing: Easing) {
    if (easing === "ease-in") {
      // ...
    } else if (easing === "ease-out") {
    } else if (easing === "ease-in-out") {
    } else {
      // It's possible that someone could reach this
      // by ignoring your types though.
    }
  }
}

let button = new UIElement();
button.animate(0, 0, "ease-in");
button.animate(0, 0, "uneasy"); //❌

creating function overloads

function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// ... more overloads ...
function createElement(tagName: string): Element {
  // ... code goes here ...
}

Numeric Literal Types

interface MapConfig {
  lng: number;
  lat: number;
  tileSize: 8 | 16 | 32;
}

setupMap({ lng: -73.935242, lat: 40.73061, tileSize: 16 });

Functions

function add(x: number, y: number): number {
  return x + y;
}

let myAdd = function(x: number, y: number): number {
  return x + y;
};

then why this

let myAdd: (baseValue: number, increment: number) => number = function(
  x: number,
  y: number
): number {
  return x + y;
};

And this is valid too

function buildName(firstName: string, lastName?: string) {
  if (lastName) return firstName + " " + lastName;
  else return firstName;
}

Default Parameters

function buildName(firstName: string, lastName = "Smith") {
  // ...
}

Types with Arrow Functions

let printName = (firstName: string, lastName?: string): string  => {
  if (lastName) return firstName + " " + lastName;
  else return firstName;
}

console.log(printName("Ekaspeet", "Singh"));

Similarly

let printName: (x: string, y: string ) => string = (firstName, lastName) => {
  if (lastName) return firstName + " " + lastName;
  else return firstName;
}

console.log(printName("Ekaspeet", "Singh"));

Rest Parameters

function buildName(firstName: string, ...restOfName: string[]) {
  return firstName + " " + restOfName.join(" ");
}

// employeeName will be "Joseph Samuel Lucas MacKinzie"
let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");

Classes

Traditional JavaScript uses functions and prototype-based inheritance to build up reusable components.

class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    return "Hello, " + this.greeting;
  }
}

let greeter = new Greeter("world");

Inheritance

class Animal {
  move(distanceInMeters: number = 0) {
    console.log(`Animal moved ${distanceInMeters}m.`);
  }
}

class Dog extends Animal {
  bark() {
    console.log("Woof! Woof!");
  }
}

const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();

Complex Problem:

class Animal {
  name: string;
  constructor(theName: string) {
    this.name = theName;
  }
  move(distanceInMeters: number = 0) {
    console.log(`${this.name} moved ${distanceInMeters}m.`);
  }
}

class Snake extends Animal {
  constructor(name: string) {
    super(name);
  }
  move(distanceInMeters = 5) {
    console.log("Slithering...");
    super.move(distanceInMeters);
  }
}

class Horse extends Animal {
  constructor(name: string) {
    super(name);
  }
  move(distanceInMeters = 45) {
    console.log("Galloping...");
    super.move(distanceInMeters);
  }
}

let sam = new Snake("Sammy the Python");
let tom: Animal = new Horse("Tommy the Palomino");

sam.move();
tom.move(34);

Takeaway: One difference from the prior example is that each derived class that contains a constructor function must call super() which will execute the constructor of the base class. What’s more, before we ever access a property on this in a constructor body, we have to call super(). This is an important rule that TypeScript will enforce.

Public(default), private, and protected modifiers

class Animal {
  public name: string;
  public constructor(theName: string) {
    this.name = theName;
  }
  public move(distanceInMeters: number) {
    console.log(`${this.name} moved ${distanceInMeters}m.`);
  }
}

ECMAScript Private Fields

With TypeScript 3.8, TypeScript supports the new JavaScript syntax for private fields:

class Animal {
    #name: string;
    constructor(theName: string) { this.#name = theName; }
}

new Animal("Cat").#name; //❌

Private & Protected Modifiers

Readonly Modifier

class Octopus {
  readonly name: string;
  readonly numberOfLegs: number = 8;
  constructor(theName: string) {
    this.name = theName;
  }
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; //❌

Using a class as an interface

A class declaration creates two things: a type representing instances of the class and a constructor function. Because classes create types, you can use them in the same places you would be able to use interfaces.

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

interface Point3d extends Point {
  z: number;
}

let point3d: Point3d = { x: 1, y: 2, z: 3 };

Generics

For any major language, one of the main tools in the toolbox for creating reusable components is generics.

function identity(arg: number): number {
  return arg;
}

But not sure of the types given to identity function. You can any. But that is just not a solution. It actually avoids type-checking.

function identity(arg: any): any {
  return arg;
}

Let's check this out

function identity<T>(arg: T): T {
  return arg;
}
let output = identity<string>("myString"); //✅

And this valid too

let output = identity("myString");

Export

export interface StringValidator {
  x: number;
  y: number;
}
export interface StringValidator {
  isAcceptable(s: string): boolean;
}

tsconfig.json

tsc --init

tsconfig.json gets created in your project

Let's Explore some Compiler Options:

In some cases where no type annotations are present, TypeScript will fall back to a type of any for a variable when it cannot infer the type. Turning on noImplicitAny however TypeScript will issue an error whenever it would have inferred any:

noImplicitAny
function fn(s) { //❌
  // No error?
  console.log(s.subtr(3));
}
fn(42);

When strictPropertyInitialization set to true, TypeScript will raise an error when a class property was declared but not set in the constructor.

strictPropertyInitialization
class UserAccount {
  name: string;
  accountType = "user";

  email: string; //❌
  address: string | undefined;

  constructor(name: string) {
    this.name = name;
    // Note that this.email is not set
  }
}

Install

  • NPM Global Install
  • VS Code TSServer -> Settings -> typescript.validate.enable -> Add to settings JSON / Enable-Disable

Resources

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment