No BS TS 1-19


TypeScript

Updated Mar 14th, 2022

Jack Herrington YouTube

typescriptlang.org

#1 – Typescript Setup & Everyday Types

Why to use?

1.) Reduce Errors. Examples include: try to call something off of a null or undefined variable. Call a function without the right parameters. Don’t know the fields, or you put the wrong fields.

2.) Code Faster: Be able to know right away what the fields are, and if you’re missing fields.

npm init -y

// then 

npm run typescript ts-node

// then

npx tsc --init

Runs the files with the following command:

npx ts-node filename.ts

TS infers types and the golden hotkey to see what it is inferring is “ctrl k ctrl i”

let userName: string = "Brandon"
let hasLoggedIn: boolean = true
let myNumber: number = 10
let myRegex: RegExp = /foo/
let myArr: string[] = ["one", "two", "three"]

Another way to do arrays is with the generic type

const myValues: Array<number> = [1, 2, 3]

Objects

const myPerson: {
  first: string;
  last: string;
} = {
  first: "Tom",
  last: "Ace"
}

But you don’t want to copy and paste this all over the place so instead you will use an interface

interface Person {
  first: string;
  last: string;
}

const myPerson: Person = {
  first: "Tom",
  last: "Ace"
}

We oftentimes use objects as maps so here’s how we do that using a utility type called “Record”:

const ids: Record<number, string> = {
  10: "a",
  20: "b"
}

ids[30] = "c"

TS with conditionals is the same.

if (ids[30] === "D") {
  // the condition is false because it wants a number
}

And loops are the same, although if TS is inferring correctly then you don’t wan to set the type as you want to let TS infer as much as possible.

for (let i: number = 0; i < 10; i++) {
  console.log(i)
}

#2 – Typescript Functions

Exporting and importing functions from different files, TS does not like the “module.exports = functionName” syntax so instead use “export default functionName” and import with ‘const blank = require(“filename”)’ or use a named export.

What is the “any” type? It’s the type we are trying to avoid, it can be anything, (a number, a string, an array, an object, undefined, null, anything). We want to control the type and specify what we have.

Typing parameters and returns

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

Const functions (import with brackets to destructure)

export const addStrings = (str1: string, str2: string): string => {
  `${str1} ${str2}`
}

Default parameters

export const addStrings = (str1: string, str2: string = ""): string => {
  `${str1} ${str2}`
}

Union Types

export const format = (title: string, param: string | number): string => {
  `${title} ${param}`
}

Void functions. When you are not returning anything at all, (remember “console.log” is considered a side-effect).

export const printFormat = (title: string, param: string | number): void => {
  // format function defined earlier
  console.log(format(title, param))
}

Promise functions

export const fetchData = (url: string): Promise<string> => {
  Promise.resolve(`Data from ${url}`)
}

Note: if you get a warning that the promise is not defined go into you “tsconfig.json” file and change the “target” to “esnext.”

Rest parameters, (multiple arguments and then coalesce into an array)

function introduce(salutation: string, ...names: string[]): string  {
  return `${salutation} ${names.join(' ')}`
}

Biggest misconception is when types are enforced. Types are not enforced on runtime but on compilation time!

compile a ts file into a js file with the following command:

npx tsc filename.ts

// outputs a filename.js file

node filename.js

So if you are exporting some functions in a .ts module file and compile them down to a “.js” file, and import and use in a different “.js” file, you can still get errors if you call a function that is expecting arguments with no arguments so implement type safety not just with types but with some handy helper stuff like the optional chaining strategy. As an example back in the TS code,

export function getName(user: {first: string, last: string}): string {
  return `${user?.first} ${user?.last}`
}

// makes sure user is defined before we reference it

This will add some checking into the compiled version of the function to make sure the user actually exists before we can reference it.

export function getName(user: {first: string, last: string}): string {
  return `${user?.first ?? "first"} ${user?.last ?? "last"}`
}

// the nullish coalescing operator adds some defaults
// to prevent getting undefined undefined

#3 – Typescript Functions with Functions

Functions that take functions or create functions

function parameters

export function printToFile(text: string, callback: () => void): void {
  console.log(text)
  callback()
}

function with params

export function arrayMutate(numbers: number[], mutate: (v:number) => number): number []{
  return numbers.map(mutate)
}

console.log(arrayMutate([1, 2, 3], (v) => v.map(v * 10)))
// expected: [10, 20, 30]

If the above seems hard to read can convert to a function as a type, (this could be exported itself to be reused).

type mutationFunction = (v: number) => number

export function arrayMutate(numbers: number[], mutate: muationFunction): number[] {
  return numbers.map(mutate)
}

console.log(arrayMutate([1, 2, 3], (v) => v.map(v * 10)))
// expected: [10, 20, 30]

Returning functions

An example of a classic JS closure:

export function createAdder(num: number) {
  return (val: number) => num + v
}

const addOne = createAdder(1)
console.log(addOne(10))
// expected: 11 because the 1 was captured by the closure

So to define the output type in this:

export function createAdder(num: number): (val: number) => number  {
  return (val: number) => num + v
}

const addOne = createAdder(1)
console.log(addOne(10))
// expected: 11 because the 1 was captured by the closure

But same as before, if this reads funky you can define as a function type and reference that:

type AdderFunc = (val: number) => number

export function createAdder(num: number): AdderFunc  {
  return (val: number) => num + v
}

const addOne = createAdder(1)
console.log(addOne(10))
// expected: 11 because the 1 was captured by the closure

It really is worth getting familiar with functions that take in functions and function s that return functions because there is a lot of hype around functional programming and a lot of the frameworks use this stuff too.

#4 – Function Overloading in Typescript

Often overlooked feature of TS.

Want to create a function that parses coordinates but we want them to be able to be specified in a number of different ways.

interface Coordinate {
  x: number,
  y: number
}

Implementation #1

function parseCoordinateFromObject(obj: Coordinate): Coordinate {
  return {
    ...obj,
  }
}


function parseCoordinateFromNumbers(x: number, y: number): Coordinate {
  return {
    x,
    y,
  }
}

This works just fine but it doesn’t feel like JavaScript. Normally we would just have a “parseCoordinate” function that will look at what is coming in and depending on that, return the same. So the TS feature we are looking for to do this is called function overloading.

Implementation #2

Define the variants you want immediately above. Then define a function with parameters having an “unknown” type. What is an “unknown” type? It is basically an “any” but you have to cast it before you use it; kind of like a safe “any.”

Then you have to tell TS which parameters are optional and we do that with a question mark, (?). Then we use some typecasting with “typeof” and the TS keyword “as.”

function parseCoordinate(obj: Coordinate): Coordinate;
function parseCoordinate(x: number, y: number): Coordinate;
function parseCoordinates(arg1: unknown, arg2?: unknown): Coordinate {
  let coord: Coordinate {
    x: 0,
    y: 0,
  }

  if (typeof arg1 === "object") {
    coord = {
      ...(arg1 as Coordinate)
    }
  } else {
    coord = {
      x: arg1 as number,
      y: arg2 as number
    }
  }

  return coord;
}

console.log(parseCoordinate(10, 20))
console.log(parseCoordinate(x: 52, y: 35))

When we call the function above, we can see in the VSCode hinting, it says “1/2” meaning there are two signature you can choose from.

How can you handle a string variant, (“x:12,y:22”)? Define another variant above the exisiting ones and add another “else if” in the typecasting. Will need to run split method on the “,” and then a forEach that runs a split on the “:” and you will also need the “as” keyword for the key as well:

if (typeof arg1 === "string") {
  (arg1 as string).split(",").forEach(str => {
    const [key, value] = str.split(":")
    coord[key as "x" | "y"] = parseInt(value, 10)
  })
}

#5 – Optionals in Typescript

Optional parameters

function printIngredient(quantity: string, ingredient: string, extra?: string) {
  console.log(`${quantity} ${ingredient}`)
}

Optional parameters need to come at the end of the parameter list.

How can you check for an optional parameter’s existence?

// what a crazy example? nested backticks
console.log(${quantity} ${ingredient} ${extra ? `${extra}` : ""}`)

optional fields (like in an object):

interface User {
  id: string,
  info?: {
    email?: string
  }
}

// old school
// using ! means you know better than TS, it will be there
// if you're using ! a lot, you're probably not doing the right thing

function getEmail(user: User): string {
  if (user.info) {
    // return user.info.email!
  }
  return ""
}


// new school easy way
// notice the optional chaining feature
// also notice the nullish coalescing operator

function getEmailEasy(user: User): string {
  return user?.info?.email ?? ""
}


  

Optional callbacks

function addWithCallback(x:number, y:number, callback: () => void) {
  console.log([x, y])
  callback()
}

Callback could be optional by using question mark and then wrapping in conditional

function addWithCallback(x:number,y:number, callback?: () => void) {
  console.log([x, y])
  if (callback) {
    callback()
  }
}

or by using question mark (when defining parameter) and then using “?.” syntax

function addWithCallback(x:number,y:number, callback?: () => void) {
  console.log([x, y])
  callback?.()
}

#6 – Tuples in Typescript

type ThreeDCoordinate = [x: number, y: number, z: number]
function add3DCoordinate(c1: ThreeDCoordinate, c2: ThreeDCoordinate): ThreeDCoordinate {
  return [
    c1[0] + c2[0],
    c1[1] + c2[1],
    c1[2] + c2[2]
  ]
}

Another example: React devs comes across the “useState” tuple all the time.

function simpleStringState(initial: string): [() => string, (v:string) => void] {
  let str: string = initial
  return [
    () => str,
    (v: string) => {
      str = v
    }
  ]
}

// note this function is a closure

const [str1getter, str1setter] = simpleStringState("hello") 

#7 – Generics in Typescript

Continuing with example below what would happen if we replaced string with a different type, any type, (not the “any” type but a type that we specify).

function simpleState<T>(initial: T): [() => T, (v: T) => void] {
  let val: T = initial
  return [
    () => val,
    (v: T) => {
      val = v
    }
  ]
}

// note this function is a closure

const [state1getter, state1setter] = simpleState(10) 

Once we call the function with a number, every place where T is will become a number.

There is a small issue here when calling the function with null as initial state because we need to override T when we call it.

const [state1getter, state1setter] = simpleState<string | null>(null) 

Another example with a function called “ranker.”

function ranker<RankItem>(items: RankItem[], rank: (v: RankItem) => number): RankItem[] {

  interface Rank {
    item: RankItem;
    rank: number;
  }

  const ranks: Rank[] = items.map(item => {
    item,
    rank: rank(item)
  })

  ranks.sort((a, b) => a.rank - b.rank)

  return ranks.map(rank => rank.item)
}

But interface Rank only has access to RankItem since it is defined inside the function. So here is what we can do instead,

interface Rank<RankItem> {
    item: RankItem;
    rank: number;
  }

function ranker<RankItem>(items: RankItem[], rank: (v: RankItem) => number): RankItem[] {

  const ranks: Rank<RankItem>[] = items.map(item => {
    item,
    rank: rank(item)
  })

  ranks.sort((a, b) => a.rank - b.rank)

  return ranks.map(rank => rank.item)
}

A cool thing about TS is that you can make almost anything a generic, (class, function, interface, a type). Super powerful.

interface Pokemon {
  name: sting;
  hp: number;
}

const pokemon: Pokemon[] = [
  {
    name: "Bulb",
    hp: 20,
  },
  {
    name: "Pikachu",
    hp: 50,
  }
]

const ranks = ranker(pokemon, ({hp}) => hp)

console.log(ranks)

// generic ranking algorithm

#8 – Generics with keyof in Typescript

pluck example

function pluck<DataType, KeyType extends keyof DataType>(
  items: DataType[],
  key: KeyType
): DataType[KeyType][] {
  return items.map(item => item[key])
}

const dogs = [
  {name: "Mimi", age: 12},
  {name: "LG", age: 13}
]

console.log(pluck(dogs, ""))

Note: the reason this is cool is when you call the pluck function, when you put quote in the second argument you get hinting at what is available to you.

Example #2 Event Map

interface BaseEvent {
  time: number;
  user: string;
}

interface EventMap {
  addToCart: BaseEvent & {quantity: number; productID: string},
  checkout: BaseEvent
}

function sendEvent<Name extends keyof EventMap>(name: Name, data: EventMap[name]): void {
  console.log([name, data])
}

sendEvent("addToCart", {productID: 'foo', and more...})

Note the single ampersand meaning take this type and add it to this type.

The important thing here is that you cannot mess up.

#9 – Typescript Utility Types

Mechanism you can use to create another type from an existing type in interesting ways. “Partial” and “Record” are popular versions of these.

“Partial” easily makes a defined interfaces field’s optional

type MyUserOptionals = Partial<MyUser>

“Required” takes off optionality in a pre-defined interface

“Pick” is another utility type shown.

“Record” example:

const mapById = (users: NyUser[]): Record<string, MyUser> => {
  return users.reduce((a, b) => {
    return {
      ...a,
      [v.id]: v,
    }
  }, {})
}

console.log(mapById([
   {id: "green", name: "Mr. Green"},
   {id: "plum", name: "Professor Plum"}
]))

“Omit” does the opposite of “Pick”

#10 – Readonly And Immutability in Typescript

Not good you can change a cat’s name

add “readonly” in front of a field’s name

One way is to make a readonly cat:

function makeCat(name: string, breed: string): Readonly<Cat> {//...}

“readonly” tuples

the issue with “const”

const reallyConst = [1, 2, 3] as const

#11 – Enums and Literal Types in Typescript

enum LoadingState {
  beforeLoad = "beforeLoad",
  loading = ""loading,
  loaded = "loaded",
}

Note: don’t need to do the equality with the string but it may be a good idea to leave it for console.log purposes.

Literal types

roll dice example

constrain the number down to values of dice

Numeric Literals

String Literals

#12 – Typescript Classes; Member Visibility and Implements

Create a simple NoSQL style database

Member visibility is private, protected, public

class PersistentMemoryDB extends InMemoryDatabase implements Persistable {
  // ....
}

#13 – Generics in Typescript Classes

Continuing with the database example from episode 12 but want the type stored to not just be number. Make it flexible with generics.

<T, K>

TS #14 – Mapped Types in Typescript

function listenToObject<T>(obj: T, listeners): void {
  
}

using template literals and “Capitalize” utility type

type Listeners<Type> = {
  [Property in keyof Type as `on${Capitalize<string & Property>}Change`]: (newValue: Type[Property]) => void
}

#15 – Readonly and Static in Typescript Classes

Public in the constructor

class Doggy {
  constructor(public readonly name: string, public age: number) {
   // ...
  }
}

Statics: we can only have one “dog” list

class DogList {
  private doggies: Doggy[] = []

  static instance: DogList = new DogList()

  private constructor() {}
}

DogList.instance

#16 – Abstract Classes in Typescript

Modeling street fighters

Abstract means you can never instantiate it directly

abstract class StreetFighter {
  constructor() {}

  move() {}
  fight () {}

  abstract getSpecialAttack(): string {}
}

#17 – Mixins in Typescript

Add functionality to classes that you may or may not control

functions creating a generic type

#18 – Conditional Types in Typescript

Example with fetch function, pokemon api

import node-fetch

couldn’t find declaration file. Go to a community written types package.

import @types/node-fetch

shows multiple ways to do the same thing

#19 – Utility Types in Typescript – Part 2

“Parameters”

ConstructorType

ReturnType

InstanceType