Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better types #1

Open
JAForbes opened this issue Feb 27, 2022 · 3 comments
Open

Better types #1

JAForbes opened this issue Feb 27, 2022 · 3 comments
Labels
enhancement New feature or request

Comments

@JAForbes
Copy link
Owner

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.

@JAForbes JAForbes added the enhancement New feature or request label Feb 27, 2022
@JAForbes
Copy link
Owner Author

I'm thinking the second syntax could be used for query params too.

type Tag = 'Awesome' | 'Amazing'
Path`/todos/${'todo_id'}?{ archived: boolean, tags: Tag[] }`

@JAForbes
Copy link
Owner Author

CC @foxdonut any thoughts?

@foxdonut
Copy link

foxdonut commented Mar 4, 2022

@JAForbes interesting! sorry for the delay, haven't yet had a chance to experiment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants