Skip to content

Better types #1

Open
Open
@JAForbes

Description

@JAForbes

I'm experimenting with better typescript support for sum-type and superouter. I spent a few hours in the typescript playground trying different things and eventually got two ideas working.

Here's a public link to a secret gist where I'm tracking the idea https://gist.github.com/JAForbes/7fbd05df701069e097fbe38373c21e9b

The first idea is to have a typed constructor that returns a tagged template literal:

let Route = 
    type('Route', {
        Messages: (o: { message_id: string }) => Path`/messages/${o.message_id}`,
        Home: () => Path`/`,
        Wow: (o: { todo_id: string }) => Path`/todos/${o.todo_id}`
    })

Route.Messages({ message_id: 'hello' }).value.message_id
Route.Home()
Route.Wow({ todo_id: '1' }).value.todo_id

This type checks really well, and allows for discriminated checks, e.g. .value.message_id type checks for Route.Messages, but not Route.Wow

The idea behind the tagged template literal is to get away from express style patterns and instead parse the pattern at initialization and then compare a URL against the raw parts. It isn't strictly nessecessary, maybe express patterns are good.

But I found I can actually get back a typed tuple from Path which is probably useful generally:

let Path = function<
    T extends Array<U>
    , U=unknown
>(strings : TemplateStringsArray, ...args: T ) {
    return { strings, args }
}

let x = Path`/messages/${4}/${'hello'}`
type T = typeof x.args
// [number, string]

I thought it was pretty cool you can extract a tuple out instead of getting back (string | number)[].

Next idea, which is my favourite is to infer the constructor from a tagged template expression. There's a bit of noise because it requires as const for each value. But I think the payoff is worth it.

let Route = 
    type('Route', {
        Messages: Path`/messages/${'message_id' as const}`,
        Home: Path`/`,
        Wow: Path`/todos/${'todo_id' as const}`
    })

let p = Path`/messages/${'message_id' as const}`

p.r.message_id

Route.Messages({ message_id: '4' }).value.message_id // ✅
Route.Messages({ massage_id: '4' }).value.message_id // ❌
Route.Messages({ message_id: '4' }).value.massage_id // ❌

This approach infers the keys of the constructor input object from the Path expression. It is annoying you need to use as const but I am hoping that in a future TS version that won't be required, because in that case, as const should be automatically inferred (passing in a string literal should always default to as const imo).

I'm thinking this + nested routers is worth a v1.

I think for JS codebases, I'd still support /messages/:message_id as Path`/messages/${'message_id'}` isn't giving you any benefit in JS and its just extra effort for no pay off.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions