TYPES ARE

GREAT?

LET'S TRY A...

REAL WORLD EXAMPLE

LET'S USE TYPESCRIPT TO...

FETCH DATA

OUR EXAMPLE:

// common.ts
interface User { id: number, name: string }
interface Todo { id: number, title: string }

// server.ts
function appGet(
    url: string, 
    fn: (req: Request) => Promise<any>): void;

// client.ts
function fetch(url: string): Promise<any>;

QUICK TIP: DECLARE & THEN USE!

// client.ts
declare function fetch(url: string): Promise<any>;

// client-usage.ts
const users: User[] = await fetch("/users")

LESSON LEARNED

USE DECLARE 1st

FEW EXAMPLE USAGES:

// client-usage.ts
const users: User[] = await fetch("/users")
const todos: Todo[] = await fetch("/todos")

const users: User[] = await fetch("/user")
const todos: Todo[] = await fetch("/users")

const users: User[] = await fetch(42)
const users: User[] = await fetch({ users: true})

LET'S COMPILE!

// client-usage.ts
const users: User[] = await fetch("/users")
const todos: Todo[] = await fetch("/todos")

const users: User[] = await fetch("/user")
const todos: Todo[] = await fetch("/users")

const users: User[] = await fetch(42)
// => ERROR: number is not assignable to string
const users: User[] = await fetch({ users: true})
// => ERROR: object is not assignable to string

CAN WE SHIP IT?

🥺

 compiles

❔ it runs?

TypeError: failed to fetch

 compiles

it does not run

🤔

TYPES ARE

💩

REDUCE CODEBASE BY 25%

// client-usage.ts
const users: User[] = await fetch("/users")
const todos: Todo[] = await fetch("/todos")

const users: User[] = await fetch("/user")
const todos: Todo[] = await fetch("/users")

DON'T USE TYPES

THE END

HAVE A GREAT COFFE BREAK!

WAIT A MOMENT

IT'S NOT SO EASY...

LET'S TAKE A STEP BACK:

// client-usage.ts
const users: User[] = await fetch("/users")
const todos: Todo[] = await fetch("/todos")

const users: User[] = await fetch("/user")
const todos: Todo[] = await fetch("/users")

LET'S TAKE A STEP BACK:

// common.ts
interface User { id: number, name: string }
interface Todo { id: number, title: string }

// server.ts
function appGet(
    url: string, 
    fn: (req: Request) => Promise<any>): void;

// client.ts
function fetch(url: string): Promise<any>;

THE SYLVESTER THE CAT PROBLEM:

"Sylvester is a cat, a cat is not Sylvester"

THE URL PROBLEM:

"URL are strings, any string is not a valid URL"

URL
string

USING STRICTER TYPES:

// common.ts
interface User { id: number, name: string }
interface Todo { id: number, title: string }
type URL = "/users" | "/todos"

// server.ts
function appGet(
    url: URL, 
    fn: (req: Request) => Promise<any>): void;

// client.ts
function fetch(url: URL): Promise<any>;

USING STRICTER TYPES:

// client-usage.ts
const users: User[] = await fetch("/users")
const todos: Todo[] = await fetch("/todos")

const todos: Todo[] = await fetch("/users")


const users: User[] = await fetch("/user")
// => ERROR: "/user" is not assignable to "/users" | "/todos"

LESSON LEARNED

OOTB TYPES

ARE TOO WIDE

LESSON LEARNED

DOMAIN TYPES

ARE FUNDAMENTAL

CAN WE SHIP IT?

🥺

 compiles

❔ it runs?

title is undefined

 compiles

it does not run

0
 Advanced issues found

🤔

WHAT'S THE PROBLEM?

// client-usage.ts
const users: User[] = await fetch("/users")
const todos: Todo[] = await fetch("/todos")

const todos: Todo[] = await fetch("/users")

🐰 "Uhm, what's up doc?"

LESSON LEARNED

ALL HUMANS MAKE MISTAKES

LOWERING THE ERROR POSSIBILITIES

// client-usage.ts
const users: User[] = await fetch("/users")
const todos: Todo[] = await fetch("/todos")

const todos: Todo[] = await fetch("/users")

USING STRICTER TYPES:

// common.ts
interface User { id: number, name: string }
interface Todo { id: number, title: string }
type URL = "/users" | "/todos"

// server.ts
function appGet(
    url: URL, 
    fn: (req: Request) => Promise<any>): void;

// client.ts
function fetch(url: URL): Promise<any>;

USING STRICTER TYPES:

// server.ts
function appGet(
    url: "/users", 
    fn: (req: Request) => Promise<User[]>): void;
function appGet(
    url: "/todos", 
    fn: (req: Request) => Promise<Todo[]>): void;
/* ... */

// client.ts
function fetch(url: "/users"): Promise<User[]>;
function fetch(url: "/todos"): Promise<Todo[]>;
/* ... */

WHAT IF... THE INFERENCE JUST KNOWS IT?

// client-usage.ts
const users = await fetch("/users")
const todos = await fetch("/todos")

const todos = await fetch("/users")

😍 "Look Mum! No types!"

IS IT REALLY DRY?

// server.ts
function appGet(
    url: "/users", 
    fn: (req: Request) => Promise<User[]>): void;
function appGet(
    url: "/todos", 
    fn: (req: Request) => Promise<Todo[]>): void;
function appGet(
    url: "/tacos", 
    fn: (req: Request) => Promise<Taco[]>): void;

// client.ts
function fetch(url: "/users"): Promise<User[]>;
function fetch(url: "/todos"): Promise<Todo[]>;
function fetch(url: "/tacos"): Promise<Taco[]>;

TYPE-LEVEL FUNCTIONS MEANS...

TYPE-LEVEL PROGRAMMING

JavaScript

on v8 in the browser or NodeJS

Types

on the type-system in the editor or the compiler

My editor is yelling at me

TAKING A STEP BACK...

TYPESCRIPT FEATURES

STRING LITERALS

type UsersURL = "/users"

// correct assignment
const users: UsersURL = "/users"

// incorrect assignment
const users: UsersURL = "/todos"

// string is not /users statically
const stringVar: string = "/users"
const users: UsersURL = stringVar

OBJECT TYPES (OR INTERFACES)

type User = {
    id: number,
    name: string
    surname: string
    age: number
    prefix: "mr." | "mss."
}

KEY OF

type User = {
    id: number,
    name: string
    surname: string
    age: number
    prefix: "mr." | "mss."
}

type A = keyof User;
// => id | name | surname | age | prefix

PROPERTY TYPE ACCESSING


type A = User["name"]; 
// => string

type B = User["age"]; 
// => number

type C = User["name" | "age"]; 
// => string | number

something 🐟y is about to happen

🤔

LET'S CHANGE OUR DOMAIN TYPES

type RoutesMap = {
    "/users": User[],
    "/todos": Todo[]
}

type URL = keyof RoutesMap

🎉 OUR FIRST TYPE-LEVEL FUNCTION! 🎉

type RoutesMap = {
    "/users": User[],
    "/todos": Todo[]
}
type URL = keyof RoutesMap

type GetResponseType<U extends URL> = RoutesMap[U]

type A = GetResponseType<"/users">
// => User[]
type B = GetResponseType<"/todos">
// => Todo[]
type C = GetResponseType<"/batman">
// => ERROR: batman is not a valid URL
// server.ts
function appGet<U extends URL>(
    url: U, 
    fn: (req: Request) => Promise<GetResponseType<U>>
  ): void;

// client.ts
function fetch<U extends URL>(url: U): Promise<GetResponseType<U>>;

🎉 OUR FIRST TYPE-LEVEL FUNCTION! 🎉

IN THE END...WE DID IT!

// client-usage.ts
const users = await fetch("/users")
// => User[]
const todos = await fetch("/todos")
// => Todo[]

const todos = await fetch("/users")
// => User[]

😍 "Look Mum! 25% of code reduction!"

WHAT IF...

PARAMETRIZED URLs

PARAMETRIZED URLs

// client-usage.ts
const john = await fetch("/users/1")
const mattia = await fetch("/users/2")
const doe = await fetch("/users/3")

const mattiaPhoto = await fetch("/users/2/photos/42")

🤔 "We cannot encode any possible string"

EVENTUALLY WE'LL NEED TO...

CHANGE SIGNATURE

PARAMETRIZED URLs

const john = await fetch("/users/{id}")(1)
// => User
const mattia = await fetch("/users/{id}")(2)
// => User
const mattiaPhoto = await fetch("/users/{id}/photos/{photo_id}")(2, 42)
// => Photo

PARAMETRIZED URLs

type RoutesMap = {
  "/users": 
  	{ args: [], response: User[] },
  "/users/{id}": 
  	{ args: [number], response: User },
  "/users/{id}/photos/{photo_id}":
  	{ args: [number, number], response: Photo}
}
/* ... */
type GetResponseType<U extends URL> = 
	RoutesMap[U]["response"]
type GetRouteArgs<U extends URL> = 
	RoutesMap[U]["args"]

ANOTHER USE CASE...

TYPED FSM

A TRIBUTE TO K🎹

TYPED FINITE STATE MACHINE

HOME

WORK

BED

train

train

sleep

wakeup

TYPED FINITE STATE MACHINE

type DailyMachine = {
  work: {
    train: "home"
  },
  home: {
    train: "work",
    sleep: "bed"
  },
  bed: {
    wakeup: "home"
  }
}

TYPED FINITE STATE MACHINE

type DailyMachine = {
  work: { /* ... */ },
  home: { /* ... */ },
  bed: { /* ... */ }
}

type MachineStates = keyof DailyMachine
// => work | home | bed

TYPED FINITE STATE MACHINE

type DailyMachine = { /* ... */
  home: {
    train: "work",
    sleep: "bed"
  }, /* ... */
}

type GetAvailableEvents<
  S extends MachineStates
> = keyof DailyMachine[S]

type Test1 = GetAvailableEvents<"home">
// => sleep | train

TYPED FINITE STATE MACHINE

type DailyMachine = { /* ... */
  home: {
    train: "work",
    sleep: "bed"
  }, /* ... */
}

type GetNextState<
  C extends MachineStates, 
  E extends GetAvailableEvents<C>
> = DailyMachine[C][E]

type Test1 = GetNextState<"home", "sleep">
// => bed

TYPED FINITE STATE MACHINE

declare function transition<
  S extends MachineStates, 
  E extends GetAvailableEvents<S>
>(
  state: S,
  event: E
): GetNextState<S, E>;

const newState1 = transition("bed", "wakeup") 
// => home
const newState2 = transition("home", "train") 
// => work
const newState3 = transition("work", "sleep") 
// => CompileError

AND REMEMBER...

0 JAVASCRIPT EMITTED

JUST TYPES! :D

🤯

LET'S HAVE SOME...

FUN!

HOW FAR CAN TYPE-LEVEL LOGIC GO?

WHAT ABOUT...

TYPE-LEVEL BOOLEAN LOGIC?

AT THE VERY BEGINNING... BOOLEAN LOGIC

// typelevel-booleans.ts
type TLTrue = "T"
type TLFalse = "F"

type TLBoolean = TLTrue | TLFalse

const a: TLTrue = "T" // => OK
const a: TLTrue = true // => ERROR

AT THE VERY BEGINNING... BOOLEAN LOGIC

type TLIf<A extends TLBoolean, IfTrue, IfFalse> = {
  "T": IfTrue,
  "F": IfFalse
}[A]

type Test1 = TLIf<TLTrue, 1, 2> // => 1
type Test2 = TLIf<TLFalse, 1, 2> // => 2

AT THE VERY BEGINNING... BOOLEAN LOGIC

// my first program
type TLSampleFunction<
  AtAlicante extends TLBoolean
  > = TLIf<
    AtAlicante, 
    "Hello React Alicante!", 
    "I'm not done with the slides yet."
  >
type TestA = TLSampleFunction<TLTrue>
// => Hello React Alicante!
type TestB = TLSampleFunction<TLFalse>
// => I'm not done with the slides yet.

AT THE VERY BEGINNING... BOOLEAN LOGIC

type TLAnd<A extends TLBoolean, B extends TLBoolean> = {
  "F": TLFalse,
  "T": B
}[A]

type Test1 = TLAnd<"T", "F"> // => F
type Test2 = TLAnd<"T", "T"> // => T
type Test3 = TLAnd<"F", "T"> // => F
type Test4 = TLAnd<"F", "F"> // => F

AT THE VERY BEGINNING... BOOLEAN LOGIC

type TLOr<A extends TLBoolean, B extends TLBoolean> = {
  "T": TLTrue,
  "F": B
}[A]

type Test5 = TLOr<"T", "F"> // => T
type Test6 = TLOr<"T", "T"> // => T
type Test7 = TLOr<"F", "T"> // => T
type Test8 = TLOr<"F", "F"> // => F

WHAT ABOUT...

TYPE-LEVEL NATURAL NUMBERS?

(ANY INTEGER POSITIVE NUMBER) 

NATURAL NUMBERS

type TLZero = {
  isZero: TLTrue,
  prev: never
}
type TLOne = {
  isZero: TLFalse,
  prev: TLZero
}
type TLTwo = {
  isZero: TLFalse,
  prev: TLOne
}
type TLNext<N extends TLNat> = {
  isZero: TLFalse,
  prev: N
} 

type TLOne = TLNext<TLZero>
type TLTwo = TLNext<TLOne>
type TLThree = 
  TLNext<TLNext<TLNext<Zero>>>

NATURAL NUMBERS

// some utilities
type IsZero<N extends TLNat> = N["isZero"]

type Test1 = IsZero<TLZero> // => "T"
type Test2 = IsZero<TLOne> // => F

NATURAL NUMBERS

// some utilities
type TLPrev<N extends TLNat> = N["prev"]

type Test1 = TLPrev<TLTwo> // => TLOne
type Test2 = TLPrev<TLZero> // => never

NATURAL NUMBERS

type SeemsSuspicious<N extends TLNat> = {
  "F": { NaNa: SeemsSuspicious<TLPrev<N>>},
  "T": "Batman!"
}[IsZero<N>]

type GuessWhat = SeemsSuspicious<TLEight>

NATURAL NUMBERS

type GuessWhat = {
    NaNa: {
        NaNa: {
            NaNa: {
                NaNa: {
                    NaNa: {
                        NaNa: {
                            NaNa: {
                                NaNa: "Batman!";
                            };
                        };
                    };
                };
            };
        };
    };
}

TYPE-LEVEL IS...

FUN

TYPE-LEVEL IS...

JUST AN HACK

TYPE-LEVEL IS...

DANGEROUS

CONDITIONAL TYPES

MAPPED TYPES

PHANTOM FIELDS

TYPEOF OPERATOR

TYPE LEVEL HETEROGENEOUS LISTS

EVERYONE...

THANKS FOR YOUR TIME!

@MATTIAMANZATI

TypeLevel programming fun

By mattiamanzati

TypeLevel programming fun

  • 1,812