my-guide

to keep track of my learning journey

View on GitHub

TypeScript

⬅️ Back to home

Quiz

Notes

1️⃣🚩Reference Course: React and TypeScript by Steve Kinney from Frontend Masters Course Website Course Code

Why use TypeScript?

Common Types

// string, number, boolean
type CounterProps = {
  incident: string;
  count: number;
  enabled: boolean;
};

// array of strings
type GroceryListProps = {
  items: string[]; // array of strings
  status: "loading" | "error" | "success" // literal types
}

//
type Item = {
  id: string;
  title: string;
};

type ContrivedExampleComponentProps = {
  item: Item;
  items: Item[];
}

//
type ItemHash = {
  [key: string]: Item;
};

type Dictionary = {
  [key: number]: string;
}

What is structural typing?

???

2️⃣🚩Reference Course: TypeScript Fundamentals, v3 by Mike North from Frontend Masters Course Website Course Code

Intro

What is TypeScript? TypeScript is an open source,typed syntactic superset of JavaScript

Why developers want types?

Using tsc and compiling TS code into JavaScript

tsc: typescript complier, complies to readable JS watch: Watch input files, watches for source changes, and rebuilds automatically preserveWatchOutput: Disable wiping the console in watch mode.

Creating .d.ts files from .js filies

// tsconfig.json
{
  // Change this to match your project
  "include": ["src/**/*"],
  "compilerOptions": {
    // Tells TypeScript to read JS files, as
    // normally they are ignored as source files
    "allowJs": true,
    // Generate d.ts files
    "declaration": true,
    // This compiler run should
    // only output d.ts files
    "emitDeclarationOnly": true,
    // Types should go into this directory.
    // Removing this would place the .d.ts files
    // next to the .js files
    "outDir": "dist",
    // go to js file when using IDE functions like
    // "Go to Definition" in VSCode
    "declarationMap": true,
    // js files emitted can be run using node if they are "CommonJS" type modules
    "module": "CommonJS"
  }
}

If you’d like to write tests for your .d.ts files, try tsd.

Variables and simple values

let age = 6 on hover let age: number TypeScript is able to infer that age is a number, based on the fact that we’re initializing it with a value as we are declaring it.

Literal type

const age = 6 on hover const age: 6 here age will always be 6 in this program.

let mostFlexibleType, if no type is given then it ends up being an implicit any, let mostFlexibleType: any

any is the normal way JavaScript variables work, in that you could assign the above variable to a number, then later a function, then a string.

TypeScript uses the syntax : type after an identifier as the type annotation, where type can be any valid type. Once an identifier is annotated with a type, it can be used as that type only.

Function arguments and return values

Type annotations are used to describe function arguments and its return value

function add(a: number, b: number): number {return a + b}

Objects and arrays

object types are defined by:

The names of the properties that are (or may be) present The types of those properties


let car: {
  make: string
  model: string
  year: number
  chargeVoltage?: number // We can state that this property is optional using the ? operator
}

car = {
  make: "Toyota",
  model: "Corolla",
  year: 2002
}

Index Signature

const phones = {
  home: { country: "+1", area: "211", number: "652-4515" },
  work: { country: "+1", area: "670", number: "752-5856" },
  fax: { country: "+1", area: "322", number: "525-4357" },
}

// Index signature
const phones: {
  [k: string]: {
    country: string
    area: string
    number: string
  }
} = {}

Array types

const fileExtensions = ["js", "ts"];

const fileExtensions: string[]

const cars = [
  {
    make: "Toyota",
    model: "Corolla",
    year: 2002,
  },
]

const cars: {
    make: string;
    model: string;
    year: number;
}[]

Tuples

definition: multi-element, ordered data structure, where position of each item has some special meaning or convention.

// here myCar type is implicitly decided as `let myCar: (string | number)[]`
let myCar = [2002, "Toyota", "Corolla"]
// which gives modal types as  `const model: string | number`
const [year, make, model] = myCar

using tuple type to define the type of tuple helps us to define with finite length and position of the value

let myCar: [number, string, string] = [
  2002,
  "Toyota",
  "Corolla",
]

const [year, make, model] = myCar;
// here the types of above variables are considered as and any other type won't be considered
const year: number
const make: string
const model: string

Limitation of tuple: tuple length constraints is present on assignment but not around push and pop.

Structural vs. Nominal Types

What is type checking?

Type-checking can be thought of as a task that attempts to evaluate the question of compatibility or type equivalence.

Static vs dynamic

TypeScript’s type system is static

Static type systems type checking is performed at compile time. examples: Java, C#, C++

Dynamic type systems perform their “type equivalence” evaluation at run time. examples: JavaScript, Python, Ruby, Perl and PHP

Nominal vs structural

TypeScript type system is structural

Reference

Duck typing

“Duck typing” gets its name from the “duck test”.

“If it looks like a duck, swims like a duck, and quack like a duck, then it probably is a duck”.

In practice, this is very similar to structural typing, but “Duck typing” is usually used to describe dynamic type systems.

“Strong” vs. “Weak” types

“Duck typing” gets its name from the “duck test”.

“If it looks like a duck, swims like a duck, and quack like a duck, then it probably is a duck”.

In practice, this is very similar to structural typing, but “Duck typing” is usually used to describe dynamic type systems.

Union and Intersection types

Union type

Union Type: works like OR |

function flipCoin(): "heads" | "tails" {
  if (Math.random() > 0.5) return "heads"
  return "tails"
}

function maybeGetUserInfo():
  | ["error", Error]
  | ["success", { name: string; email: string }] {
  if (flipCoin() === "heads") {
    return [
      "success",
      { name: "Mike North", email: "mike@example.com" },
    ]
  } else {
    return [
      "error",
      new Error("The coin landed on TAILS :("),
    ]
  }
}

const outcome = flipCoin()
const outcome = maybeGetUserInfo()

const [first, second] = outcome
// const first: "error" | "success" - infered type
// const second: Error | {
//  name: string;
//  email: string;
// }
Narrowing with type guards

using Type guards we can access uncommon methods of the outcome

Type guards are expressions, which when used with control flow statement, allow us to have a more specific type for a particular value.


if (second instanceof Error) {
  // In this branch of your code, second is an Error
  // second type is
  // const second: Error
  console.log(second.name)
} else {
  // In this branch of your code, second is the user info
  // second type here is
  // const second: {
  //     name: string;
  //     email: string;
  // }
  console.log(second.email)
}

Discriminated Unions

The first and second positions of the tuple defined below are connected which is understood by TypeScript.

const outcome = maybeGetUserInfo()
if (outcome[0] === "error") {
  // In this branch of your code, second is an Error
  // outcome type infered by typescript as
  // const outcome: ["error", Error]
} else {
  // In this branch of your code, second is the user info
  // outcome type infered by typescript as
  // const outcome: ["success", {
  //   name: string;
  //   email: string;
  // }]
}

Intersection types

Intersection types in TypeScript can be described using the & (ampersand) operator.


function makeWeek(): Date & { end: Date } {
  //⬅ return type

  const start = new Date()
  const end = new Date(start.valueOf() + ONE_WEEK)

  return Object.assign(start, { end: end } // kind of Object.assign
}

const thisWeek = makeWeek()
thisWeek.toISOString()
// thisWeek type
// const thisWeek: Date & {
//     end: Date;
// }

thisWeek.end.toISOString()
// (property) end: Date - end type

Note: It is far less common to use intersection types compared to union types

Interfaces and Type Aliases

TypeScript uses ‘interfaces’ and ‘type aliases’ for centrally defining types and giving them useful and meaningful names

Type Aliases

A name for any type

We can import and export them and give meaningful names and mostly use to define objects type

///////////////////////////////////////////////////////////
// @filename: types.ts
export type UserContactInfo = {
  name: string
  email: string
}
///////////////////////////////////////////////////////////
// @filename: utilities.ts
import { UserContactInfo } from "./types"

function printContactInfo(info: UserContactInfo) {
  console.log(info) // (parameter) info: UserContactInfo
  console.log(info.email) // (property) email: string
}

const painter = {
  name: "Robert Ross",
  email: "bross@pbs.org",
  favoriteColor: "Titanium White",
}

printContactInfo(painter) // totally fine

limitations:

Inheritence:

You can create type aliases that combine existing types with new behavior by using Intersection (&) types.

type SpecialDate = Date & { getReason(): string }

const newYearsEve: SpecialDate = {
  ...new Date(),
  getReason: () => "Last day of the year",
}
newYearsEve.getReason()

Interfaces

An interface is a way of defining an object type

interface UserInfo {
  name: string
  email: string
}

function printUserInfo(info: UserInfo) {
  info.name // (property) UserInfo.name: string
}

Inheritence: EXTENDS:heritage clause, a sub-interface extends from a base-interface


interface Animal {
  isAlive(): boolean
}
interface Mammal extends Animal {
  getFurOrHairColor(): string
}
interface Dog extends Mammal {
  getBreed(): string
}
function careForDog(dog: Dog) {
  dog.getBreed()
  dog.getBreed()
  dog.getFurOrHairColor()
}

IMPLEMENTS: second heritage clause, A given class should produce instances that confirm to a given interface.

interface AnimalLike {
  eat(food): void
}

class Dog implements AnimalLike {
  bark() {
    return "woof"
  }
  eat(food) {
    consumeFood(food)
  }
}

While TypeScript (and JavaScript) does not support true multiple inheritance (extending from more than one base class), this implements keyword gives us the ability to validate, at compile time, that instances of a class conform to one or more “contracts” (types).

Note that both extends and implements can be used together:

class LivingOrganism {
  isAlive() {
    return true
  }
}
interface AnimalLike {
  eat(food): void
}
interface CanBark {
  bark(): string
}

class Dog
  extends LivingOrganism
  implements AnimalLike, CanBark
{
  bark() {
    return "woof"
  }
  eat(food) {
    consumeFood(food)
  }
}

A class can only implement an object type or intersection of object types with statically known members. class can not implement the below type type CanBark = | number | { bark(): string } For this reason, it is best to use interfaces for types that are used with the implements heritage clause.

Open interfaces

You can have multiple interface declarations in same scope which are merged to create single interface.

interface AnimalLike {
  isAlive(): boolean
}
function feed(animal: AnimalLike) {
  animal.eat // (method) AnimalLike.eat(food: any): void
  animal.isAlive // (method) AnimalLike.isAlive(): boolean
}

// SECOND DECLARATION OF THE SAME NAME
interface AnimalLike {
  eat(food): void
}

Choosing which to use In many situations, either a type alias or an interface would be perfectly fine, however…

If you need to define something other than an object type (e.g., use of the | union type operator), you must use a type alias If you need to define a type to use with the implements heritage term, it’s best to use an interface If you need to allow consumers of your types to augment them, you must use an interface.

Recursion

Recursive types, are self-referential, and are often used to describe infinitely nestable types

type NestedNumbers = number | NestedNumbers[]

const val: NestedNumbers = [3, 4, [5, 6, [7], 59], 221]

Example using all the above concepts:

type stringFunctionArgOne = (color: string) => string;

interface colorCodeCharacters {
  getCharacter: stringFunctionArgOne
}

interface colorCodePowers {
  getPower: stringFunctionArgOne
}

class colorCodePlanet {
  getPlanet: stringFunctionArgOne = printColor;
}

class powerRangers extends colorCodePlanet implements colorCodeCharacters, colorCodePowers {
  getCharacter = printColor;
  getPower = printColor;
  getName = printColor;
}

function isPowerRangers(name: any): name is powerRangers {
  return name instanceof powerRangers
}

interface blackClover extends colorCodePowers {
  getName: stringFunctionArgOne
  getTeam: stringFunctionArgOne
}

function newAnime(name: powerRangers | blackClover): string {
  if('getTeam' in name) {
    name.getTeam('blackClover-team');
  }
  if(name instanceof powerRangers) {
    name.getCharacter('powerRangers-character');
  }
  if(isPowerRangers(name)) {
    name.getCharacter('powerRangers-character2');
    name.getPlanet('powerRangers-planet')
  }
  return name.getName('name');
}

let pr = new powerRangers();

let bc: blackClover = {
  getName: printColor,
  getTeam: printColor,
  getPower: printColor
}

function printColor(color: string) {
  console.log(color);
  return color;
}
newAnime(pr);
newAnime(bc);

Hack: Writing types for JSON values

[Exercise Link] (https://www.typescript-training.com/course/fundamentals-v3/08-exercise-json-types/)

Functions

Call signatures

const add: TwoNumberCalculation = (a, b) => a + b const subtract: TwoNumberCalc = (x, y) => x - y

// ‘Function type experssion’ syntax const addIndiv = (x: number, y: number): number => x+y;

function subtractIndiv(x: number, y: number): number { return x - y; }


- Functions which doesn't have return has the return type as `void`

> The return value of a void function is intended to be ignored

#### Construct signatures

Construct signatures are similar to call signatures, except they describe what should happen with the new keyword.

```js
 interface DateConstructor {
  new (value: number): Date
}

let MyDateConstructor: DateConstructor = Date
const d = new MyDateConstructor()
// const d: Date

Function Overloads


type FormSubmitHandler = (data: FormData) => void
type MessageHandler = (evt: MessageEvent) => void

function handleMainEvent(
  elem: HTMLFormElement,
  handler: FormSubmitHandler
)
function handleMainEvent(
  elem: HTMLIFrameElement,
  handler: MessageHandler
)
function handleMainEvent(
  elem: HTMLFormElement | HTMLIFrameElement,
  handler: FormSubmitHandler | MessageHandler
) {}

const myFrame = document.getElementsByTagName("iframe")[0]
// const myFrame: HTMLIFrameElement
const myForm = document.getElementsByTagName("form")[0]
// const myForm: HTMLFormElement

// function handleMainEvent(elem: HTMLIFrameElement, handler: MessageHandler): any (+1 overload)
handleMainEvent(myFrame, (val) => {
})
// function handleMainEvent(elem: HTMLFormElement, handler: FormSubmitHandler): any (+1 overload)
handleMainEvent(myForm, (val) => {
})

We have effectively created a linkage between the first and second arguments, which allows our callback’s argument type to change, based on the type of handleMainEvent’s first argument.

“implementation” function signature must be general enough to include everything that’s possible through the exposed first and second function heads

this types

function myClickHandler(
  this: HTMLButtonElement,
  event: Event
) {
  this.disabled = true // (property) HTMLButtonElement.disabled: boolean
}
myClickHandler // function myClickHandler(this: HTMLButtonElement, event: Event): void

const myButton = document.getElementsByTagName("button")[0]
const boundHandler = myClickHandler.bind(myButton)
// const boundHandler: (event: Event) => void

boundHandler(new Event("click")) // bound version: ok
myClickHandler.call(myButton, new Event("click")) // also ok

Note TypeScript understands that .bind, .call or .apply will result in the proper this being passed to the function as part of its invocation.

Function type best practices

Classes in TypeScript

Class Fields

class Car {
  // class fields
  make: string
  model: string
  year: number
  constructor(make: string, model: string, year: number) { // constructor argument
    this.make = make
    this.model = model
    this.year = year
  }
}

let sedan = new Car("Honda", "Accord", 2017)
sedan.activateTurnSignal("left") // not safe!
new Car(2017, "Honda", "Accord") // not safe!

Access Modifiers

keyword who can access
public everyone (this is the default)
protected the instance itself, and subclasses
private only the instance itself
class Human {
  public likes: number
  public color: string
  public hair: string
  protected gender = (g: string) => console.log(g);
  private genderOptions = ['male', 'female']

  constructor(likes: number, color: string, hair: string) {
    this.likes = likes;
    this.color = color;
    this.hair = hair;
  }

  protected createGender() {
    return this.gender(this.genderOptions[Math.floor((Math.random() * 2))])
  }
}

const yamuna = new Human(5, 'red', 'curly');
console.log(yamuna.genderOptions, yamuna.hair);

class Animal extends Human {
  constructor(likes: number, color: string, hair: string) {
    super(likes, color, hair)
  }

  public getGender() {
    this.createGender()
  }
}

const dog = new Animal(26, 'white', 'straight')
dog.getGender()

We see two examples of “limited exposure”

Note: Just like any other aspect of type information, access modifier keywords are only validated at compile time, with no real privacy or security benefits at runtime. This means that even if we mark something as private, if a user decides to set a breakpoint and inspect the code that’s executing at runtime, they’ll still be able to see everything.

JS private #fields

class Car {
  public make: string
  public model: string
  #year: number

  constructor(make: string, model: string, year: number) {
    this.make = make
    this.model = model
    this.#year = year
  }
}
const c = new Car("Honda", "Accord", 2017)
c.#year
// Property '#year' is not accessible outside class 'Car' because it has a private identifier.

readonly

class Car {
  public make: string
  public model: string
  public readonly year: number

  constructor(make: string, model: string, year: number) {
    this.make = make
    this.model = model
    this.year = year
  }

  updateYear() {
    this.year++
    // Cannot assign to 'year' because it is a read-only property.
  }
}

Param properties

Concise syntax for defining class properties

class Car {
  constructor(
    public make: string,
    public model: string,
    public year: number
  ) {}
}

const myCar = new Car("Honda", "Accord", 2017)
myCar.make

The first argument passed to the constructor should be a string, and should be available within the scope of the constructor as make. This also creates a public class field on Car called make and pass it the value that was given to the constructor

class Base {}

class Car extends Base {
  foo = console.log("class field initializer")
  constructor(public make: string) {
    super()
    console.log("custom constructor stuff")
  }
}

const c = new Car("honda")
--------------is compiled to--------------------------
"use strict";
class Base {
}
class Car extends Base {
    constructor(make) {
        super();
        this.make = make;
        this.foo = console.log("class field initializer");
        console.log("custom constructor stuff");
    }
}
const c = new Car("honda");

Top and bottom types

User-defined Type guards

Handling nullish values

Generics

Hack: higher-order functions for dictionaries

Wrap up