Extending built-in keywords

Hey folks 👋 I’m building a cool tool that uses (and extends) ArkType, and I'm trying to figure out the right way to "extend" the default keywords (like string, number, etc.) so they can have extra functionality/modules. Here’s what I’ve tried:
import { scope, type } from "arktype";
import { number } from "arktype/internal/keywords/number.ts";
import { string } from "arktype/internal/keywords/string.ts";

export const extensionToStringScope = scope({
host: "string.ip | 'localhost'",
});

export const extensionToNumberScope = scope({
port: type("string", "=>", (data, ctx) => {
const asNumber = Number.parseInt(data);
const isInteger = Number.isInteger(asNumber);
const isBetween = 0 <= asNumber && asNumber <= 65535;
if (!isInteger || !isBetween) {
ctx.mustBe("an integer between 0 and 65535");
}
return asNumber;
}),
});

const extendedStringModule = type.module({
...string,
...extensionToStringScope.export(),
});

const extendedNumberModule = type.module({
...number,
...extensionToNumberScope.export(),
});

export const rootScope = scope({
string: extendedStringModule,
number: extendedNumberModule,
});

const CoolHost = rootScope.type("string.host");
import { scope, type } from "arktype";
import { number } from "arktype/internal/keywords/number.ts";
import { string } from "arktype/internal/keywords/string.ts";

export const extensionToStringScope = scope({
host: "string.ip | 'localhost'",
});

export const extensionToNumberScope = scope({
port: type("string", "=>", (data, ctx) => {
const asNumber = Number.parseInt(data);
const isInteger = Number.isInteger(asNumber);
const isBetween = 0 <= asNumber && asNumber <= 65535;
if (!isInteger || !isBetween) {
ctx.mustBe("an integer between 0 and 65535");
}
return asNumber;
}),
});

const extendedStringModule = type.module({
...string,
...extensionToStringScope.export(),
});

const extendedNumberModule = type.module({
...number,
...extensionToNumberScope.export(),
});

export const rootScope = scope({
string: extendedStringModule,
number: extendedNumberModule,
});

const CoolHost = rootScope.type("string.host");
This works in my IDE, but: 1. I can’t build this (it’s not portable). 2. It relies on internal types (arktype/internal/...), which is probably why it breaks portability. Question: Is there a better way to extend the default keywords (string, number, etc) with additional modules, without reaching into internals? Thank you! (ArkType is awesome)
13 Replies
ssalbdivad
ssalbdivad2mo ago
Looks cool! The built-in keywords are exposed under type.keywords as well as the top-level ark import. The approach I suggested when @Eric was recently working on a similar problem could likely work here as well:
import { scope, type } from "arktype"

// add actual yaml validation logic here
const yaml = type.string.narrow(s => s === "yaml")

const $ = scope({
string: type.module({
...type.keywords.string,
yaml
})
})

const t = $.type({
yaml: "string.yaml"
})
import { scope, type } from "arktype"

// add actual yaml validation logic here
const yaml = type.string.narrow(s => s === "yaml")

const $ = scope({
string: type.module({
...type.keywords.string,
yaml
})
})

const t = $.type({
yaml: "string.yaml"
})
https://discord.com/channels/957797212103016458/1410750863198130367
yam.codes
yam.codesOP2mo ago
Thanks @ssalbdivad, I'll give it a shot! What is the meaning of the "$" symbol (the dollar sign) in the above code? Does it mean "root scope"? I've seen it used elsewhere in the project to define generics:
type validate<def, $ = {}, args = bindThis<def>> = validateDefinition<def, $, args>;
type validate<def, $ = {}, args = bindThis<def>> = validateDefinition<def, $, args>;
ssalbdivad
ssalbdivad2mo ago
It means the scope being used to parse the definition (built-in keywords are included by default so it doesn't pollute every hover)
yam.codes
yam.codesOP2mo ago
Thanks @ssalbdivad, that explains the generic. I meant the constant name in your code snippet, though:
const $ = scope({
string: type.module({
...type.keywords.string,
yaml
})
})
const $ = scope({
string: type.module({
...type.keywords.string,
yaml
})
})
Why use $ as the variable name? What does it mean? Similar code can be found under "thunks", where $ is also used as a name for a const https://arktype.io/docs/scopes#thunks
ssalbdivad
ssalbdivad2mo ago
I use it as a convention at runtime as well to refer to a variable that represents a scope. You could name it something like myScope or whatever you prefer, but internally $ is a convention for referring to either a type parameter or variable that represents a scope
yam.codes
yam.codesOP2mo ago
Awesome, thanks @ArkDavid , I am trying to be as consistent as possible with the established convention (even internal ones) so it's good to know. It also helps me understand the codebase better. ❤️ My code works great now, thanks to you. I appreciate your help! P.s. I'm building an environment variable library based on ArkType, you can check it out here https://arkenv.js.org/. Once it's a little more baked I'd love to link to it on the ArkType website somehow - maybe in a new Plugins, or 3rd-Party Integrations section? Similar discussion: https://discord.com/channels/957797212103016458/1085979656919994429/1402366141413986389
ArkEnv: Typesafe environment variables powered by ArkType
ArkEnv is a tool for managing environment variables in your project.
No description
ssalbdivad
ssalbdivad2mo ago
The website is 🔥 If you'd be willing to create a PR adding a new ecosystem section like that I would merge it!
Eric
Eric2mo ago
Starred! I’ll use this tonight. Nice work 🤩
omarishere
omarishere2mo ago
I was looking for that section just now 😆 , need to add my project to it
yam.codes
yam.codesOP2mo ago
Wow!!! Thank you so much 🙏 (I really respect your work) Let’s make this section awesome!
ssalbdivad
ssalbdivad2mo ago
@yam.codes hey small suggestion for the arkenv API. If the primrary export is called arkenv or arkEnv (or anything starting with ark or Ark), anyone with the ArkType extension installed will also get syntax highlighting, which is quite a nice DX improvement especially if you have string literal unions like those from your NODE_ENV example.

Did you find this page helpful?