A
arktype•2mo ago
picklemik

Can't infer type of schema when using "scope"

I am using arktype to parse data from incoming web requests. I define the schema and the parsed result will be passed into my handler. It looks like this and is working perfectly fine right now:
server.endpoint({
url: "PUT /admin/user/:id",
auth: "basic",
params: type({
id: "string",
}),
query: type({
"year?": "string.integer.parse",
}),
form: type({
firstName: "string",
lastName: "string",
code: "string",
})
async handler(req, reply) {
// req.params has the infered type from the schema, e.g. { id: string }
}
server.endpoint({
url: "PUT /admin/user/:id",
auth: "basic",
params: type({
id: "string",
}),
query: type({
"year?": "string.integer.parse",
}),
form: type({
firstName: "string",
lastName: "string",
code: "string",
})
async handler(req, reply) {
// req.params has the infered type from the schema, e.g. { id: string }
}
What I want to do now is change id in the params schema to not be a string, but instead define my own "objectId" value. It would parse the string into a mongo ObjectId object, so I don't have to do that in the handler. I believe scopes are what I am looking for, so I tried doing that, but it breaks my type system and I don't understand why. I recreated the issue I have in this playground: https://tinyurl.com/arktype
ArkType
ArkType Playground
TypeScript's 1:1 validator, optimized from editor to runtime
7 Replies
TizzySaurus
TizzySaurus•2mo ago
The easiest thing is probably a morph So type({id: ["string", "=>", parseStringToObjectId]})
TizzySaurus
TizzySaurus•2mo ago
ArkType
ArkType Docs
TypeScript's 1:1 validator, optimized from editor to runtime
picklemik
picklemikOP•2mo ago
Thanks, but that is not really the problem I have. I am already using morph, it has to have something to do with the scope and my generic type in the function. This is the code from the playground, there you can see the type error
import { type, scope, Type } from "arktype"

class ObjectId {
constructor(public id: string) {}
}

const Scope = scope({
objectId: type("string.hex").pipe((s) => new ObjectId(s))
})

const NotWorkingThing = Scope.type({
id: "objectId"
})

const WorkingThing = type({
id: "string.hex"
})

function whyDoesThisNotWork<T extends Type>(schema: T, fn: (v: T["infer"]) => void) {
const result = schema({ id: "12345" })
fn(result)
}

// This works with a basic type
whyDoesThisNotWork(WorkingThing, (v) => {
console.log(v.id)
})

// This is using a scope and does not work anymore
whyDoesThisNotWork(NotWorkingThing, (v) => {
console.log(v.id)
})
import { type, scope, Type } from "arktype"

class ObjectId {
constructor(public id: string) {}
}

const Scope = scope({
objectId: type("string.hex").pipe((s) => new ObjectId(s))
})

const NotWorkingThing = Scope.type({
id: "objectId"
})

const WorkingThing = type({
id: "string.hex"
})

function whyDoesThisNotWork<T extends Type>(schema: T, fn: (v: T["infer"]) => void) {
const result = schema({ id: "12345" })
fn(result)
}

// This works with a basic type
whyDoesThisNotWork(WorkingThing, (v) => {
console.log(v.id)
})

// This is using a scope and does not work anymore
whyDoesThisNotWork(NotWorkingThing, (v) => {
console.log(v.id)
})
TizzySaurus
TizzySaurus•5w ago
Oh, right, I think you need to use type.Any or Type<unknown, typeof Scope> (one/both should work) Instead of just Type in extends Type
picklemik
picklemikOP•5w ago
Perfect, thanks! I replaced all my extends Type with extends type.Any and I think it works now Maybe one additional quick question while you are already here 🫣 Where would I best put my scope that I would want to reuse often? Should I just put it in some util file and use that instead of the default type when I create my types or is there something more elegant In this case e.g. it is kind of an "extension" or "plugin" where I want to provide types for mongo db But not sure if I should create multiple scopes, or just one global scope I use in the whole app
TizzySaurus
TizzySaurus•5w ago
I think a util file is way to go Whether you want one scope or multiple is kinda down to you I typically think of scopes as a "category" of types, and use different scopes per "category" Fwiw iirc you can have nested scopes which is how things like string.hex or string.numeric.parse are implemented Actually, I think that's submodules which is a different thing. I forget the technical details
picklemik
picklemikOP•5w ago
Okay, will check that out. For now I think I will just "replace" the arktype type function with my own scope. Like
import { scope, type as _type } from "arktype";
import { ObjectId } from "mongodb";

export const type = scope({
"string.objectId.parse": _type("string.hex").pipe((s) => new ObjectId(s)).as<ObjectId>,
objectId: _type.instanceOf(ObjectId),
}).type;

...
import { type } from "../lib/arktype.ts";
const schema = type({
id: "string.objectId.parse",
})
import { scope, type as _type } from "arktype";
import { ObjectId } from "mongodb";

export const type = scope({
"string.objectId.parse": _type("string.hex").pipe((s) => new ObjectId(s)).as<ObjectId>,
objectId: _type.instanceOf(ObjectId),
}).type;

...
import { type } from "../lib/arktype.ts";
const schema = type({
id: "string.objectId.parse",
})
Not sure if this is a good idea in the long term, but it is easy and works for now. The string.objectId.parse works, but I don't get auto complete for these. Maybe the nested scopes/submodules will help there Thanks anyway, helped a lot

Did you find this page helpful?