Skip to content

Instantly share code, notes, and snippets.

@cdrini
Last active January 15, 2021 23:19
Show Gist options
  • Save cdrini/f5b17059ec35129cd62b7a657b5c2c75 to your computer and use it in GitHub Desktop.
Save cdrini/f5b17059ec35129cd62b7a657b5c2c75 to your computer and use it in GitHub Desktop.
TypeScript Things I Like and Dislike [DRAFT]

Note: This is a draft and not well proofread. Likely contains errors.

Things I like:

instanceof type guards

class Book { id: string }
class Film { imdbId: string }

async function fetchMetadata(item: Book | Film) {
    if (item instanceof Book)
        return fetch(`https://blah.org/${item.id}`).then(r => r.json());
    else if (item instanceof Film)
        return fetch(`https://blah.org/${item.imdbId}`).then(r => r.json());
}

Yaaay! It can deduce the type after an instanceof!

Union types

type Suites = 'Hearts' | 'Clubs' | 'Diamonds' | 'Spades';

function Card(suite: Suites) {
    this.suite = suite;
}

const card = new Card('Hearts');

Intersection types

type GenericMetadataRecord = {
    identifier: string
    mediatype: string
}

type BookMetadataRecord = GenericMetadataRecord & {
    mediatype: 'texts'
    title: string
    /** @deprecated Use openlibrary_edition */
    openlibrary?: string
    openlibrary_edition?: string
    openlibrary_work?: string
}

function getOLID(book: BookMetadataRecord) {
    return book.openlibrary_edition || book.openlibrary_work;
}

getOLID({ identifier: 'foo', mediatype: 'texts', title: 'Hello' });

Intersection types + generics + recursive types

type TreeNode<T> = T & { children?: TreeNode<T>[] };

function recurForEach<T>(node: TreeNode<T>, fn: (node: TreeNode<T>) => void) {
    fn(node);
    if (node.children)
        for (const child of node.children) recurForEach(child, fn);
}

const tree = {
    short: 'J',
    label: 'Political Science',
    children: [
        {
            short: 'J--',
            label: ' General legislative and executive papers'
        }
    ]
};

recurForEach(tree, x => console.log(x.label));

Ah, won't aut-detect deeper; need to pre-define ShelfNode:

type TreeNode<T> = T & { children?: TreeNode<T>[] };

function recurForEach<T>(node: TreeNode<T>, fn: (node: TreeNode<T>) => void) {
    fn(node);
    if (node.children)
        for (const child of node.children) recurForEach(child, fn);
}

type ShelfNode = TreeNode<{short: string, label: string}>;

const tree = {
    short: 'J',
    label: 'Political Science',
    children: [
        {
            short: 'J--',
            label: ' General legislative and executive papers',
            children: [
                {
                    short: 'J--',
                    label: ' General legislative and executive papers'
                }
            ]
        }
    ]
};

recurForEach<ShelfNode>(tree, x => console.log(x.label));

Template Literal String Types!

Introduced in (I think) TS 4.1 ; not working in VS Code yet for me. But awesome!

const x: `${number}%` = '33%';

/** @type {`${number}%`} */
const x = '33%';

TS JS-Doc bindings

You can do any of this stuff with just jsdoc; makes setup a breeze! And keeps the compile-time-only type info separated from the code. For the most part, just need to replace type Foo = ... with /** @typedef {...} Foo */, and add // @ts-check to the top of your JS file.

See https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html .

Here are some basic examples:

// @ts-check
// ^ Don't forget the @ts-check! This is what makes VS Code check for type errors in .js files.

// You can put anything that would be in a TS `type` definition here
/** @typedef {'Hearts' | 'Clubs' | 'Diamonds' | 'Spades'} Suite */

// This syntax is useful for interface/pojos:
/**
 * @typedef {object} Card
 * @property {Suite} suite
 * @property {'ace'|2|3|4|5|6|7|8|9|'jack'|'queen'|'king'} value
 */

// How to specify the type of a fixed value
/** @type {Card} */
const card = { suite: 'Hearts', value: 'ace' };

Structural typing

TODO

Things I don't like:

Classes

What I want to write:

type Suite = 'Hearts' | 'Clubs' | 'Diamonds' | 'Spades';

class Card {
    constructor(suite: Suite) {
        this.suite = suite;
    }
}

What I have to write:

type Suite = 'Hearts' | 'Clubs' | 'Diamonds' | 'Spades';

class Card {
    suite: Suite;

    constructor(suite: Suite) {
        this.suite = suite;
    }
}

I get why, but this is a non-essential shift from normal JS syntax. It would be wonderful if it supported both.

Actually is possible with TS-JSDoc:

/** @typedef {'Hearts' | 'Clubs' | 'Diamonds' | 'Spades'} Suite */

class Card {
    /**
     * @param {Suite} suite
     */
    constructor(suite) {
        this.suite = suite;
    }
}

Multiple Dispatch

What I want to write:

class Book { id: string }
class Film { imdbId: string }

async function fetchMetadata(book: Book) {
    return fetch(`https://blah.org/${book.id}`).then(r => r.json());
}

async function fetchMetadata(film: Film) {
    return fetch(`https://blah.org/${film.imdbId}`).then(r => r.json());
}

const items = [new Book(), new Film()];

Promise.all(items.map(fetchMetadata));

What I have to write:

class Book { id: string }
class Film { imdbId: string }

async function fetchMetadata(book: Book);
async function fetchMetadata(film: Film);
async function fetchMetadata(item: Book | Film) {
    if (item instanceof Book)
        return fetch(`https://blah.org/${item.id}`).then(r => r.json());
    else if (item instanceof Film)
        return fetch(`https://blah.org/${item.imdbId}`).then(r => r.json());
}

const items = [new Book(), new Film()];

Promise.all(items.map(fetchMetadata));

The syntax for this is just very odd. I would love to be able to just do multiple dispatch.

Also, what I would want to write:

class Book { id: string }
class Film { imdbId: string }
async function fetchMetadata(item: Book | Film) {
    return fetch(`https://blah.org/${item.id ?? item.imdbId}`).then(r => r.json());
}

But TS' inference system isn't good enough.

Promises

What I want to do:

async function fetchSomething(url: string): Object {
    return fetch('foo').then(r => r.json());
}
// The return type of an async function or method must be the global Promise<T> type. Did you mean to write 'Promise<Object>'?ts(1064)

You know an async function always returns a promise, so why don't you just do that? What I have to do:

async function fetchSomething(url: string): Promise<Object> {
    return fetch('foo').then(r => r.json());
}

Sometimes out-of-sync with JS spec

const x = [[1,2,3]].flat();
const y = [[1,2,3]].flatMap(x => x**2);

Function type syntax

What I want to write:

function forEach(fn: any => void) { ... }
// OR
function forEach(fn: (any) => void) { ... }
// OR
function forEach(fn: function(any): void) { ... }

What I have to write:

function forEach<T>(fn: (node: any) => void) { ... }

The only valid way of doing this is nice in that it's consistent, but the other 2 ways are also consistent with the ways functions are defined in js/ts. Limiting this to only one specific way feels inconsistent.

Nullable type non-sense

class Book { titles: string[] }

function foo(book?: Book) {
    // Why doesn't this throw an error? I'm ignoring the fact that this can be
    // null
    return book.titles.join('; ');
}

I want to have to write:

class Book { titles: string[] }

function foo(book?: Book) {
    return book?.titles.join('; ');
}

Switch case type inference

What I want to write:

type Suites = 'Hearts' | 'Clubs' | 'Diamonds' | 'Spades';

function getSuiteEmoji(suite: Suites) {
    switch(suite) {
        case 'Hearts': return '♥';
        // Want a compile-time ERROR: Incomplete switch case
    }
}

In order to get this to error at compile time, I either have to add a typescript eslint plugin @typescript-eslint/switch-exhaustiveness-check, or write something like:

Still no multiple dispatch

function find<T>(arr: [], predicate: (el: T) => boolean) { return false; }

function find<T>([head, ...rest]: T[], predicate: (el: T) => boolean) {
    return predicate(head) || find(rest, predicate);
}

With Literal types and destructuring, this feels like such a natural way to write code :(

No dynamic type checking

What I want to write:

type Person = { firstName: String, lastName: String }
const x = { firstName: 'Jimmy', lastName: 'Carr' }

if (x instanceof Person) {
    // blah
}

There's no real type information available at runtime :( It all goes away. I could do something like this:

type Person = { firstName: String, lastName: String };

function matchesType<T>(x: T) {
    return true;
}

const x = { firstName: 'Jimmy', lastName: 'Carr' };
const y = { x: 3, y: 3 };
if (matchesType<Person>(x)) {
    // blah
}
if (matchesType<Person>(y)) {
    // blah
}

But this is a compile time check that returns an error; if I was writing a library to deal with dynamic content (e.g. if x was fetched from a network), this function would always return true after this function was compiled. In fact, it's equivalent to:

type Person = { firstName: String, lastName: String };

const x = { firstName: 'Jimmy', lastName: 'Carr' };
const y = { x: 3, y: 3 };
if (x as Person) {
    // blah
}
if (y as Person) { // <- Compile-time error
    // blah
}

The only recommended way I can find for this on the official docs, is:

type Person = { firstName: String, lastName: String };

function isPerson(x: any): x is Person {
    return 'firstName' in (x as Person) && 'lastName' in (x as Person);
}

const x = { firstName: 'Jimmy', lastName: 'Carr' };
const y = { x: 3, y: 3 };

if (isPerson(x)) {
    // blah
}
if (isPerson(y)) {
    // blah
}

No Pattern matching

What I want to write:

const lendingInfo = {
    status: "OK",
    lending_status: {
        is_lendable: true,
        is_printdisabled: true,
        is_readable: false,
        is_login_required: false,
        max_lendable_copies: 7,
        available_lendable_copies: 0,
        max_browsable_copies: 7,
        available_browsable_copies: 0,
        max_borrowable_copies: 6,
        available_borrowable_copies: 0,
        users_on_waitlist: 0,
        last_waitlist: null,
        copies_reserved_for_waitlist: 0,
        upgradable_browses: 0,
        active_browses: 0,
        last_browse: null,
        next_browse_expiration: null,
        active_borrows: 6,
        last_borrow: "2020-10-16 17:08:34",
        next_borrow_expiration: "2020-10-20 16:01:14",
        orphaned_acs_loans: 1,
        available_to_browse: false,
        available_to_borrow: false,
        available_to_waitlist: true,
        user_is_printdisabled: false,
        user_loan_count: 0,
        user_at_max_loans: false,
        user_has_browsed: false,
        user_has_borrowed: false,
        user_loan_record: [],
        user_on_waitlist: false,
        user_can_claim_waitlist: false,
        user_has_acs_borrowed: false
    }
};

switch(lendingInfo.lendingStatus) {
    case { is_readable: true, available_to_browse: false }: 'publicly available';
    case { is_lendable: true, available_to_browse: true, }: 'available to browse'
    default: 'not available';
}

Esoterica

Peano Arithmetics with TypeScript's type inference

Can't recall if I got this working or not; but there are better attempts at this by other online if you Google it! TypeScript's type system is, I believe, Turing complete :)

type Zero = { prev: 'nil' }
type PNum = { prev: PNum } | Zero

type Incr<T extends PNum> = { prev: T }
type Decr<T extends PNum> = T extends Zero ? Zero : T['prev']

type One = Incr<Zero>
type Two = Incr<One>
type Three = Incr<Two>
type Four = Incr<Three>
type Five = Incr<Four>
type Six = Incr<Five>
type Seven = Incr<Six>
type Eight = Incr<Seven>
type Nine = Incr<Eight>
type Ten = Incr<Nine>

type Sum<T1 extends PNum, T2 extends PNum> =
T1 extends Zero ? T2 :
T2 extends Zero ? T1 :
{ prev: Sum<Decr<T1>, T2> }

// This seems to have an error, but is still working 🤷‍♂️
type Product<T1 extends PNum, T2 extends PNum> =
T1 extends Zero ? Zero :
T2 extends Zero ? Zero :
T1 extends One ? T2 :
T2 extends One ? T1 :
{ prev: Decr<Sum<Sum<T1, T1>, Product<T1, Decr<Decr<T2>>>>> }

const one: One = { prev: { prev: 'nil' } };
const two: Two =  { prev: { prev: { prev: 'nil' } } }
const three: Three = { prev: { prev: { prev: { prev: 'nil' } } } }


// hover over the left side to get the answer in the type!
type X = Sum<Two, One>
type Y = Product<Three, Three>
type Z = Sum<Five, Five>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment