Skip to content

Instantly share code, notes, and snippets.

@jhahspu
Last active July 28, 2023 12:59
Show Gist options
  • Select an option

  • Save jhahspu/d1eaac770b939da430c10537f9a9c67c to your computer and use it in GitHub Desktop.

Select an option

Save jhahspu/d1eaac770b939da430c10537f9a9c67c to your computer and use it in GitHub Desktop.
typescript
// A typesafe shopping cart in typescript.

// Immutable map :)

declare class Map<T, U> {
	set(t:T, u:U):Map<T, U>
	has(t:T):boolean;
	delete(t:T):Map<T,U>
	count:number;
}

type SKU = string;

type Item = {
    sku: SKU,
    quantity: number
};

type Items = Map<SKU, Item>;


// Cart states.

// enum tags let you get nominal types instead of structural 
// which in turn lets us differentiate between Loaded and Paid carts
// that have the same structure

enum TEmpty{}
enum TLoaded{}
enum TPaid{}

class Empty {
	private tag:TEmpty;
}
class Loaded { 
	private tag:TLoaded;
    constructor(public items:Map<SKU, Item>) {}
}
class Paid {
	private tag:TPaid;
	constructor(public items:Map<SKU, Item>) {}
}

type Cart = Empty | Loaded | Paid

export function create():Empty {
	return new Empty();
}

// Add an item to a cart that isn't in the paid state.
export function addItem(cart: Loaded | Empty, item: Item):Loaded {
	var items: Map<SKU, Item>;
	if (cart instanceof Loaded) {
		items = cart.items;
	}
	else {
		items = new Map();		
	}
	return new Loaded(items.set(item.sku, item));
};

// Remove an item from a non-empty, non-paid cart.  Note: you may receive a
// loaded or an empty cart back, so your code *must* branch after this call
// or flow will fail to compile your code!
export const removeItem
    : (cart: Loaded, item: Item) => Loaded | Empty
    = (cart, item) => {
        const items = cart.items;
        if (!items.has(item.sku)) {
            throw new Error(`Item is not in cart: ${ item.sku }.`);
        }
        var res = items.delete(item.sku);
        return res.count === 0
            ? new Empty()
            : new Loaded(res);
    };

// Replace an item in a non-empty, non-paid cart.
export const replaceItem
    : (cart: Loaded, item: Item) => Loaded
    = (cart, item) => {
        const items = cart.items;
        if (!items.has(item.sku)) {
            throw new Error(`Item is not in cart: ${ item.sku }.`);
        }
        return new Loaded(items.set(item.sku, item));
    };

// Pay for a non-empty, non-paid cart.
export const pay
    : (cart: Loaded) => Paid
    = (cart) => {
        // do some payments stuff here
        return new Paid(cart.items);
    };

// Some example code:

const emptyCart = create(); // cart is Empty
const singleItemCart = addItem(emptyCart, { sku: '123213', quantity: 2 }); // cart is Loaded
const paidCart = pay(singleItemCart);

// Some example code that does *not* compile.

// this line should fail to type-check since you can't call `removeItem` with
// an empty cart (removeItem does not accept `Empty`):
const cart = removeItem(create(), { sku: '123213', quantity: 2 });

// this is also an error, as you can only pay loaded carts, but not paid. The enum tag
// ensures that the type's structure is different.
const twicePaidCart = pay(paidCart)

TS Basics

  • installation
    • npm i -g typescript
  • ts-node-dev
    • yarn add ts-node-dev --dev
    • npm install -g ts-node-dev
  • hot reload
    • tsnd --respawn FILENAME.ts
  • build
    • tsc FILENAME.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment