Skip to content

Instantly share code, notes, and snippets.

@mathesond2
Created February 25, 2023 17:46
Show Gist options
  • Save mathesond2/31b4f1cde3bf16e4476e3ce9dfa8ca2e to your computer and use it in GitHub Desktop.
Save mathesond2/31b4f1cde3bf16e4476e3ce9dfa8ca2e to your computer and use it in GitHub Desktop.
Notes from Mike North's "Typescript Fundamentals" Frontend Master's course

Dictionaries

TL;DR: An object which stores objects/arrays by key

Sometimes we need to represent a type for dictionaries, where values of a consistent type are retrievable by keys.

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 signatures

TL;DR: type the key of a dictionary like []

Clearly, it seems that we can store phone numbers under a “key” — in this case home, office, fax, and possibly other words of our choosing — and each phone number is comprised of three strings. We could describe this value using what’s called an index signature:

const phones: {
  [k: string]: {
    country: string;
    area: string;
    number: string;
  };
} = {};
//now use it
phones.fax;

Now, no matter what key we lookup, we get an object that represents a phone number.

ALWAYS CHECK TO SEE IF TS CAN INFER TYPES BEFORE TYPING

Tuples

TL;DR: arrays of fixed length where index position has a specific meaning

Tuples - an array of fixed length, where the position of each item has some special meaning or convention

Ex:

let myCar = [2002, 'Toyota', 'Corolla'];
const [year, make, model] = myCar;

^ nice, but we dont have a fixed length here..we should explicitly state it:

let myCar: [number, string, string] = [2002, 'Toyota', 'Corolla'];
// ERROR: not the right convention
myCar = ['Honda', 2017, 'Accord'];
// Type 'string' is not assignable to type 'number'.
// Type 'number' is not assignable to type 'string'.

// ERROR: too many items
myCar = [2017, 'Honda', 'Accord', 'Sedan'];
// Type '[number, string, string, string]' is not assignable to type '[number, string, string]'.
// Source has 4 element(s) but target allows only 3.

Atm, we cannot enforce tuple length in TS, so you can push/pop() to your hearts delight and it wont err out. Its only caught at tuple initialization atm

Types of typing

  1. Static type systems - they make you write the types in your code (ex: ts, java)
  2. Dynamic type systems - run ‘type checking’ at runtime
  • Nominal type systems - all about names
  • Structural type systems = only care about data shape.
  • Duck typing - ‘if it looks like a duck, it is a duck’ - if it looks like this type and acts like it, then its ok...this is dynamic typing…’does this have the base info needed to do the thing? If not, it errs out at runtime (aka JS)

Union Types (OR → |)

example

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 = maybeGetUserInfo()
^ //what this actually means, type-wise:
const outcome: ["error", Error] | ["success", {
    name: string;
    email: string;
}]

However….we’ll notice that when we destructure the values of this return union type, we get some interesting possible problems:

const outcome = maybeGetUserInfo();

const [first, second] = outcome;
First; //evals to….:
const first: 'error' | 'success';

Second; //evals to….:
const second:
  | Error
  | {
      name: string;
      email: string;
    };

Now This sucks bc we know that second should never be an error, it should only be the success object. Also, when we try to access second.name, we’ll hit a type error bc theres no ‘name’ property on Error, and again with the union type, it could be either the Error or the success object.

How do we type/guard against this? We can narrow down our options w type guards.

Type Guards

TL;DR: expressions like instanceOf and typeOf, which when used w a control flow statement like switch() or if() allow us to have a more specific type for a particular value

Type guards - expressions, which when used w control flow statement (ex: a conditional), allows us to have a more specific type for a particular value.

“I like to think of these as ‘glue’ b/w compile time typing-checking and runtime execution of your code.

Ex: instanceOf

const outcome = maybeGetUserInfo();
const [first, second] = outcome;
// remember....it can be eithere here:
// const second: Error | {
//   name: string;
//   email: string;
// }

if (second instanceof Error) {
  // In this branch of your code, second is an Error
} else {
  // In this branch of your code, second is the user info
}

Intersection Types (AND → &)

TL;DR: define “all of this type and tack on this other type too”

Basically an & operator - its a merging of 2 types:

function makeWeek(): Date & { end: Date } {
 const start = new Date()
 const end = new Date(start.valueOf() + ONE_WEEK)
 return { ...start, end } // kind of like Object.assign
}

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

thisWeek.end.toISOString()
(property) end: Date

Type aliases and Interfaces

Type aliases

Type declarations disappear as part of the build process - no bloat!

Interfaces

A way of defining an ‘object type’...an object type can be thought of as ‘an instance of a class could possibly look like this”

So you couldn't do this: interface Blah = string | number

They come from class-based languages like Java...so you can extend/implements as you do with extending superclasses and implementing defined abstractions (contracts).

Function Overloads

TL;DR: define args/return statements for 2 diff scenarios of how a fn may be used...weird syntax

what if we need similar but slightly different functionality to occur for 2 separate elements, and we need this to be in calling the same singular function?

Basically you declare 2 versions of a fn w the same params but diff type values in those params, and on the 3rd fn declaration you give it {} to state that it could be either situation.

Heads - The first 2 fns declared Original implementation - 3rd fn

Imagine the following situation:

<iframe src="https://example.com" />
<!-- // -->
<form>
  <input type="text" name="name" />
  <input type="text" name="email" />
  <input type="password" name="password" />
  <input type="submit" value="Login" />
</form>

What if we had to create a function that allowed us to register a “main event listener”? If we are passed a form element, we should allow registration of a “submit callback” If we are passed an iframe element, we should allow registration of a ”postMessage callback” Let’s give it a shot:

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

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

const myFrame = document.getElementsByTagName("iframe")[0]
const myFrame: HTMLIFrameElement

handleMainEvent(myFrame, (val) => {
(parameter) val: any
})

This is not good — we are allowing too many possibilities here, including things we don’t aim to support (e.g., using a HTMLIFrameElement with FormSubmitHandler, which doesn’t make much sense).

We can solve this using function overloads, where we define multiple function heads that serve as entry points to a single implementation:

Check this out

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

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

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

Top Types

Types that describe anything (symbol: T)...could be a fn, a string, null, any damn thing. Thats what makes it a top type...TS gives us 2 top types: any and unknown.

Any

Sometimes its appropriate to use any...think of console.log()...it can console.log anything.

Unknown

TL;DR: ‘any’, but makes users of the resulting code do a type check before using

Like any but diff in an important way - it cant be used without applying a type guard (ex: typeOf, instanceOf)

let flexible: unknown = 4;
flexible = 'Download some more ram';
flexible = window.document;
flexible = setTimeout;

ex:

let myUnknown: unknown = 14
myUnknown.it.is.possible.to.access.any.deep.property
Object is of type 'unknown'.

// This code runs for { myUnknown| anything }
if (typeof myUnknown === "string") {
 // This code runs for { myUnknown| all strings }
 console.log(myUnknown, "is a string")
  let myUnknown: string
} else if (typeof myUnknown === "number") {
 // This code runs for { myUnknown| all numbers }
 console.log(myUnknown, "is a number")
  let myUnknown: number
} else {
 // this would run for "the leftovers"
 //       { myUnknown| anything except string or numbers }
}

So it has the flexibility of ‘any’ in terms of storing values, but places a responsibility on who uses it to check for what the type is before actually using it.

This useful for say, endpoint calls from a 3rd party where you don't know what the values may always be, but you make sure that the users of that data have to perform some light validation before using them...this way errs are caught earlier and can be surfaced w a bit more context

Ex: api response...response may/may not have a specific key (which has a value of a specific data shape) , you can type the api response as unknown and then check for that specific key. If you can validate that you have that key, you then know exactly what else you have.

Bottom Types

never type = Things that hold no possible value…”pick anything you want from this empty box”..pointless right? Not so fast..

Ex: ‘exhaustive conditionals’

class Car {
  drive() {
    console.log('vroom');
  }
}
class Truck {
  tow() {
    console.log('dragging something');
  }
}
type Vehicle = Truck | Car;

let myVehicle: Vehicle = obtainRandomVehicle();

// The exhaustive conditional
if (myVehicle instanceof Truck) {
  myVehicle.tow(); // Truck
} else if (myVehicle instanceof Car) {
  myVehicle.drive(); // Car
} else {
  // NEITHER!
  const neverValue: never = myVehicle;
}

Its either Car or Truck or nothing. Now lets add Boat as a vehicle type:

type Vehicle = Truck | Car;

let myVehicle: Vehicle = obtainRandomVehicle();

// The exhaustive conditional
if (myVehicle instanceof Truck) {
  myVehicle.tow(); // Truck
} else if (myVehicle instanceof Car) {
  myVehicle.drive(); // Car
} else {
  // NEITHER!
  const neverValue: never = myVehicle;
  //ERR: Type ‘Boat’  is not assignable to type ‘never’
}

^ we’ve basically alerted here that there's a new possibility for Vehicle, Boat, that we aren't taking care of here..

we can create an error subclass to provide a useful error message to show "hey, some shit that we never thought was gonna get hit did get hit, and heres what it is":

class UnreachableError extends Error {
  constructor(\_nvr: never, message: string) {
    super(message)
  }
}

^ (notice the never type)...use it like:

if (myVehicle instanceof Truck) {
myVehicle.tow() // Truck
} else if (myVehicle instanceof Car) {
myVehicle.drive() // Car
} else {
throw New UnreachableError(myVehicle, `Unreachable vehicle type:” ${myVehicle}`)
}

^ so 3 things happen in the final else block:

  1. We have handled every case before reaching it, making an "exhaustive conditional"
  2. We have alerted to the fact that theres a new use case for myVehicle that we havent considered, to catch changes that may appear upstream in an unrelated area of code.
  3. We throw a meaningful error

Type Guards and narrowing

Theres built-in type guards and user defined type guards...the link provides a good list of built-in ones, so lets peep user-defined ones..

User-defined

Often we dont just wanna check primitive types like string/number, but more complex objects like User, AccountDetails, etc.

Is

Returns a boolean but the boolean specifically checks for a Type.

Use case: you’ve got a fn that checks for a specific type and returns a boolean:

if (isBlah(someUnknownThing)) {
  someUnknownThing.blahPropertyOne;
}

^ the check might work but inside the resulting conditional, someUnknownThing is still just any. Using is within isBlah()s return type allows us to use blah type, so someUnknownThing will be a type of blah and have type safety inside the resulting conditional code block.

Ex:

Type isBlah {
  blahPropertyOne: string
  blahPropertyTwo: number
}

function isBlah(valueToTest: any): valueToTest is isBlah {
  return  //some check evaluation
}

This ensures that we then have type safety inside here:

if (isBlah(someUnknownThing)) {
  someUnknownThing.blahPropertyOne; //this is typed!!
}

Asserts

‘Asserts value is foo’....This fn throws an error if it's not a specific type, ex:

function isBlah(valueToTest: any): asserts valueToTest is isBlah {
  if (!someCheckEvaluation) throw new Error(`value does not appear to be isBlah: ${valueToTest}`);
}

Diff b/w the 2: asserts throws an error if is not type, is just checks if type

Nullish values

  • Null - the specific value of nothing
  • Undefined - the value is not available (yet?)
  • Void - explicitly for fn returns, tells TS that the return value should be ignored

Generics

We want type safety, but flexibility...we need a relationship b/w what we’re passed and what we’ll return.

We commonly use the keyword T to stand for a generic type. Most basic ex:

function wrapInArray<T>(arg: T): [T] {
  return [arg];
}

^ here we say, ‘here’s this generic type...its passed-in as an arg and returned out as a tuple of one.’

This is useful because we can use a variety of data types to work for this single function and we get type safety for all of them:

wrapInArray(3);
// TS: function wrapInArray<number>(arg: number): [number]
wrapInArray(new Date());
// TS: function wrapInArray<Date>(arg: Date): [Date]
wrapInArray(new RegExp('/s/'));
// TS: function wrapInArray<RegExp>(arg: RegExp): [RegExp]

Now check out this more detailed fn, which transforms an array to a dict

function listToDict<T>(list: T[], idGen: (arg: T) => string): { [k: string]: T } {
  const dict: { [k: string]: T } = {};

  list.forEach((element) => {
    const dictKey = idGen(element)
    dict[dictKey] = element
  });

  return dict;
}

Const dict1 = listToDict(
  [ {name: ‘David’}, {name: ‘Mike’} ],
  (item) => item.name
);

Resulting TS (type safety as we use dict1)

function listToDict<{name: string;}>(
  list: { name: string; }[],
  idGen: (arg: { name: string;}) => string): {
    [k: string]: { name: string; };
  }
const dict2 = listToDict(phoneList, (p) => p.customerId);

Resulting TS (type safety as we use dict2)

function listToDict<{customerId: string; areaCode: string; num: string; }>(
  list: {
    customerId: string;
    areaCode: string;
    num: string;
  }[],
  idGen: (arg: {
    customerId: string;
    areaCode: string;
    num: string;
  }) => string): {
      [k: string]: {
      customerId: string;
      areaCode: string;
      num: string;
    };
  }

Pretty dope. This gives us a linkage between a fn’s args and what’s returned, being generic af while having type safety throughout, regardless of scenario.

Giving constraints to generics

Bare min requirement for your generic type. “This type is generic, but it NEEDS to have this bare min req”...

Remember this guy?

function listToDict<T>(list: T[], idGen: (arg: T) => string): { [k: string]: T } {
  const dict: { [k: string]: T } = {};

  list.forEach((element) => {
    const dictKey = idGen(element);
    dict[dictKey] = element;
  });

  return dict;
}

Lets just look at the fn signature:

function listToDict<T>(list: T[], idGen: (arg: T) => string): { [k: string]: T } {
  return {};
}

Here we ask listToDict to always give us a way of getting an id (idGen), but lets imagine that every type we wish to use this with has an id property and we should just use that key instead of the supplied fn…

How would we do this w/o generics?

interface HasId {
  id: string;
}

interface Dict<T> {
  [k: string]: T;
}

function listToDict(list: HasId[]): Dict<HasId> {
  const dict: Dict<HasId> = {};

  list.forEach((item) => {
    //Notice we are accessing ‘id’ directly
    dict[item.id] = item;
  });

  return dict;
}

So this works...we say,’hey, gimme a list where every object has at least an id, and we’ll return a dict of objects where each one has an id’.

Lets do this with generics:

interface HasId {
  id: string;
}

interface Dict<T> {
  [k: string]: T;
}

function listToDict<T>(list: T[]): Dict<T> {
  const dict: Dict<T> = {};

  list.forEach((item) => {
    //ERROR: property ‘id’ does not exist on type T
    dict[item.id] = item;
  });

  return dict;
}

We have an err here bc before we were using a supplied fn to find the id and now we’re asking for it directly. We need to add a specific constraint to our generic type.

We need to say,”hey this is a generic type, but we are guaranteed that it will have the ID property we can use”

From:

function listToDict(list: HasId[]): Dict<HasId> {

To:

function listToDict<T extends HasId>(list: T[]): Dict<T> {

We describe constraints to the generic type: “hey this is generic but it at least always has this one thing.”

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