Luca Del Puppo - Senior Software Developer

Dive into TypeScript

Hi, I born in 2012 in a Microsoft repository

 

  • I love types and improving the JavaScript codebases

  • Not all are happy about me

  • I’m here to simplify your job and prevent mistakes

  • JavaScript is my friend and I always support it

And now I'm a super hero 🚀

Or maybe not

Luca Del Puppo

  • Senior Software Engineer
  • JavaScript enthusiast
  • TypeScript lover
  • “Youtuber”
  • “Writer”

Love sport: running, hiking

Love animals

TypeScript vs JavaScript

This is JavaScript

function main(name) {
   console.log(`Hello from ${name}`)
}

This is TypeScript

function main(name: string): void {
   console.log(`Hello from ${name}`)
}

TypeScript

JavaScript

TypeScript Code

Compilation/Transpiling

Vanilla JavaScript

TypeScript File (*.ts)

Classes, Functions Types ..

TypeScript Compiler (tsc)

Target: ES3, ES6, ES7...

JavaScript File (*.js)

Runs Everywhere

When should you use me?

  • Big Projects

  • Many Developers

  • Shared Libraries

  • Strong type applications

When shouldn't you use me?

  • Dynamic objects (but real dynamic, ehm...)

  • Small libraries with performance purposes

  • Side project in your own room *

* but that depends on you 😅

Types fundamentals

Commons

  • number
  • string
  • boolean
  • bigint
  • object
  • function
  • array
  • symbols
let n: number = 1
let s: string = 'hello'
let isDone: boolean = false
let b: boolean = true
let o: object = { foo: 'bar' }
let a: string[] = ['hello', 'world']
let a2: Array<string> = ['hello', 'world']
let f: Function = function (x: number, y: number): number {
  return x + y
}
let f2: (x: number, y: number) => number =
    (x, y) => x + y
let sy = Symbol('key')

Commons

  • null
  • undefined
let n: null = null
let u: undefined = undefined
let o: {foo?: string} = {foo: undefined}

Any vs Unknown

let myAny: any = true
myAny.trim().reverse().split(',');
let myAnyNumeric: number = myAny;
const sum = myAnyNumeric + 3;

Good luck in production 

Typical developers behaviour

let myUnknown: unknown = true;
myUnknown.trim();
let myAnyNumeric: number = myUnknown;
if (typeof myUnknown === 'string') {
  myUnknown.trim();
}
if (typeof myUnknown === "number") {
  let myAnyNumeric: number = myUnknown;
  const sum = myAnyNumeric + 3;
}

 'myUnknown' is of type 'unknown'.ts(18046)

Type 'unknown' is not assignable to type 'number'.ts(2322)

Custom Types

type Square = {
  size: number,
}

type Rectangle = {
  width: number,
  height: number,
}

type Circle = {
  radius: number,
}
interface Square {
  size: number,
}

interface Rectangle {
  width: number,
  height: number,
}

interface Circle {
  radius: number,
}

Interfaces Merging

interface Rectangle {
  width: number,
}

interface Rectangle {
  height: number,
}

let rect: Rectangle = {
  width: 100,
  height: 200,
}
interface Rectangle {
  width: number,
  height: number,
}

Merge

Classes

Classes are part of Javascript

  • Member Visibility
    public, protected, private, #

  • Method

  • Getter/Setter

  • Static

  • Implements/Extends

class Shape {
  private square: Square;

  constructor(square: Square, public rectangle: Rectangle, private circle: Circle) {
    this.square = square;
  }

  getSquare(): Square {
    return this.square;
  }

  getRectangle(): Rectangle {
    return this.rectangle;
  }

  getCircle(): Circle {
    return this.circle;
  }
}

let square: Square = { size: 10 };
let rectangle: Rectangle = { width: 10, height: 20 };
let circle: Circle = { radius: 5 };

let shape = new Shape(square, rectangle, circle);

"Advanced" TypeScript

Literals

type Ingredient = string

type PizzaIngredient = 'flour' | 'mozzarella' 
			| 'tomato' | 'pepperoni'
const ingredientOne: PizzaIngredient = 'flour'
const ingredientTwo: PizzaIngredient = 'tomato'
const ingredientThree: PizzaIngredient = 'mozzare'

Type '"mozzare"' is not assignable to type 'PizzaIngredient'.ts(2322)

type Name = 'John' | 'Jane' | 'Jack';
type Surname = 'Doe' | 'Dane' | 'Black';

type FullName = `${Name} ${Surname}`;

let JohnDoe: FullName = 'John Doe';
let JaneDane: FullName = 'Jane Dane';
let JackBlack: FullName = 'Jack Black';
let who: FullName = 'John Blue'; 

Type '"John Blue"' is not assignable to type
'"John Doe" | "John Dane" | "John Black" | "Jane Doe"
| "Jane Dane" | "Jane Black" | "Jack Doe" | "Jack Dane"
| "Jack Black"'.ts(2322)

Intersections &

type Point1D = { x: number }
type Point2D = Point1D & { y: number }
type Point3D = Point2D & { z: number }

Union |

type Dish = 'Pizza' | 'Pineapple'

function order (dish: Dish) {
  switch (dish) {
    case 'Pizza':
      return '🍕'
    case 'Pineapple':
      return '🍍'
  }
}

Never

type Dish = 'pizza' | 'pasta' | 'sandwich'

function doOrder(dish: Dish) {
    switch (dish) {
        case 'pizza':
            return '🍕'
        case 'pasta':
            return '🍝'
        default:
            let unknownDish: never = dish;
            throw new Error(`Dish ${unknownDish} doesn't exists`)
    }
}

Type 'string' is not assignable
to type 'never'.(2322)

Control your flow

Assertion functions

function assert(conditional: unknown, message: string):
    asserts conditional {
  if (!conditional) {
    throw new Error(message);
  } 
}

assert({}, 'Should be defined')
assert(10, 'Should be defined')
assert(undefined, 'Should be defined')
// Error: Should be defined
type Person = {name: string;};

let person: Person | undefined;

function assertIsDefined(person: undefined | Person): asserts person is Person {
  if (person === undefined) {
    throw new Error("Person should be defined");
  } 
}

function printPersonWithoutCheck() {
  console.log(person.name);
  // 'person' is possibly 'undefined'.ts(18048)
}

function printPersonWithCheck() {
  assertIsDefined(person);
  console.log(person.name);
}

Type guards

type Square = { kind: 'square', size: number }
type Circle = { kind: 'circle', radius: number }
type Shape = Square | Circle

function isSquare(shape: Shape): shape is Square {
  return shape.kind === 'square'
}
function isCircle(shape: Shape): shape is Circle {
  return shape.kind === 'circle'
}

function area(shape: Shape): number {
  if (isSquare(shape))
    return shape.size * shape.size
  if (isCircle(shape)) 
    return Math.PI * shape.radius ** 2
}

Conditional Types

type Apple = { kind: 'fruit' }
type Strawberry = { kind: 'fruit' }
type Car = { kind: 'vehicle' }
type Truck = { kind: 'vehicle' }
type Tomato = { kind: 'vegetable' }
type Cucumber = { kind: 'vegetable' }

type AllTogether = Apple | Strawberry | Car | Truck | Tomato | Cucumber

type GetFruits<T> = T extends { kind: 'fruit' } ? T : never
type GetVehicles<T> = T extends { kind: 'vehicle' } ? T : never
type GetVegetables<T> = T extends { kind: 'vegetable' } ? T : never

type Fruits = GetFruits<AllTogether> // Apple | Strawberry
type Vehicles = GetVehicles<AllTogether> // Car | Truck
type Vegetables = GetVegetables<AllTogether> // Tomato | Cucumber

Advanced Utilities

Index Signature

type Obj = {
  [key: string]: unknown
}
let obj: Obj = {
  name: 'Jack',
  age: 18
}
type Obj = Record<string, unknown>

Mapped Types

keyof

type Person = {
  name: string;
  surname: string;
  age: number;
}
type PersonKeys = keyof Person;
// 'name' | 'surname' | 'age'
type PersonTypes = Person[keyof Person];
// string | number

Readonly

type Person = {
  readonly name: string
  readonly surname: string
}
let obj: Person = {
  name: 'Jack',
  surname: 'Sparrow',
}

obj.name = 'John'
// Index signature in type 'Obj'
// only permits reading.ts(2542)
type PersonReadonly = Readonly<{
  name: string
  surname: string
}>

Readonly under the hood

type Readonly<T> = {
  +readonly [K in keyof T]: T[K]
}

Sugar syntax

const person = {
  name: 'John',
  surname: 'Doe',
  age: 30
} as const;

NoReadonly

type Person = {
  readonly name: string
  readonly surname: string
}
type NoReadonly<T> = {
  -readonly [K in keyof T]: T[K]
}
type NoReadonlyPerson = NoReadonly<Person>

let obj: NoReadonlyObj = {
  name: 'Jack',
  surname: 'Sparrow',
}

obj.name = 'John'

Partial

type Person = {
  name?: string
  surname?: string
}
let obj: Person = {
  name: 'Jack',
}
type Person = Partial<{
  name: string
  surname: string
}>

Partial under the hood

type Partial<T> = {
  [K in keyof T]+?: T[K]
}

Required

type Person = {
  name: string
  surname: string
}
let obj: Person = {
  name: 'Jack',
  surname: 'Black'
}
type Obj = Required<{
  name?: string
  surname?: string
}>

Required under the hood

type Required<T> = {
  [K in keyof T]-?: T[K]
}

Many others utilities

TypeScript Tips

TypeScript Documentations

BUT I ALREADY HAVE MANY .JS FILES

Definitions File .d.ts

import { createError } from '@middy/util'

const wsRouteHandler = (routes) => {
  const routesStatic = {}
  for (const route of routes) {
    const { routeKey, handler } = route

    // Static
    routesStatic[routeKey] = handler
  }

  return (event, context, abort) => {
    const { routeKey } = event.requestContext ?? {}
    if (!routeKey) {
      throw new Error('[ws-router] Unknown ws event format')
    }

    // Static
    const handler = routesStatic[routeKey]
    if (typeof handler !== 'undefined') {
      return handler(event, context, abort)
    }

    // Not Found
    throw createError(404, 'Route does not exist')
  }
}

export default wsRouteHandler
import middy from '@middy/core'
import { APIGatewayProxyWebsocketHandlerV2 } from 'aws-lambda'

interface Route<T = never> {
  routeKey: string
  handler: APIGatewayProxyWebsocketHandlerV2<T>
}

declare function wsRouterHandler (routes: Route[]): middy.MiddyfiedHandler

export default wsRouterHandler

index.d.ts

Remember to export it

{
  ...
  "main": "./index.cjs",
  "module": "./index.js",
  "exports": {
    ".": {
      "import": {
        "types": "./index.d.ts",
        "default": "./index.js"
      },
      "require": {
        "types": "./index.d.ts",
        "default": "./index.cjs"
      }
    }
  },
  "types": "index.d.ts",
  "files": [
    "index.js",
    "index.cjs",
    "index.d.ts"
  ],
  ...
}

Do you know you should test your types?

import middy from '@middy/core'
import { APIGatewayProxyWebsocketHandlerV2 } from 'aws-lambda'
import { expectType } from 'tsd'
import wsRouterHandler from '.'

const connectLambdaHandler: APIGatewayProxyWebsocketHandlerV2 = async () => {
  return {
    statusCode: 200,
    body: 'Connected to websocket'
  }
}

const middleware = wsRouterHandler([
  {
    routeKey: '$connect',
    handler: connectLambdaHandler
  }
])
expectType<middy.MiddyfiedHandler>(middleware)

I would talk about many other TypeScript features
but time is over 🥲

Conclusion

Conclusion

  • helps you to understand the flow of your code

  • helps with the “documentation” of your code

  • helps you of the future to understand what happens in the code

  • remember that the Typescript magic disappears after the build (Zod, valibot, TypeBox)

  • could be introduced by steps

  • love all your javascript code

Slides

Resources

TypeScript Tips

Typescript - Tips & Tricks

Luca Del Puppo

@puppo92

Luca Del Puppo

Puppo_92

@puppo

We are hiring

Thank you!

Dive into TypeScript

By Luca Del Puppo

Dive into TypeScript

  • 476