Typed JavaScript

Why Types in JS?

  • Static-time analysis.
  • Readability and Intent
  • Tooling
  • Performance?

Wait... Performance?

It's not guaranteed, but an interesting idea to entertain

Current Type Solutions for JS

TypeScript

  • It's a superset of JavaScript.

TypeScript

function name (obj) {
  return obj?.name;
}
  
function init (opts) {
  const isEnabled = opts.enabled ?? true;
}

Use advanced JavaScript features like optional-chaining and null-coalescing operators without worrying about browser support.

Type-system Types

Nominal Typing: Is X an instance of type Y?

Structural Typing: Checks against the structure, only cares about the shape of an object.

TypeScript has structural typing system

Examples: All strongly-typed languages

Examples: Haskell, OCaml

TypeScript has structural typing system

Nominal vs Structural

class Dog {
  breed: string;
  constructor(breed: string) {
    this.breed = breed;
  }
}

function printDog(dog: Dog) {
  console.log("Dog: " + dog.breed);
}

Nominal vs Structural

class Cat {
  breed: string;
  constructor(breed: string) {
    this.breed = breed;
  }
}

const kitten = new Cat("Maine Coon");
printDog(kitten);

Structural Type System in a nutshell

Think in Structures not types

function print ({ name }: { name: string; }) {
  console.log(name);
}

function print (dog: Dog) {
  console.log(dog.name);
}

function print (name: string) {
  console.log(name);
}

Type Specificity

Wider vs Narrower

any
any[]
string[]
[string, string, string]
["abc", string, string]
....
never

Wider vs. Narrower

const greet1 = "Hello";
let greet2 = "Hello";


function print (value: typeof greet1) {
  // ...
}

test(greet1); // OK
test(greet2); // ERROR

Declaration Spaces

Type Declaration Space

interface User {
  name: string;
  age: number;
}

type LiterallyHello = 'Hello';

const user = User; // ERROR
const hello = LiterallyHello; // ERROR

Value Declaration Space

let value = '1234';

let another: value; // ERROR

So what about this?

class User {
  // ...
}

const user = User;
const another: User = new User();

Converting JS ▶ TS

3 Steps

Don't

  • Introduce functional changes.
  • Type stuff too strongly too early.
  • Attempt without proper testing methodology in place.

Step 1: Loose Mode

  • Allow implicit any
  • Rename .js files to .ts files
  • Fix compilation errors
  • Don't change behavior
  • Test

Step 2: Explicit Any

  • Set "noImplicitAny" to true in tsconfig.json
  • For every error, provide a specific type
    • Use `DefinitelyTyped` packages.
    • Explicitly use `any` when you cannot type it.

Step 3: Strict Mode

  • Enable "strict" mode by enabling:
    • strictNullChecks
    • strict
    • strictFunctionTypes
    • strictBindCallApply
  • Replace explicit any with stronger types.
  • Avoid unsafe casts with the `as` keyword.
  • Test

Generics

😱

Generics

function wrap (value) {
  return {
    value
  };
};

const num = wrap(10);
const str = wrap("hi");

num.value; // ???
str.value; // ???

How can we type the `wrap` function?

Generics

function wrap<T>(value: T) {
  return {
    value
  };
};

const num = wrap(10);
const str = wrap("hi");

num.value; // number
str.value; // string

Generics

function filter<T>(arr: T[], fn: (item: T) => boolean) {
  const acc: T[] = [];
  for (let i = 0; i < arr.length; i++) {
    if (fn(arr[i])) {
      acc.push(arr[i]);
    }
  }

  return acc;
}

const test = [1, 2, 3, 4, 5];
// X will be treated as a number
filter(test, x => x % 2 === 0);

Generics

function map<T, U>(arr: T[], fn: (item: T) => U) {
  const acc: U[] = [];
  for (let i = 0; i < arr.length; i++) {
    acc.push(fn(arr[i]));
  }

  return acc;
}

const test = [1, 2, 3, 4, 5];
// X will be treated as a number
const out = map(test, x => x % 2 === 0); // array of booleans

Generics

function useMutation<TData = any, TVars = QueryVariables>(mutation) {
  // ...
}

Top and Bottom Types

Exhaustive Conditionals and Type Guards

Top Types

any: Literally anything

Types that can hold any type of values

unknown: Literally 🤷‍♂️

const anything: any = 1231231;
const nani: unknown = 'Hello there!';

console.log(anything.asdhhasd.asdasd);
console.log(nani.split('')); // ERROR

Top Types

const nani: unknown = 'Hello there!';

if (typeof nani === 'string') {
  console.log(nani.split('')); // OK
}

(nani as string).split(''); // OK

Bottom Types

never: ERROR IN THE SIMULATION

Types that cannot hold any type of values

let x: string | number = '123';
if (typeof x === 'string') {
  // In this block, x is a string
  x.split('');
} else if (typeof x === 'number') {
  // In this block, x is a number
  x.toFixed(2);
} else {
  // In this block, x is a `never`
  // because there is no way it happens
  x; // never
  throw new Error('Someone messed up!');
}
export type KnownKeys<T> = {
  [K in keyof T]: string extends K ? never : number extends K ? never : K;
} extends { [_ in keyof T]: infer U }
  ? U
  : never;

interface User {
  name: string;
  age: number;
  [k: string]: string;
}

type UserKnownKeys = KnownKeys<User>;

This is a type that accepts an interface and extracts only the named keys from it

And to blow your mind 🤯

Resources

Thank You 👋

Typed JavaScript

By Abdelrahman Awad

Typed JavaScript

  • 1,050