A
arktype•3mo ago
Bjorno

Extending types similar to an interface

How would one go about extending types similar to how one would extend an interface in TypeScript? It's really the only thing I feel like is missing for ArkType to be a viable alternative. A minimal example of what I want for ArkType, but written in TypeScript:
interface BaseTitle {
kind: 'series' | 'episode' | 'movie';
name: string;
seasonNumber?: string;
episodeNumber?: string;
seasonCount?: number;
episodeCount?: number;
}

interface SeriesTitle extends BaseTitle {
kind: 'series';
seasonCount: number;
episodeCount: number;
seasonNumber?: never;
episodeNumber?: never;
}

interface EpisodeTitle extends BaseTitle {
kind: 'episode';
seasonNumber: string;
episodeNumber: string;
seasonCount?: never;
episodeCount?: never;
}

interface MovieTitle extends BaseTitle {
kind: 'movie';
seasonNumber?: never;
episodeNumber?: never;
seasonCount?: never;
episodeCount?: never;
}

type Title = SeriesTitle | EpisodeTitle | MovieTitle;
interface BaseTitle {
kind: 'series' | 'episode' | 'movie';
name: string;
seasonNumber?: string;
episodeNumber?: string;
seasonCount?: number;
episodeCount?: number;
}

interface SeriesTitle extends BaseTitle {
kind: 'series';
seasonCount: number;
episodeCount: number;
seasonNumber?: never;
episodeNumber?: never;
}

interface EpisodeTitle extends BaseTitle {
kind: 'episode';
seasonNumber: string;
episodeNumber: string;
seasonCount?: never;
episodeCount?: never;
}

interface MovieTitle extends BaseTitle {
kind: 'movie';
seasonNumber?: never;
episodeNumber?: never;
seasonCount?: never;
episodeCount?: never;
}

type Title = SeriesTitle | EpisodeTitle | MovieTitle;
12 Replies
PIat
PIat•3mo ago
You can do something like this:
const baseTitleType = type({
kind: "'series' | 'episode' | 'movie'",
name: 'string'
})

const seriesTitleType = type(baseTitleType, '&', {
kind: "'series'",
seasonCount: 'number'
})

const episodeTitleType = type(baseTitleType, '&', {
kind: "'episode'",
'episodeCount?': 'never'
})

const titleType = type(seriesTitleType, '|', episodeTitleType)
const baseTitleType = type({
kind: "'series' | 'episode' | 'movie'",
name: 'string'
})

const seriesTitleType = type(baseTitleType, '&', {
kind: "'series'",
seasonCount: 'number'
})

const episodeTitleType = type(baseTitleType, '&', {
kind: "'episode'",
'episodeCount?': 'never'
})

const titleType = type(seriesTitleType, '|', episodeTitleType)
If you really need it to work like interfaces, you can do this: https://discord.com/channels/957797212103016458/1162539860574347385/1162722103959760926
ssalbdivad
ssalbdivad•3mo ago
You can chain .and or .merge depending on the behavior you want. Instead of manually typing the keys as never, you can just add "+": "reject" on your object definitions to specify that no undeclared keys are allowed.
Bjorno
BjornoOP•3mo ago
I guess my question is more related to discriminated unions from a DX standpoint. I find myself not being able to declare types as I mostly do in TS with ArkType. And that's the only reason I can't adopt it.
ssalbdivad
ssalbdivad•3mo ago
You can literally write:
const BaseTitle = type({
kind: "'series'|'episode'|'movie'",
name: "string",
"seasonNumber?": "string",
"episodeNumber?": "string",
"seasonCount?": "number",
"episodeCount?": "number"
})

const SeriesTitle = BaseTitle.merge({
kind: "'series'",
"seasonCount": "number",
"episodeCount": "number",
"seasonNumber?": "never",
"episodeNumber?": "never"
})
const BaseTitle = type({
kind: "'series'|'episode'|'movie'",
name: "string",
"seasonNumber?": "string",
"episodeNumber?": "string",
"seasonCount?": "number",
"episodeCount?": "number"
})

const SeriesTitle = BaseTitle.merge({
kind: "'series'",
"seasonCount": "number",
"episodeCount": "number",
"seasonNumber?": "never",
"episodeNumber?": "never"
})
What could be more similar than that 😅
Bjorno
BjornoOP•3mo ago
It is similar, but it's not enforcing a constraint, you could merge with a kind not related to the original type.
ssalbdivad
ssalbdivad•3mo ago
I guess you want .and If the types are disjoint for a non-optional key, it would throw a parse error But the second still wouldn't have to be a subtype of the first
Bjorno
BjornoOP•3mo ago
Is there any way to make that work within ArkType? Or is it planned?
ssalbdivad
ssalbdivad•3mo ago
Yeah there are lots of ways one sec Something like this would be one way, although it would require a full subtype (not missing props):
const title = generic(["t", {
kind: "'series'|'episode'|'movie'",
name: "string",
"seasonNumber?": "string",
"episodeNumber?": "string",
"seasonCount?": "number",
"episodeCount?": "number"
}])("t")

const seriesTitle = title({
kind: "'series'",
name: "string",
seasonCount: "number",
episodeCount: "number",
"seasonNumber?": "never",
"episodeNumber?": "never"
})

const invalidTitle = title({
kind: "'whoops'",
name: "string",
seasonCount: "number",
episodeCount: "number",
"seasonNumber?": "never",
"episodeNumber?": "never"
})
const title = generic(["t", {
kind: "'series'|'episode'|'movie'",
name: "string",
"seasonNumber?": "string",
"episodeNumber?": "string",
"seasonCount?": "number",
"episodeCount?": "number"
}])("t")

const seriesTitle = title({
kind: "'series'",
name: "string",
seasonCount: "number",
episodeCount: "number",
"seasonNumber?": "never",
"episodeNumber?": "never"
})

const invalidTitle = title({
kind: "'whoops'",
name: "string",
seasonCount: "number",
episodeCount: "number",
"seasonNumber?": "never",
"episodeNumber?": "never"
})
It would be pretty easy to build a utility function or something that would iterate over each prop and check if it's present instead if you wanted to do that. Personally I think there would be better ways to express these kinds of relationships in general. I'm not sure I'd add the exact equivalent of interface extends to the API when and and or generics can be used to do something quite similar, but I'm open to considering it more. The implementation would be fairly easy externally anyways, so really a matter of whether it convolutes the API
Bjorno
BjornoOP•3mo ago
I'm assuming this would break if you had nested discriminated unions
ssalbdivad
ssalbdivad•3mo ago
Why? Another option that would give better type errors if something is incorrect for a complex constraint like that would be something like this:
const subtype = <base, sub extends base>(base:Type<base>, sub:Type<sub>) => {
if(!base.extends(sub as never)) throw new Error(`Type ${sub.expression} must extend ${base.expression}`)
return sub
}
const subtype = <base, sub extends base>(base:Type<base>, sub:Type<sub>) => {
if(!base.extends(sub as never)) throw new Error(`Type ${sub.expression} must extend ${base.expression}`)
return sub
}
You could change it to accept a validated definition so you don't have to wrap it in type with some extra work
Bjorno
BjornoOP•3mo ago
I don't think I understand Could you give an example of nested discriminated unions, similarly to how you would do in TS?
ssalbdivad
ssalbdivad•3mo ago
I don't know what you mean you can just do whatever you'd do in TS the same way:
type({
foo: {
key: "'bar'"
}
}).or({
foo: {
key: "'baz'"
}
})
type({
foo: {
key: "'bar'"
}
}).or({
foo: {
key: "'baz'"
}
})
Keep in mind internally arktype always discriminates unions for you so you'll get those optimizations for free without having to do anything special Including nested discriminated unions
Want results from more Discord servers?
Add your server