#Type can't be indexed

26 messages · Page 1 of 1 (latest)

random dust
#

I'm not sure if this is a compiler bug or if I'm just incapable of proper typing. I'm working on generating an API client for some route specifications. I whittled it down to this repro.

type Methods = "get" | "delete" | "post" | "put"
interface TestOp<TOp extends string> {
    id: TOp
}
type TestPath<TOps extends string = string> = Partial<{
    [TMethod in Methods]: TestOp<TOps>
}>

function useTestPath<TOps extends string, TP extends TestPath<TOps>>(path: TP) {
    return path
}

const tp = useTestPath({
    get: { id: "hello" },
    put: { id: "goodbye"}
})

type OpKeys<TPath extends TestPath> = TPath[keyof TPath]["id"]
type ok = OpKeys<typeof tp>

OpKeys gets the error Type '"id"' cannot be used to index type 'TPath[keyof TPath]'
Which feels wrong. But what's more interesting is VSCode is able to give the type hint that ok is infact "hello" | "goodbye"

Ultimately I'm trying to get a mapped object of functions that have the names of id, but this weird indexing issue is blocking me. Maybe I did something wrong with the <TOp extends string> stuff. But it's the only way I found get useTestPath to properly infer the id as literals and not string.

autumn prawn
#

generic constraints are lower bounds. TPath may have arbitrary additional properties besides those specified by TestPath which may not have id properties

#

for example:

indigo quarryBOT
#
mkantor#0

Preview:```ts
type Methods = "get" | "delete" | "post" | "put"
interface TestOp<TOp extends string> {
id: TOp
}
type TestPath<TOps extends string = string> = Partial<{
[TMethod in Methods]: TestOp<TOps>
}>

function useTestPath<
TOps extends string,
TP extends TestPath<TOps>

(path: TP)
...```

autumn prawn
#

Ultimately I'm trying to get a mapped object of functions that have the names of id
can you explain this more and/or show some examples of desired usage? i/someone else may be able to give you ideas for alternate approaches

random dust
#

Oh interesting. Thanks for the help.

Ultimately I'm trying to make a sort of OpenAPI style spec in a shared lib between the front and back end. Then on the backend the routes get implemented and on the front end they get used to create api calling functions.

//Shared lib
const routes = defineRoutes({
  "/test": {
    get: {
      id: "getTest",
      //... TypeBox types etc
    },
    post: {
      id: "postTest"
    }
  }
});


//Client
const client = useRoutes(routes)

client.getTest()

client.postTest(/* body data */)


//Server
implementRoutes(routes, {
  "/test": {
     get: {
       async handle (req) {
          //server stuff
       }
     },
     post: {
       async handle (req) {
          //server stuff
       }
     }
  }
})
#

But mapping the inferred routes type into flat object of just the ids as keys has been a tad challenging. (At least on the type side of things)

autumn prawn
#

to make sure i get it, tell me if the following is right...

you want a generic type which when given a type like this as a parameter:

type Input = {
  "/test": {
    get: {
      id: "getTest"
    },
    post: {
      id: "postTest"
    }
  }
}

gives this as output:

type Output = {
  getTest: () => unknown
  postTest: () => unknown
  // (maybe with more specific function types, but i think that's not super important)
}
random dust
#

Essentially yes, there can be more routes under Input as well

autumn prawn
#

what happens if there are duplicate id values within the input?

random dust
#

In a perfect world there would be a compile time error. At runtime it currently just throws an error

#

But I'm not sure if it's possible to get typescript to error on that, so it's not particularly important

autumn prawn
#

what's the motivation for having these separate ids? seems like something like this would be simpler for both you and your users:

type Output = {
  "/test": {
    get: () => unknown
    post: () => unknown
  }
}
#

as a user i can imagine it getting annoying to manually specify these additional IDs (and make sure they remain unique) if i have many routes (which are already uniquely identified)

#

or if flatness is important, something like this perhaps:

type Output = {
  "GET /test": () => unknown
  "POST /test": () => unknown
}
random dust
#

Well its three fold. The path can also contain routing placeholders like /test/:id which has the unfortunate requirement of them also being captured by the type system so proper typing can be generated on the functions.

But also the OpenAPI spec includes operationIds for each path.

And finally client.opName() looks better than client["/test"].get() at least in my opinion

Swagger Docs
#

So long route names would make even longer function call signatures

autumn prawn
#

oh are you trying to conform to OpenAPI semantics? i interpreted "OpenAPI style spec" more loosely

#

anyway i'll play around with this in a bit and send an example. seems doable

random dust
#

I want to output an OpenAPI spec so I can use existing API doc tools, but I guess I'm not entirely married to the whole spec itself

#

Thanks, I really appreciate your help!

autumn prawn
#

how's this?

indigo quarryBOT
#
mkantor#0

Preview:```ts
// See https://stackoverflow.com/a/50375286/3625
type UnionToIntersection<U> =
(
U extends any ? (x: U) => void
: never
) extends (x: infer I) => void ? I
: never

type DoTheThing<T extends Record<PropertyKey, Record<PropertyKey, { id: string }>>> =
...```

random dust
#

This looks really promising. I didn't know you could use as in the mapped object type. That's really cool

autumn prawn
random dust
#

I think this is going to get me where I need to go! Thanks again for your help!