#Infer param type based on object

41 messages · Page 1 of 1 (latest)

tight trench
#

I'm trying to infer a functions paramers based on a predefined object. So:

function doTheThing((data) => {}, { data: { type: 'string', optional: true, default: 'hello' }) 

Using this type:

type InferParams<T extends Params | undefined> = {
  [K in keyof T]: T[K]['optional'] extends true
    ? T[K]['default'] extends undefined
      ? InferParamType<T[K]> | undefined
      : InferParamType<T[K]>
    : InferParamType<T[K]>;
};

But I'm having trouble with the 'optional' and 'default' properties. If I remove that default clause it works great. If optional is true the param can be the type or undefined. But as soon as I add the default clause it is never optional, always just the type.

How do I get the type to correctly branch if there is no default?

inland tide
#

it's hard to be certain without a more complete example (e.g. you haven't shown how/where you're using the InferParams type), but my guess is that you're getting tripped up by distribution. the value true may be getting inferred as type boolean, which is equivalent to the union true | false, and conditional types distribute over unions

tight trench
#

Oh sure, here is all of the relevant types:

interface ParamDefinition {
  type: 'string' | 'number' | 'object';
  optional?: boolean;
  default?: any;
  properties?: Params;
}

interface Params {
  [key: string]: ParamDefinition;
}

type InferParamType<T extends ParamDefinition> = T['type'] extends 'string'
  ? string
  : T['type'] extends 'number'
  ? number
  : T['type'] extends 'object'
  ? InferParams<T['properties']>
  : never;

type InferParams<T extends Params | undefined> = {
  [K in keyof T]: T[K]['optional'] extends true
    ? T[K]['default'] extends undefined
      ? InferParamType<T[K]> | undefined
      : InferParamType<T[K]>
    : InferParamType<T[K]>;
};

type RouteConfig<T extends Params> = {
  method: 'GET' | 'POST' | 'PATCH';
  path: string;
  params?: T;
  /** Express Middleware to pass to the route before the handler function */
  middleware?: RequestHandler[];
  handler: (
    args: InferParams<T>,
    info: { req: Request; res: Response; rawParams: Record<string, any> }
  ) => Promise<any>;
};
inland tide
#

i threw that into a playground but i see some errors that don't seem to be the error you're describing. is this right?

native locustBOT
#
mkantor#0

Preview:```ts
import {RequestHandler} from "express"

interface ParamDefinition {
type: "string" | "number" | "object"
optional?: boolean
default?: any
properties?: Params
}

interface Params {
[key: string]: ParamDefinition
}

type InferParamType<T extends ParamDefinition> =
T["type"] extends "string"
? stri
...```

tight trench
#

That is right, I haven't taken the time to figure out why it doesn't like T[K] yet. It runs for now and I was planning on dealing with that later

inland tide
#

once you have type errors deep in a type like that, all bets are off. TS does its best to continue and show additional errors, but i wouldn't draw any solid conclusions from what happens later

#

here, i fixed that error and slightly refactored:

native locustBOT
#
mkantor#0

Preview:```ts
import {RequestHandler} from "express"

interface ParamDefinition {
type: "string" | "number" | "object"
optional?: boolean
default?: any
properties?: Params
}

interface Params {
[key: string]: ParamDefinition
}

type InferParamType<T extends ParamDefinition> =
T["type"] extends "string"
? stri
...```

inland tide
#

please make sure that's the behavior you want. if it is, can you add enough code to show the problem you were originally asking about (assuming my changes haven't fixed it)?

tight trench
#

Unfortunately that doesn't fix it. Maybe I can better describe the problem.

I'm using the function like this:

function doTheThing(
  ({ sortOrder }) => { //do some stuff }),
  { 
    params: { 
      sortOrder: {
        type: 'string',
        optional: true,
        default: 'dateCreated'
      }
    }
  }

With your changes sortOrder is always string. Even if I remove the default key, it is still string never string | undefined

inland tide
#

you still haven't told me how any of the types you shared relate to the doTheThing function. can you write it all out in the playground please?

tight trench
#

Here is the entire file:

#

Okay, well the playground link is too long

native locustBOT
#
serahph#0

Preview:```ts
export default function createRoutes(
shared?: SharedConfig
) {
const router = express.Router()

function defineRoute<T extends Params>(
config: RouteConfig<T>
) {
const middleware: RequestHandler[] = [
...(shared?.middleware || []),
...(config.middleware || []),
]
router[config.method.toLowerCase()](
config.path,
...middleware,
async (req: Request, res: Response)
...```

inland tide
#

there's a link shortener plugin built-in. check the right sidebar. but you could also try to reduce the example; i probably don't need the whole file

tight trench
#

So this is just the function that uses these types

inland tide
#

oh you did it already, thanks

#

i'm still lost, sorry. i don't see any parameter/variable named sortOrder there, and i see some seemingly-unrelated errors (like when indexing into rawParams you have Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{}'.). can you dumb it down for me and tell me exactly what line(s) i should be looking at here?

#

here's a complete playground of everything we have so far, in case that makes things easier:

native locustBOT
#
mkantor#0

Preview:```ts
import {RequestHandler} from "express"

interface ParamDefinition {
type: "string" | "number" | "object"
optional?: boolean
default?: any
properties?: Params
}

interface Params {
[key: string]: ParamDefinition
}

type InferParamType<T extends ParamDefinition> =
T["type"] extends "string"
? stri
...```

inland tide
#

(i had to guess the definition of SharedConfig—it may be wrong)

tight trench
#

Here is the use of the function, if you hover over sortOrder, it should show that the type is string when it should be string | undefined

inland tide
#

ah okay, i get it now. you were talking about callers of defineRoute. thanks

tight trench
#

Because optional is true and there is no defined default

inland tide
#

is the type of defineRoute (<T extends Params>(config: RouteConfig<T>) => void) what you expect it to be? or does that seem wrong?

#

(i know RouteConfig itself has this issue; i mean besides that)

tight trench
#

Yes, that seems to be working just fine

#

It's just the infered types of the handler params

inland tide
#

cool, gimme a few minutes to dig in

#

in the meantime here's a reduced reproduction of the problem that you can peruse:

native locustBOT
#
mkantor#0

Preview:```ts
import {RequestHandler} from "express"

interface ParamDefinition {
type: "string" | "number" | "object"
optional?: boolean
default?: any
properties?: Params
}

interface Params {
[key: string]: ParamDefinition
}

type InferParamType<T extends ParamDefinition> =
T["type"] extends "string"
? stri
...```

tight trench
#

Oh yeah, that's way simpler to follow

inland tide
#

so the problem is that default is still just any there, and any extends undefined

#

did you mean undefined extends T['default'] instead of T['default'] extends undefined perhaps?

tight trench
#

That does seem to fix it

inland tide
#

yeah i think that works the way you want

tight trench
#

That was such a simple fix I never would have thought of

#

Thank you very much

inland tide
#

sure thing, happy to help