Advanced TypeScript

City JS Athens - June 7 2024

Today!

9:00am - Introductions & set-up

9:30am - TypeScript

10:00am - Types

10:30am - Casting

12:30pm - Lunch 🍽️

11:15am - Generics

12:00pm - Conditional Types

1:00pm - Mapped Types

3:00pm - Finish 🎉

2:00pm - Helper Types

Today!

Intros!

My name is __________

My most irrational fear is __________

Today I want to learn __________

Intros!

Intros!

My name is Craig

My most irrational fear is cash!

I want to learn all your names

Set-up!

github.com/phenomnomnominal/advanced-typescript

Go to the repo:

Set-up!

Set-up!

Get the code on your machine:

git clone https://github.com/phenomnomnominal/advanced-typescript.git

Set-up!

Install dependencies:

npm i

Node.js and npm version shouldn't matter, but latest is good!

Set-up!

Check it works:

npm run test

TypeScript!

TypeScript!

Typed super-set of JavaScript - all valid JavaScript code is valid TypeScript

Launched by Microsoft in 2012

Aims to make it easier to write larger JavaScript applications by introducing type-safety.

Has quickly become one of the most-used and most-loved programming languages in the world!

Syntax!

const hello: string = 'world';
let beast: number = 666;
function add (a: number, b: number): number {
  return a + b;
}
class Person {
  constructor (
    private _name: string
  ) { }

  public get name (): string {
    return this._name; 
  }
}
enum Sizes {
  SMALL,
  MEDIUM,
  LARGE
}

Defining values with a type

Defining parameters with types

Defining return type

Defining a class of objects

With typed properties

Defining a enum

Syntax!

interface Set<T> {
  /**
   * Appends a new element with a specified value to the 
   * end of the Set.
   */
  add(value: T): this;
  clear(): void;
  /**
   * Removes a specified value from the Set.
   * @returns Returns true if an element in the Set existed
   * and has been removed, or false if the element does not
   * exist.
   */
  delete(value: T): boolean;
  /**
   * Executes a provided function once per each value in
   * the Set object, in insertion order.
   */
  forEach(callbackfn: (value: T, value2: T, set: Set<T>) => void, thisArg?: any): void;
  /**
   * @returns a boolean indicating whether an element with 
   * the specified value exists in the Set or not.
   */
  has(value: T): boolean;
  /**
   * @returns the number of (unique) elements in Set.
   */
  readonly size: number;
}

Defining a generic interface for a built-in JavaScript type

Using the generic type

Adding modifiers to properties

Syntax!

type Digits = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';

Defining a union type for digits

type ParseSeconds<SecondsStr extends string> = 
  SecondsStr extends Seconds
    ? SecondsStr
    : DateParseError<`ParseSeconds: Invalid Seconds: ${SecondsStr}`>;
type ParseMinutes<MinutesStr extends string> = 
  MinutesStr extends Minutes
    ? MinutesStr
    : DateParseError<`ParseMinutes: Invalid Minutes: ${MinutesStr}`>;
type ParseHours<HourStr extends string> = 
  HourStr extends Hours
    ? HourStr
    : DateParseError<`ParseHours: Invalid Hour: ${HourStr}`>;
type Hours = `0${Digits}` | `1${Digits}` | '20' | '21' | '22' | '23' | '24';
type Minutes = `${'0' | '1' | '2' | '3' | '4' | '5'}${Digits}`;
type Seconds = Minutes;
type DateParseError<Message extends string> = { error: Message };

Defining a generic error type

Defining Template Literal types for Hours, Minutes and Seconds

Defining a Conditional Type to parse time strings

Compiler!

// index.js

var hello = 'world';

function add (a, b) {
  return a + b;
}
// index.ts

var hello = 'world';

function add (a, b) {
  return a + b;
}
tsc

The TypeScript compiler (tsc) does two main things:

  1. It statically analyses the code and checks for Type Errors
  2. It transpiles (converts) TypeScript code into JavaScript code

Type Checking!

// index.ts
const variable = 'whoop!';

console.log(variable * 3);
const variable: "whoop!";

The left-hand side of an arithmetic operation must be of type 'any',
'number', 'bigint', or an enum type.
tsc

TypeScript knows what a const is so it knows that the variable will always be a string!

tsc error message - they are often much more convoluted than this!

Type Checking!

// index.ts
const variable: string = null;

console.log(variable.toLowerCase());
tsc

By default, tsc allows any variable to accept  null values!

The type-checker will not report an issue, by there could be runtime errors!

 "Billion Dollar Mistake"

Type Checking!

// index.ts
const variable: string = null;

console.log(variable.toLowerCase());
tsc --strict
const variable: string;

Type 'null' is not assignable to type 'string'.

In strict mode the type-checker will exclude null as a valid value

Another pretty readable error message!

Type Checking!

// ./src/index.ts
// ./src/types.ts
// third-party-fun
// @types/third-party-fun
// tsconfig.json
tsc --noEmit

✨✨✨✨

// lib.dom.d.ts

Transpiling!

// index.js
tsc
// index.ts

interface Set<T> {
  add(value: T): this;
  clear(): void;
  delete(value: T): boolean;
  forEach(callbackfn: (value: T, value2: T, set: Set<T>) => void, thisArg?: any): void;
  has(value: T): boolean;
  readonly size: number;
}

type Numbers = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';

type DateParseError<Message extends string> = { error: Message };

type Hours = `0${Numbers}` | `1${Numbers}` | '20' | '21' | '22' | '23' | '24';
type Minutes = `${'0' | '1' | '2' | '3' | '4' | '5'}${Numbers}`;
type Seconds = Minutes;

Transpiling!

// index.js

var hello = 'world';

function add(a, b) {
    return a + b;
}
tsc --target ES5
// index.ts

const hello: string = 'world';

const add = (a: number, b: number): number => {
  return a + b;
}

Transpiling!

// index.ts

const hello: string = 'world';

const add = (a: number, b: number): number => {
  return a + b;
}
// index.js
 
const hello = 'world';

const add = (a, b) => {
    return a + b;
};
tsc --target ES2015

Transpiling!

// index.ts

async function getFileContents (path: string): Promise<string> {
    return await readFile(path, 'utf-8')   
}
// index.js
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
function getFileContents(path) {
    return __awaiter(this, void 0, void 0, function* () {
        return yield readFile(path, 'utf-8');
    });
}
tsc --target ES2015

Transpiling!

// index.ts

async function getFileContents (path: string): Promise<string> {
    return await readFile(path, 'utf-8')   
}
// index.js

async function getFileContents(path) {
    return await readFile(path, 'utf-8');
}
tsc --target ES2017

Transpiling!

// index.ts

enum HTTPErrors {
    BadRequest = 400,
    Unauthorized = 401,
    Forbidden = 403,
    NotFound = 404
}
// index.js

var HTTPErrors;
(function (HTTPErrors) {
    HTTPErrors[HTTPErrors["BadRequest"] = 400] = "BadRequest";
    HTTPErrors[HTTPErrors["Unauthorized"] = 401] = "Unauthorized";
    HTTPErrors[HTTPErrors["Forbidden"] = 403] = "Forbidden";
    HTTPErrors[HTTPErrors["NotFound"] = 404] = "NotFound";
})(HTTPErrors || (HTTPErrors = {}));
tsc

Transpiling!

// index.ts

enum HTTPErrors {
    BadRequest = 400,
    Unauthorized = 401,
    Forbidden = 403,
    NotFound = 404
}

ECMAScript Enum

Transpiling!

// index.js
// index.ts

const enum HTTPErrors {
    BadRequest = 400,
    Unauthorized = 401,
    Forbidden = 403,
    NotFound = 404
}
tsc

Transpiling!

// index.ts

const enum HTTPErrors {
    BadRequest = 400,
    Unauthorized = 401,
    Forbidden = 403,
    NotFound = 404
}

console.log(HTTPErrors.BadRequest);
// index.js

console.log(400 /* HTTPErrors.BadRequest */);
tsc

Transpiling!

// index.js
var HTTPErrors;
(function (HTTPErrors) {
    HTTPErrors[HTTPErrors["BadRequest"] = 400] = "BadRequest";
    HTTPErrors[HTTPErrors["Unauthorized"] = 401] = "Unauthorized";
    HTTPErrors[HTTPErrors["Forbidden"] = 403] = "Forbidden";
    HTTPErrors[HTTPErrors["NotFound"] = 404] = "NotFound";
})(HTTPErrors || (HTTPErrors = {}));
console.log(HTTPErrors.BadRequest);
babel --presets @babel/preset-typescript 
// index.ts

const enum HTTPErrors {
    BadRequest = 400,
    Unauthorized = 401,
    Forbidden = 403,
    NotFound = 404
}

console.log(HTTPErrors.BadRequest);

Transpiling!

// http-errors.ts

export const enum HTTPErrors {
    BadRequest = 400,
    Unauthorized = 401,
    Forbidden = 403,
    NotFound = 404
}
// index.ts

import { HTTPErrors } from './http-errors';

console.log(HTTPErrors.BadRequest);
// http-errors.js
// index.js
import { HTTPErrors } from './http-errors';

console.log(HTTPErrors.BadRequest);
babel
babel

Transpiling!

// index.js

console.log(400 /* HTTPErrors.BadRequest */);
tsc
// http-errors.ts

export const enum HTTPErrors {
    BadRequest = 400,
    Unauthorized = 401,
    Forbidden = 403,
    NotFound = 404
}
// index.ts

import { HTTPErrors } from './http-errors';

console.log(HTTPErrors.BadRequest);
// http-errors.js

Modules!

// index.ts

import { resolve } from 'path';

const CWD = process.cwd();
const SRC = './src';

export const RESOVLED = resolve(CWD, SRC);
"use strict";
Object.defineProperty(exports, "__esModule", { 
  value: true
});
exports.RESOVLED = void 0;
const path_1 = require("path");
const CWD = process.cwd();
const SRC = './src';
exports.RESOVLED = (0, path_1.resolve)(CWD, SRC);
tsc --module CommonJS
define(["require", "exports", "path"], function (require, exports, path_1) {
  "use strict";
  Object.defineProperty(exports, "__esModule", { value: true });
  exports.RESOVLED = void 0;
  const CWD = process.cwd();
  const SRC = './src';
  exports.RESOVLED = (0, path_1.resolve)(CWD, SRC);
});
tsc --module AMD

Modules!

// index.ts

import { resolve } from 'path';

const CWD = process.cwd();
const SRC = './src';

export const RESOVLED = resolve(CWD, SRC);
import { resolve } from 'path';

const CWD = process.cwd();
const SRC = './src';

export const RESOVLED = resolve(CWD, SRC);
tsc --module ESNext

Modules!

// index.ts

import { resolve } from 'path';

const CWD = process.cwd();
const SRC = './src';

export const RESOVLED = resolve(CWD, SRC);

Declaring Types!

type EmailAddress = `${string}@${string}`;
const birthday: Date;
function power (base: number, exponent: number): number {
  return base ** exponent;
}
type Location = {
  lat: number;
  lng: number;
  address: string;
}
interface Booking {
  date: Date;
  location: Location;
}
class MeetingBooking implements Booking {
  public constructor (
    public date: Date, 
    public location: Location,
    public participants: Array<EmailAddress>
  ) { }
}

Declaring variables

Declaring functions

Declaring types

Declaring interfaces

Declaring more types

Declaring classes

Declaring Types!

declare namespace global {
    interface Window {
      mySweetGlobal: string;
    }
}
declare module "my-js-lib" {
  export function sweetAsyncRandom (): Promise<number>;
}
declare module "*.scss" {
  const styles: Record<string, string>;
  
  export default styles;
}

Augmenting the global namespace with new global variables!

Manually declaring rough types for a third-party module

Manually declaring types for non-JS modules

Exercises!

Types!

Types!

true
123_456_789
'hello world'
[]
{}
null
undefined

Types!

boolean
number
string
Array<T>
Object
null
undefined
true
86_400
'January'
[number, number]
{ id: string }
null
undefined
let name = 'Craig';
typeof name; // string is inferred

Types!

const address: string = null; // ✅ in non-strict mode
const city: string = null; // ❌ in strict mode
const country: string | null = null; // ✅ in stict mode
const age = 33;
typeof age; // 33 is inferred
const isHungry: true = false; // ❌
const isSleepy: boolean = false; // ✅
const location: string = 10000; // ❌

TypeScript uses the typeof keyword to access the type of a variable!

Types!

undefined
true
86_400
'January'
[number, number]
{ id: string }
null
.valueOf()
.toPrecision(2)
.charAt(0)
[0]
['id']

Primitives!

true
1
'A'
false
1.1
1.1234
-1
NaN
Math.PI
666
"B"
`Cc`
'WOW!'
'🔥'
null
boolean
null
number
string
type Craig = 'Craig';
type ThirtyThree = 33;

type Location = string;

type IsHungry = true;
type IsSleepy = boolean;

Type Declarations!

Objects!

Object
object
boolean
number
string
null
undefined
symbol
const address: Object = null; // ❌
const country: Object = undefined; // ❌

Objects!

Object
const name: Object = 'Craig'; // ✅
const age: Object = 33; // ✅

const location: Object = 10000; // ✅

const isHungry: Object = false; // ✅
const isSleepy: Object = false; // ✅

Objects!

{}
object
Object
Object extends object // true
Object extends {} // true
object extends Object // true
object extends {} // true
{} extends Object // true
{} extends object // true
type Compare = A extends B ? true : false;

Objects!

object
boolean
number
string
null
undefined
symbol
Array<T>
Record<T, T>
{}
Date
RegExp
class Foo
Function

Arrays/Tuples!

[symbol, symbol]
Array<boolean>
bigint[]
[Date]
[number, ...null[]]
Array<T>
[]

Array Opinions!

bigint[]
Array<bigint>

vs

type Phone = { make: string, model: string };
type Phones = Array<Phone>;

🚨

🚨

Tuple Inference!

type Scores = [number, number, number, number];
const SCORES = [100, 200, 300, 400];
typeof SCORES; // Array<number>
const SCORES = [100, 200, 300, 400] as const;
typeof SCORES; // readonly [100, 200, 300, 400]
type Scores = [100, 200, 300, 400];

const assertion

Objects!

Record<string, any>
{ [key: string]: any }
{ id: string }
Record<string, Date>

Index Signature

Objects!

Record<string, any>
{ [key: string]: any }
{ id: string }
let dictA: {
  [key: string]: any
} = {
  id: '123'
}
let dictB: Record<string, any> = {
  id: '456'
}
let dictC: { id: string } = {
  id: '789'
}
dictB = dictA; // ✅
dictA = dictC; // ✅
dictC = dictA; // ❌

Object Inference!

const DICT = { id: '789' } as const;
typeof DICT // { readonly id: '789' }

const assertion

const DICT = { id: '789' };
typeof DICT // { id: string }

Modifiers!

type ReadonlyID = {
  readonly id: string
}

const readonlyId: ReadonlyID = { id: '123' };
readonlyId.id = '456'; // ❌
type ReadonlyNumbers = ReadonlyArray<number>;

type ReadonlyTuple = readonly [1, 2, 3];
type ReadonlyStrings = readonly string[];

const readonlyNumbers: ReadonlyNumbers = [1, 2, 3];
readonlyNumbers.push(4); // ❌

readonly modifier

Modifiers!

type OptionalID = {
  id?: string
}

const optionalId: OptionalID = { } // ✅
type IDOrUndefined = {
  id: string | undefined
}

const noId: IDOrUndefined = { } // ❌
const undefinedId: IDOrUndefined = { id: undefined } // ✅

Optional modifier

Indexes!

type NumberArrayish = {
  [index: number]: number
}

const numbers: NumberArrayish = {};
numbers[0] = 0; // ✅
numbers['one'] = 1; // ❌

type Index = NumbersArrayish[0]; // number
type Strings = ReadonlyArray<string>;

type Values = Strings[number]; // string
type Value = Strings[0]; // string
type Length = Strings['length']; // number

const SCORES = [100, 200, 300, 400] as const;

type Scores = typeof SCORES[number]; // 100 | 200 | 300 | 400
type Score = typeof SCORES[0]; // 100
type TupleLength = typeof SCORES['length']; // 4

Another union type!

Indexes!

interface Dictionary {
  [key: string]: string;
}

const dict: Dictionary = {};
type Key = Dictionary['name']; // string
interface Dictionary {
  [key: string]: string;
  size: number; // ❌
}
interface Dictionary {
  [key: string]: string | number;
  size: number; // ✅
}

Yet another union type!

Indexes!

type Address = {
  streetName: string;
  streetNumber: number;
  suburb: string;
  country: string;
  postCode: number;
}
// 'streetName' | 'streetNumber' | 'suburb' | 'country' | 'postcode'
type Keys = keyof Address;
const keys = Object.keys(address);
typeof keys; // any

Structural Typing!

type Configuration = {
  input: string,
  output: string
}

function configure (config: Configuration): void {
  // ... handle config
}

configure({
  input: './src',
  output: './dist',
}); // ✅

configure({
  input: './src',
}); // ❌

Functions!

() => void
Function
(a: number) => number
type FuncConstructor = {
  new (): Func;
};
class Foo
interface Func {
  (): void;
}

🚨 Function Opinions! 🚨

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

vs

function add (a: number, b: number): number {
  // Type 'string' is not assignable to type 'number' ❌
  return a + b + ''; 
};

Functions!

Function
interface FunctionConstructor {
    new(...args: string[]): Function;
    (...args: string[]): Function;
    readonly prototype: Function;
}

declare var Function: FunctionConstructor;
interface Function {
    apply(this: Function, thisArg: any, argArray?: any): any;
    call(this: Function, thisArg: any, ...argArray: any[]): any;
    bind(this: Function, thisArg: any, ...argArray: any[]): any;

    toString(): string;
    prototype: any;
    readonly length: number;
    arguments: any;
    caller: Function;
}

this parameter declaration

Functions!

type Func = (...args: Array<unknown>) => unknown;
type FuncWithThis = (this: unknown, ...args: Array<unknown>) => unknown;
function size (this: Array<unknown>): number {
  return this.length;
}

size([]); // ❌
size.call(1); // ❌
size.call([1]); // ✅

Structural Typing!

type EventHandler = (event: Event) => void;

function onEvent (
  name: string, 
  handler: EventHandler
): void {
  // ... register handler
}

onEvent('click', () => {
  // ... handle click  
}); // ✅

onEvent('click', (name: string) => {
  // ... handle click  
}); // ❌

Structural Typing!

type
value

value

value

const version: number = 1.1; // ✅
const NEGATIVE_BEAST: number = -666; // ✅
const two: 2 = 3; // ❌

const versionStr: string = 1.1; // ❌
const versionObj: object = 1.1; // ❌
const versionObject: Object = 1.1; // ✅

type MapNumber = (a: number) => number;
const noop: MapNumber = () => {}; // ❌
const identity: MapNumber = (n: string) => n; // ❌
const pow: MapNumber = (n: number) => n ** n; // ✅

Structural Typing!

Exercises!

Casting!

const versionStr: string = 1.1; // ❌
const versionObj: object = 1.1; // ❌
const versionObject: Object = 1.1; // ✅

Widening!

type
value

value

value

Widening!

const VERSION = 1.1; // '1.1'
let version = 1.1; // number

Widening!

const VERSION = 1.1; // 1.1'
let version = VERSION; // number
const name: any = 'Craig'; // ✅
const age: any = 33; // ✅

const location: any = 10000; // ✅

const isHungry: any = false; // ✅
const isSleepy: any = false; // ✅

const address: any = null; // ✅

Widening!

Top Types!

null
undefined
any
Object
object
boolean
number
string
symbol
const name: unknown = 'Craig'; // ✅
const age: unknown = 33; // ✅

const location: unknown = 10000; // ✅

const isHungry: unknown = false; // ✅
const isSleepy: unknown = false; // ✅

const address: unknown = null; // ✅

Top Types!

Top Types!

null
undefined
unknown
Object
object
boolean
number
string
symbol

Top Types!

unknown
any
let value: any = 1000;
value.toString(); // Works ✅
value.makeItAMillion(); // Works ✅
let value: unknown = 1000;
value.toString(); // Error ❌
value.makeItAMillion(); // Error ❌

Top Types!

unknown
any

Tells TypeScript that we don't care what type an object is, so tsc should stop type-checking this variable from here forward.

Tells TypeScript that we don't know what type an object is, so tsc should make sure we figured out what type it is before we use it.

Casting!

const two: number = 2;

function square(n: number): number {
  return n ** 2;
}

// unnecessary but valid - `2` extends `number` ✅
square(two as 2);

// invalid - `number` doesn't extend `object` ❌
square(two as object);

// invalid - `number` extends `Object` but `Object`
// can't be passed to a `number` parameter  ❌
square(two as Object);

Casting!

const two: unknown = null;

function square(n: number): number {
  return n ** 2;
}

square(two as 2);

unknown is a top-type, so you can use a Type Assertion to cast it to anything

Casting!

function toArray<T>(value?: ReadonlyArray<T> | Array<T> | T): Array<T> {
  if (Array.isArray(value)) {
    return value;
  }
  
  if (isUndefined(value)) {
    return [];
  }

  return [value as T];
}
try {
  // something bad happens:
} catch (error) {
  console.log((error as Error)).message;
  throw error;
}

Top Types!

any
unknown

Bottom Type!

any
unknown
never

Bottom Type!

const name: never = 'Craig'; // ❌
const age: never = 33; // ❌

const location: never = 10000; // ❌

const isHungry: never = false; // ❌
const isSleepy: never = false; // ❌

const address: never = null; // ❌

Bottom Type!

function throwError (message: string): never {
  throw new Error(message);
}
type WithId = { id: string };
type BanId = { id?: never };

function createId<T extends object> (obj: T & BanId): T & WithId {
  const needsId: unknown = obj;
  (needsId as WithId).id = '';
  return needsId as (T & WithId);
}

createId({ }); // Works `id` doesn't exist ✅
createId({ id: '1234 '}); // Error 'string' not assignable to 'never' ❌

Widening!

any
unknown
never

Narrowing!

any
unknown
never

Set Size?

any
never
null
string
boolean

Set Size!

null
null
'January'
3.5
'January'
3.5

Unions!

true
false
boolean
true
false
boolean
type Bool = true | false;
type Bool = boolean;

Unions!

type Num = 1 | 2 | 3 | 4 | ...
type Num = number;
1
2
3
4
number
5
6
7
8
9
10
...
number
1
1
3
4
2
5
6
7
8
9
10
11
12
13
14
...

Unions!

type HTTPError = 400 | 401 | 403 | 404;
const enum HTTPError {
    BadRequest = 400,
    Unauthorized = 401,
    Forbidden = 403,
    NotFound = 404
}
const HTTP_ERRORS = [400, 401, 403, 404] as const;

type HTTPErrors = (typeof HTTP_ERRORS)[number];

Narrowing!

const HTTP_ERRORS = [400, 401, 403, 404] as const;

type HTTPErrors = (typeof HTTP_ERRORS)[number];

function isHTTPError (input: unknown): HTTPErrors {
  // ???
}

Narrowing!

const HTTP_ERRORS = [400, 401, 403, 404] as const;

type HTTPErrors = (typeof HTTP_ERRORS)[number];

function isHTTPError (input: unknown): HTTPErrors {
  if (typeof input === 'number') {
    return input;
  }
  // ???
}

Narrowing!

const HTTP_ERRORS = [400, 401, 403, 404] as const;

type HTTPErrors = (typeof HTTP_ERRORS)[number];

function isHTTPError (input: unknown): HTTPErrors {
  if (
    input === 400 || input === 401 ||
    input === 403 || input === 404
  ) {
    return input;
  }
  // ???
}

Narrowing!

const HTTP_ERRORS = [400, 401, 403, 404] as const;

type HTTPErrors = (typeof HTTP_ERRORS)[number];

function isHTTPError (input: unknown): HTTPErrors {
  if (HTTP_ERRORS.includes(input))) {
    return input;
  }
  // ???
}

Type Guard!

const HTTP_ERRORS = [400, 401, 403, 404] as const;

type HTTPErrors = (typeof HTTP_ERRORS)[number];

function isHTTPError (input: unknown): input is HTTPErrors {
  return HTTP_ERRORS.includes(input as HTTPErrors);
}



type predicate!

Type Guard!

const HTTP_ERRORS = [400, 401, 403, 404] as const;

type HTTPErrors = (typeof HTTP_ERRORS)[number];

function isHTTPError (input: unknown): input is HTTPErrors {
  return HTTP_ERRORS.includes(input as HTTPErrors);
}

function logHTTPError (input: unknown): void {
  if (isHTTPError(input)) {
    // typeof input
    console.log('Oh no!');
  } else {
    // typeof input
    console.log('Cool.')
  }
}

type predicate!

Assertion Guard!

const HTTP_ERRORS = [400, 401, 403, 404] as const;

type HTTPErrors = (typeof HTTP_ERRORS)[number];

function assertHTTPError (input: unknown): asserts input is HTTPErrors {
  if (!HTTP_ERRORS.includes(input as HTTPErrors)) {
    throw new Error();
  }
}

function logHTTPError (input: unknown): void {
  assertHTTPError(input);
  
  // typeof input
  expensiveLoggingOperation('Oh no!');
}

Intersections!

type WithName = { name: string };
type WithId = { id: string };

type Person = {
  dateOfBirth: Date
} & WithName & WithId;
interface Person extends WithName, WithId {
  dateOfBirth: Date
};
class Person implements WithName, WithId {
  public name: string;
  public id: string;
  public dateOfBirth: Date;
};

Intersections!

interface Person extends WithName, WithId {
  dateOfBirth: Date
};

interface Person {
  placeOfBirth: string;
}
// lib.es2015.collection.d.ts
interface Set<T> {
    add(value: T): this;
    clear(): void;
    delete(value: T): boolean;
    forEach(callbackfn: (value: T, value2: T, set: Set<T>) => void, thisArg?: any): void;
    has(value: T): boolean;
    readonly size: number;
}

// lib.es2015.iterable.d.ts
interface Set<T> {
    [Symbol.iterator](): IterableIterator<T>;
    entries(): IterableIterator<[T, T]>;
    keys(): IterableIterator<T>;
    values(): IterableIterator<T>;
}

Template Strings!

type Email = string;
'A'
"B"
`Cc`
'WOW!'
'🔥'
string

Template Strings!

type Email = `${string}@${string}`;
'a@a'
"foo@bar.com"
`🔥@❤️`
Email

Exercises!

Generics!

Generics!

Array<T>
Record<string, any>
Set<T>
Promise<T>
<T>(arg: T) => T

Generics!

function identity (input) {
  return input;
}
function identity (input: any): any {
  return input;
}

const i = identity('hello');
typeof i; // any
function identity (input: unknown): unknown {
  return input;
}

const i = identity('hello');
typeof i; // unknown

Generics!

function identity <T> (input: T): T {
  return input;
}

const i = identity('hello');
typeof i; // 'hello'
function identity <Input> (input: Input): Input {
  return input;
}

const i = identity<string>('hello'); // i: string
[symbol, symbol]
Array<boolean>
bigint[]
[Date]
[number, ...null[]]
Array<T>
[]
Array<number>
[number]
[1, 2, 3]

Generics!

import { promises as fs } from 'fs'

async function persist <T> (input: T): Promise<T> {
  await fs.writeFile('', JSON.stringify(input));
  return input;
}

const i = persist('hello'); // i: Promise<string>
import { promises as fs } from 'fs'

async function persist <T> (input: T): Promise<T> {
  // Property 'serialise' does not exist on type 'T'
  await fs.writeFile('', input.serialise());
  return input;
}

Behaves similarly to unknown

Generics!

import { promises as fs } from 'fs'

type Serialisable = {
  serialise(): string;
}

async function persist <T extends Serialisable> (input: T): Promise<T> {
  await fs.writeFile('', input.serialise());
  return input;
}

// Argument of type 'string' is not assignable to parameter of type 'Serialisable'
const error = persist('hello');

const good = persist({ serialise: () => 'hello' });

Generics!

interface Set<T> {
  add(value: T): this;
  clear(): void;
  delete(value: T): boolean;
  forEach(
    callbackfn: (value: T, value2: T, set: Set<T>) => void,
    thisArg?: any
  ): void;
  has(value: T): boolean;
  readonly size: number;
}

forEach on a Set is a little bit weird, it gets each value passed twice!

Generics!

typefunction Set (Item) {
  return {
    add(value: Item): this;
    clear(): void;
    delete(value: Item): boolean;
    forEach(/* ... */): void;
    has(value: Item): boolean;
    readonly size: number;
  };
}

type SetNumber = Set<number>;
// {
//   add(value: number): this;
//   clear(): void;
//   delete(value: number): boolean;
//   forEach(/* ... */): void;
//   has(value: number): boolean;
//   readonly size: number;
// }

Generics!

class Runner <RunResult> {
  public constructor (private task: () => RunResult) { }
}

const inferredRunner = new Runner(() => 3);
type InferredRunner = typeof inferredRunner; // Runner<number>
class Runner <RunResult extends () => unknown> {
  public constructor (private task: RunResult) { }
  
  public run () {
    return this.task();          
  }
}


const badRunner = new Runner(3); // ❌

const goodRunner = new Runner(() => 3); // ✅
type GoodRunner = typeof runner; // Runner<() => 3>

Generics!

function getID <T> (input: T): T['id'] {
  return input.id;
}
function getID <T extends { id: unknown }> (input: T): T['id'] {
  return input.id;
}

const id = getID({ id: '123' });

This won't work

Template Literal Types!

type USD = `US$${number}`
type Currency<
  Prefix extends string,
  Suffix extends string = '',
  Separator extends string = ',',
  Decimal extends string = '.'
> = ...

type USD = Currency<'US$'>;
type Euro = Currency<'€' | '', '€' | '', '.', ','>;
typefunction Currency (
  Prefix: string,
  Suffix: string = '',
  Separator: string = ',',
  Decimal: string = '.'
) {
  return `${Prefix}...`;
}

Template Literal Types!

type Letter = 'a' | 'b' | 'c' | 'd' | 'e' // ...
type Numbers = 0 | 1 | 2 | 3 | 4 | 5 // ...;

// "a0" | "a4" | "a2" | "a3" | "a1" | "a5" | "a6" | "a7" | "a8" | "a9" |
// "b0" | "b4" | "b2" | "b3" | "b1" | "b5" | ...
type IDs = `${Letter}${Numbers}`;
typefunction Id (
  Letters: Array<string>,
  Numbers: Array<number>
): Array<string> {
  const result = [];
  for (let letter in Letters) {
    for (let num in Numbers) {
      result.push(`${letter}${num}`);
    }
  }
  return result;
}
type HTMLTagsLower = 'a' | 'div' | 'kbd' | 'section' | ...

type HTMLTags = HTMLTagsLower | Uppercase<HTMLTagsLower>;

Intrinsic String Types!

type BlockCaps = Lowercase<string>;

const badPassword: BlockCaps = 'HELLO'; // ❌
const goodPassword: BlockCaps = 'hello'; // ✅
type EventNames = 'click' | 'doubleclick' | 'hover';

// 'onClick' | 'onDoubleclick' | 'onHover';
type EventHandlers = `on${Capitalize<EventNames>}`;
type DotNetType = {
  Name: string;
  DateOfBirth: Date;
}

// 'name' | 'dateOfBirth'; 
type JSTypeNames = Uncapitalize<keyof DotNetType>;

Exercises!

Lunch!

Back here ready to go by 1:30pm!

Conditionals!

Input extends Super ? TrueType : FalseType
typefunction Conditional (
  Input,
  Super,
  TrueType,
  FalseType,
) {
  if (Input extends Super) {
    return TrueType;
  }
  return FalseType;
}

Conditionals!

Conditionals!

type AssertExtends<
  SuperType,
  Subtype extends SuperType
> = Subtype extends SuperType ? Subtype : never;
typefunction AssertExtends (
  SuperType,
  SubType
) {
  if (SubType extends SuperType) {
    return SubType;
  }
  return never;
}

Conditionals!

type IsString1<Input> = Input extends string
  ? Input
  : never;
type IsString2<Input extends string> = Input extends string
  ? Input
  : never;
type Str1 = IsString1<false>;
type Str2 = IsString2<false>;

No error - Str1 is never

Error: Type boolean does not satisfy the constraint string

Conditional Inference!

type Func = (...args: Array<unknown>) => unknown;

type IsFunction<Input> = Input extends Func
  ? Input
  : never;
type ReturnType<Input> = 
  Input extends (...args: Array<unknown>) => infer Return
    ? Return
    : never;

infer is magic ✨

type FunctionMagic<Input extends Func> = 
  Input extends (...args: infer Parameters) => infer Return
    ? [Parameters, Return]
    : never;
typefunction FunctionMagic (
  Input: Func
) {
  if (Input extends Func) {
    const Parameters = gimmeParams(Input);
    const Return = gimmeReturn(Input);
    return [Parameters, Return];
  }
  return never;  
}

Conditional Inference!

Template Inference!

type Domain<Input extends string> = 
  Input extends `${string}@${infer D}`
    ? D
    : never;

type Gmail = Domain<'craig@gmail.com'>; // gmail.com
type Nope = Domain<'12345'>; // never
type Domain<Input extends string> = 
  Input extends `${string}@${infer D}`
    ? D
    : { message: `"${Input}" is not an email address`};

type Gmail = Domain<'craig@gmail.com'>; // gmail.com

// { message: "\"12345\" is not an email address" }
type Nope = Domain<'12345'>;

Exercises!

Maps!

Maps!

type Bundles {
   'index.js': string,
   '1.js': string,
   '2.js': string
};
type Keys = keyof Bundles; // 'index.js' | '1.js' | '2.js'

Maps!

type FuncyBundles = {
  [Path in keyof Bundles]: () => string
};

// type FuncyBundles = {
//   'index.js': () => string;
//   '1.js': () => string;
//   '2.js': () => string;
// }
typefunction FuncyBundles () {
  const Result = {};
  const Paths = keyof Bundles;
  Paths.forEach(Path => {
    Result[Path] = () => string;
  });
  return Result;
};

Should probably be a map or a reduce, but this seems clearer

Maps!

type LazyBundles = {
  [Path in keyof Bundles]: () => Promise<Bundles[Path]>
};

// type LazyBundles = {
//   'index.js': () => Promise<string>;
//   '1.js': () => Promise<string>;
//   '2.js': () => Promise<string>;
// }
typefunction LazyBundles () {
  const Result = {};
  const Paths = keyof Bundles;
  Paths.forEach(Path => {
    Result[Path] = () => Promise<Bundles[Path]>;
  });
  return Result;
};

Conditional Maps!

type LazyBundlesExceptIndex = {
  [Path in keyof Bundles]: Path extends 'index.js'
    ? Bundles[Path]
    : () => Promise<Bundles[Path]>
};

// type LazyBundlesExceptIndex = {
//   'index.js': string;
//   '1.js': () => Promise<string>;
//   '2.js': () => Promise<string>;
// }
typefunction LazyBundlesExceptIndex () {
  const Result = {};
  const Paths = keyof Bundles;
  Paths.forEach(Path => {
    if (Path === 'index.js') {
      Result[Path] = Bundles[Path];
    } else {
      Result[Path] = () => Promise<Bundles[Path]>;
    }
  });
  return Result;
};

Mapped Keys!

type LazyBundles = {
  [Path in keyof Bundles as `get${Capitalize<Path>}`]:
    () => Promise<Bundles[Path]>
};

// type LazyBundles = {
//   "getIndex.js": () => Promise<string>;
//   "get1.js": () => Promise<string>;
//   "get2.js": () => Promise<string>;
// }
typefunction LazyBundles () {
  const Result = {};
  const Paths = keyof Bundles;
  Paths.forEach(Path => {
    Result[`get${Capitalize(Path)}`] = () => Promise<Bundles[Path]>;
  });
  return Result;
};

Conditional Keys!

type LazyBundles = {
  [
    Path in keyof Bundles as Path extends 'index.js'
      ? Path : never
  ]: () => Promise<Bundles[Path]>
};

// type LazyBundles = {
//   "getIndex.js": () => Promise<string>;
// }
typefunction LazyBundles () {
  const Result = {};
  const Paths = keyof Bundles;
  Paths.forEach(Path => {
    if (Path === 'index.js') {
      Result[Path] = () => Promise<Bundles[Path]>;    
    }
  });
  return Result;
};

Mapped Modifiers!

type ConfigInput = {
  src?: string;
  silent?: boolean;
  output?: string;  
};

type Required<Input> = {
  [Property in keyof Input]-?: Input[Property];
}

type ConfigOutput = Required<ConfigInput>;
// type ConfigInput = {
//   src: string;
//   silent: boolean;
//   output: string;  
// };

Kind of weird syntax!

Mapped Modifiers!

type ReadonlyConfig = {
  readonly src: string;
  readonly silent: boolean;
  readonly output: string;  
};

type Mutable<Input> = {
  -readonly [Property in keyof Input]: Input[Property];
}

type MutableConfig = Mutable<ReadonlyConfig>;
// type ConfigInput = {
//   src: string;
//   silent: boolean;
//   output: string;  
// };

Exercises!

Helpers!

Helpers!

type State = {
  counter: number;
  items: Array<string>;
}

function updateState (state: Partial<State>): State {
  return {
    ...oldState,
    ...state
  };
}
type Partial<T> = unknown;

Helpers!

type ConfigInput = {
  src?: string;
  silent?: boolean;
  output?: string;  
};

type ConfigOutput = Required<ConfigInput>;
type Required<T> = unknown;

Helpers!

type ConfigInput = {
  src?: string;
  silent?: boolean;
  output?: string;  
};

type ConfigOutput = Required<ConfigInput>;

type ImmutableCounfig = Readonly<ConfigOutput>;
type Readonly<T> = unknown;

Helpers!

type Things = [1, 2, 'hello', null, RegExp];

// 2 | 1
type Numbers = Extract<Things[number], number>;
// RegExp | null | 'hello'
type NonNumbers = Exclude<Things[number], number>;
type Exclude<T, U> = unknown;

type Extract<T, U> = unknown;

Helpers!

type Address = {
  streetName: string;
  streetNumber: number;
  suburb: string;
  country: string;
  postCode: number;
}

type Addressish = Pick<Address, 'suburb' | 'country'>;
type Addressish2 = Omit<Address, 'streetName' | 'streetNumber' | 'postCode'>;
type Pick<T, K extends keyof T> = unknown;

type Omit<T, K extends keyof any> = unknown;

Helpers!

// string
type JustString = NonNullable<string | null | undefined>;
type NonNullable<T> = unknown;

Helpers!

type ReturnType<Input> = unknown;
type Parameters<Input> = unknown;
function add (a: number, b: number): number {
  returb a + b;
}

type Params = Parameters<typeof add>; // [a: number, b: number]
type Result = ReturnType<typeof add>; // number

Helpers!

type ConstructorParameters<
  T extends abstract new (...args: any) => any
> = unknown;
class Person {
  public constructor (name: string, age: string) { }
}

// Type 'typeof Person' does not satisfy the constraint '(...args: any) => any'
type BadParams = Parameters<typeof Person>; // ❌

// [name: string, age: string]
type GoodParams = ConstructorParameters<typeof Person>;

Helpers!

type InstanceType<
  T extends abstract new (...args: any) => any
> = unknown;
class Person {
  public constructor (name: string, age: string) { }
}

// Type 'typeof Person' does not satisfy the constraint '(...args: any) => any'
type BadReturnType = ReturnType<typeof Person>; // ❌

// Person
type GoodInstanceType = InstanceType<typeof Person>;

Helpers!

type ThisParameterType<T> = unknown;
function size (this: Array<unknown>): number {
  return this.length;
}

type SizeThis = ThisParameterType<typeof size>; // Array<unknown>

Helpers!

interface CallableFunction extends Function {
  // ...
  bind<T>(this: T, thisArg: ThisParameterType<T>): OmitThisParameter<T>;
}
class Toggle {
  constructor(props) {
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState(prevState => ({ ... }));
  }

  render() {
    return (
      <button onClick={this.handleClick}></button>
    );
  }
}
type OmitThisParameter<T> = unknown;

Helpers!

type Awaited<T> = unknown;
type AsyncNumber = Promise<number>;
type SyncNumber = Awaited<AsyncNumber>; // number
type ReallySyncNumber = Awaited<Promise<Promise<number>>; // number;

Everything!

Advanced TypeScript Workshop - CityJS Athens

By Craig Spence

Advanced TypeScript Workshop - CityJS Athens

Advanced TypeScript Workshop - CityJS Athens 2024

  • 678