Wrapper around `type()`

In my configuration loader that I'm building, I have a "registry" pattern where I want to have developers declare the type schema of the config they expect, and then they need to register the config in order to get it loaded, looks something like this:
const GoodConfig = registerConfig(
'GoodConfig',
type({
server: {
base_url: 'url',
},
customer_support_email: 'email',
widget_counter_increment: 'parse.integer',
'some_optional_thing?': 'uuid',
})
);
const GoodConfig = registerConfig(
'GoodConfig',
type({
server: {
base_url: 'url',
},
customer_support_email: 'email',
widget_counter_increment: 'parse.integer',
'some_optional_thing?': 'uuid',
})
);
But I don't love having registerConfig() wrapped around type(), instead I would love to just have the second parameter to the register function be passed through from type() so that I could call type() internally, i.e.
function registerConfig(name:string, typeDef: Parameters<typeof type>[0]){
const configType = type(typeDef)
//...etc
function registerConfig(name:string, typeDef: Parameters<typeof type>[0]){
const configType = type(typeDef)
//...etc
This is ideal as it avoids the need to import both my config library and arktype, as well as just being simpler for the consumer of my library. The problem I'm having is that Parameters<typeof type>[0] is not resolving correctly. Is there a better way to get to that type?
51 Replies
ssalbdivad
ssalbdivad9mo ago
This is the pattern you'll want to use:
const registerConfig = <const def>(
name: string,
def: validateTypeRoot<def>
): Type<inferTypeRoot<def>, {}> => {
const t = type(def)
// implementation
}
const registerConfig = <const def>(
name: string,
def: validateTypeRoot<def>
): Type<inferTypeRoot<def>, {}> => {
const t = type(def)
// implementation
}
SneakyTurtle
SneakyTurtleOP9mo ago
Oh huzzah! What's the name of the <const def> syntax? I see that all over the arktype codebase but I've not used it previously
ssalbdivad
ssalbdivad9mo ago
I've just heard const type parameters, it was introduced in TS 5.0 I believe
SneakyTurtle
SneakyTurtleOP9mo ago
ahhh I'm getting some angry ban-types linting on the use of {} here, is that expected? And type(def) is not happy either the ts error for type(def) is
Argument of type 'validateTypeRoot<def>' is not assignable to parameter of type '[def] extends [Terminal] ? Terminal & def : ...
Argument of type 'validateTypeRoot<def>' is not assignable to parameter of type '[def] extends [Terminal] ? Terminal & def : ...
it goes on for many lines after that
ssalbdivad
ssalbdivad9mo ago
A lot of built-in lint rules suck Well at least for working with complex types like this
SneakyTurtle
SneakyTurtleOP9mo ago
got it mostly making sure that use of {} isn't some weird footgun that I need to be worried about
ssalbdivad
ssalbdivad9mo ago
You will want to cast def to never there, since it's trying to validate it.
SneakyTurtle
SneakyTurtleOP9mo ago
gotcha
ssalbdivad
ssalbdivad9mo ago
No it should be used in a scenario like this as a "baseline" for a mapped type I think in a very recent version of eslint they actually changed the default complaint about that type to hopefully be more descriptive Personally I think the lint rule about {} is actually more of a foot-gun than {} itself I've seen a bunch of people struggling with types where they followed the advice of the lint rule and replaced it with Record<string, never> or similar then tried to map over that but it adds an index signature you definitely don't want when used in that way
SneakyTurtle
SneakyTurtleOP9mo ago
Yeah I've definitely hit that before I built a pretty cool ts rpc library called cheep that had some great complex typing stuff, this is bringing back memories for me
ssalbdivad
ssalbdivad9mo ago
I think TS's greatest strength is the flexibility it provides library authors to create amazing DX at the cost of a lot of casting internally but the trade-off is worth it because if written correctly, end users get more precise types and have to cast less But a lot of people get caught in the mentality they should never have to cast which if you're writing anything to do with runtime validation you have to give up on very quickly 😛
SneakyTurtle
SneakyTurtleOP9mo ago
aint that the truth 🤣 Thanks for all the help, between your quick responses and the awesome work you've done on arktype this little project has been remarkably smooth
ssalbdivad
ssalbdivad9mo ago
Great to hear! Hopefully things will be even easier soon once the new docs are done 🙏
SneakyTurtle
SneakyTurtleOP2w ago
Looking forward to it! Hey @ArkDavid - Super excited to see v2 landing, I'm in the process of upgrading and am a bit stuck on upgrading this pattern with the changes that happened under the hood; I've tried converting to this, but I'm not clear on what the second arg to the new *Root types should be:
export async function registerConfig<const def>(
args: ConfigRegistrationArgs,
def: validateRoot<def, ?>
): Promise<distillOut<inferRoot<def, ?>>> {...
export async function registerConfig<const def>(
args: ConfigRegistrationArgs,
def: validateRoot<def, ?>
): Promise<distillOut<inferRoot<def, ?>>> {...
(I'm getting validateRoot and inferRoot from @arktype/schema) I took a shot in the dark and used {} but that yields a bunch of errors on previously good tests, to the tune of Type '{ base_url: string; }' is not assignable to type '"'server' is not on an intersection schema "'.ts(2322) (example usage with that error:)
const GoodConfig = registerConfig(
{
projectName: 'good-project',
},
{
server: {
base_url: 'url',
},
customer_support_email: 'email',
widget_counter_increment: 'parse.integer',
'some_optional_thing?': 'uuid',
get_this: {
from_ssm: 'string',
},
}
);
const GoodConfig = registerConfig(
{
projectName: 'good-project',
},
{
server: {
base_url: 'url',
},
customer_support_email: 'email',
widget_counter_increment: 'parse.integer',
'some_optional_thing?': 'uuid',
get_this: {
from_ssm: 'string',
},
}
);
ssalbdivad
ssalbdivad2w ago
I think you can just use type.infer<def> And type.validate<def>
SneakyTurtle
SneakyTurtleOP2w ago
Oh excellent Thanks!
ssalbdivad
ssalbdivad2w ago
Let me know if you have any issues!
SneakyTurtle
SneakyTurtleOP2w ago
One more question: I had previously been accessing .description on the result of calling type(def) in some logs, is there something similar to that elsewhere? Oh wait, that should still be there, I think I just have a bad cast happening
ssalbdivad
ssalbdivad2w ago
Yeah should still work! Had me worried for a second haha
SneakyTurtle
SneakyTurtleOP2w ago
Hmmm, actually I'm a little stumped:
export async function registerConfig<const def>(
args: ConfigRegistrationArgs,
def: type.validate<def>
): Promise<distillOut<type.infer<def>>> {
verifyConfigRegistrationArgs(args);
const config = type(def);
console.warn(
`Registering [${config.description}]${
args.enableDefaultSsmParameters
? ' with default SSM namespace enabled'
: ''
}`
);
export async function registerConfig<const def>(
args: ConfigRegistrationArgs,
def: type.validate<def>
): Promise<distillOut<type.infer<def>>> {
verifyConfigRegistrationArgs(args);
const config = type(def);
console.warn(
`Registering [${config.description}]${
args.enableDefaultSsmParameters
? ' with default SSM namespace enabled'
: ''
}`
);
config.description is not happy there
ssalbdivad
ssalbdivad2w ago
This is likely just a side effect of how TS assigns wonky types to values derived from generic parameters within an implementation
SneakyTurtle
SneakyTurtleOP2w ago
Ok, so safe to ignore then? I can cast for this specific use
ssalbdivad
ssalbdivad2w ago
One option would be an internal and external signature:
async function registerConfig<const def>(
args: ConfigRegistrationArgs,
def: type.validate<def>
): Promise<distillOut<type.infer<def>>>
async function registerConfig(
args: ConfigRegistrationArgs,
def: unknown
): Promise<unknown> {

}
async function registerConfig<const def>(
args: ConfigRegistrationArgs,
def: type.validate<def>
): Promise<distillOut<type.infer<def>>>
async function registerConfig(
args: ConfigRegistrationArgs,
def: unknown
): Promise<unknown> {

}
type.raw may also work type.raw is probably the easiest solution I guess It's just type without any inference
SneakyTurtle
SneakyTurtleOP2w ago
ahh
ssalbdivad
ssalbdivad2w ago
I don't think you need distillOut either
SneakyTurtle
SneakyTurtleOP2w ago
Yeah was wondering about that; should I be able to do all of this without touching @arktype/schema?
ssalbdivad
ssalbdivad2w ago
Let me double check You can use type.infer.Out<def> You should, and everything exposed through arktype directly will be stable whereas @ark/schema is still not
SneakyTurtle
SneakyTurtleOP2w ago
awesome; looks like this works for me, do I have it right?

`Registering [${name}: ${(config as ReturnType<typeof type.raw>).description}]${

`Registering [${name}: ${(config as ReturnType<typeof type.raw>).description}]${
ssalbdivad
ssalbdivad2w ago
I don't think you should need to cast?
SneakyTurtle
SneakyTurtleOP2w ago
Oh, so I should use type.raw instead of type()
ssalbdivad
ssalbdivad2w ago
Yeah They're the same thing under the hood but it avoids type-level validation/inference that is meant for literal definitions
SneakyTurtle
SneakyTurtleOP2w ago
Ok, that combined with the internal/external definition seems like a good strategy Thanks!
ssalbdivad
ssalbdivad2w ago
I don't think you should need the internal/external definition unless you still want it I guess you'd have to cast the return though if you get rid of it so whatever you prefer
SneakyTurtle
SneakyTurtleOP2w ago
Yeah, casting the return is probably fine
ssalbdivad
ssalbdivad2w ago
Either way- glad it worked out 😊
SneakyTurtle
SneakyTurtleOP2w ago
Thanks again for the great work on this library, I'm finally getting some of my coworkers turned onto it after months, which is fun to see
ssalbdivad
ssalbdivad2w ago
Exciting the docs are finally in a place people can get up and running! Though admittedly still harder for people writing wrapper APIs like you, still need to add more docs for those types And honestly no matter how much I do document there will always be nuances of TS that can make that tricky
SneakyTurtle
SneakyTurtleOP2w ago
Yeah totally; I appreciate the immense effort that clearly went into the docs up to this point Ok, odd question here, this should work, right?
type({
x: 'string.url'
})
type({
x: 'string.url'
})
I'm getting Type '"string.url"' is not assignable to type '"'string' must reference a module to be accessed using dot syntax "' this is using pure arktype, not my wrapper
ssalbdivad
ssalbdivad2w ago
This is the kind of thing that can occur if certain imports are not being resolved correctly Do you have moldule/moduleResolution NodeNext or similar in your tsconfig? Ideally it should also work for older settings but that's my guess as to what's going on
SneakyTurtle
SneakyTurtleOP2w ago
module resolution is node this is in an Nx monorepo, so tsconfigs are a bit of a wormhole
ssalbdivad
ssalbdivad2w ago
Yeah I would highgly recommend updating that as the features node is missing have been in node since like v12
SneakyTurtle
SneakyTurtleOP2w ago
Yeah, that's very valid thanks for the pointer
ssalbdivad
ssalbdivad2w ago
Basically it means you can't resolve any package.json exports
SneakyTurtle
SneakyTurtleOP2w ago
and that fixed it
ssalbdivad
ssalbdivad2w ago
Which is likely to cause issues for lots of other libraries as well
SneakyTurtle
SneakyTurtleOP2w ago
I suspect that may have also caused some issues with remix and shacdn now that I think of it
ssalbdivad
ssalbdivad2w ago
But also thanks for the heads up, there's probably something I can do on my end to make sure it works for node as well.... I noticed some issues with the tests that would have caught that issue in the last release Yeah it's really unfortunate, node should really be deprecated at this point It sounds innocuous but it's so outdated now
SneakyTurtle
SneakyTurtleOP2w ago
Hmmm, so I thought I had this sorted, but came back to it today and found that just changing to nodenext didn't solve it for me; here's the fully resolved tsconfig for reference, hoping that maybe you can spot something else that isn't playing nicely 🤞
{
"compilerOptions": {
"rootDir": "../../..",
"sourceMap": true,
"declaration": true,
"moduleResolution": "nodenext",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"importHelpers": true,
"module": "commonjs",
"target": "es2020",
"lib": [
"es2020",
"dom"
],
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"baseUrl": "../../..",
"paths": {"redacted":"..."},
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"outDir": "../../../dist/out-tsc",
"types": [
"node"
],
"resolvePackageJsonExports": true,
"resolvePackageJsonImports": true,
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"strictBuiltinIteratorReturn": true,
"alwaysStrict": true,
"useUnknownInCatchVariables": true
},
"files": [
"./src/index.ts",
"./src/lib/loader.ts",
"./src/lib/registry.ts",
"./src/lib/ssm.ts",
"./src/lib/typeguards.ts",
"./src/lib/types.ts"
],
"include": [
"src/**/*.ts"
],
"exclude": [
"vite.config.ts",
"src/**/*.spec.ts",
"src/**/*.test.ts"
]
}
{
"compilerOptions": {
"rootDir": "../../..",
"sourceMap": true,
"declaration": true,
"moduleResolution": "nodenext",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"importHelpers": true,
"module": "commonjs",
"target": "es2020",
"lib": [
"es2020",
"dom"
],
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"baseUrl": "../../..",
"paths": {"redacted":"..."},
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"outDir": "../../../dist/out-tsc",
"types": [
"node"
],
"resolvePackageJsonExports": true,
"resolvePackageJsonImports": true,
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"strictBuiltinIteratorReturn": true,
"alwaysStrict": true,
"useUnknownInCatchVariables": true
},
"files": [
"./src/index.ts",
"./src/lib/loader.ts",
"./src/lib/registry.ts",
"./src/lib/ssm.ts",
"./src/lib/typeguards.ts",
"./src/lib/types.ts"
],
"include": [
"src/**/*.ts"
],
"exclude": [
"vite.config.ts",
"src/**/*.spec.ts",
"src/**/*.test.ts"
]
}
scratch that seems to have been a weird state that got trapped in the TS language server; went away after a restart
ssalbdivad
ssalbdivad2w ago
I would also upgrade to 2.0.2 I think I removed the import that would have broken "node", although TBH I couldn't even get node working anymore internally for the tests so it feels like that ship has sailed in terms of support haha I noticed it is officially deprecated by TS at least 🎉
SneakyTurtle
SneakyTurtleOP2w ago
Yeah, I get a tooltip telling me that its a bad idea at the very least!
ssalbdivad
ssalbdivad2w ago
Nice, not sure when that happened but glad I'm fighting slightly less of an uphill battle now haha The features not supported in "node" (package.json exports) were added in Node 12 so it's crazy out of date

Did you find this page helpful?