A
arktype5mo ago
francis

Is there a recommended way to transform a type union into a discriminated union by adding a tag?

I'm not sure I'm describing this well, but I have a situation with two types that I am .or-ing together. It all works, but I'm interacting with a library that needs a discriminated union to function properly for type narrowing, and I am not sure how to add a discriminated union tag based on which type in the Or resolved (if that makes sense?) Here's a simple example to demonstrate:
const first = type({ key: "string" });
const second = type({ key: "number" });
const combined = first.or(second);
const first = type({ key: "string" });
const second = type({ key: "number" });
const combined = first.or(second);
I'd like to have this be resolve to e.g. { key: string, tag: 'first' } | { key: number, tag: 'second' } based on which branch in the union was followed. I've tried it with this:
const first = type({ key: "string" }).pipe((o) => ({ ...o, _tag: "first" }) as const);
const second = type({ key: "number" }).pipe((o) => ({ ...o, _tag: "second" }) as const);
const combined = first.or(second);
const first = type({ key: "string" }).pipe((o) => ({ ...o, _tag: "first" }) as const);
const second = type({ key: "number" }).pipe((o) => ({ ...o, _tag: "second" }) as const);
const combined = first.or(second);
and it works - but seems inelegant. Is there a more idiomatic solution?
5 Replies
francis
francisOP5mo ago
Related generic typescript question, I've defined the following:
const addTag =
<S extends string>(_tag: S) =>
<T extends Record<string, unknown>>(obj: T) => ({ ...obj, _tag });
const addTag =
<S extends string>(_tag: S) =>
<T extends Record<string, unknown>>(obj: T) => ({ ...obj, _tag });
If I call myType.pipe(addTag('foo')), I get a Record<string, unknown> out. However, if I change this call to myType.pipe((obj) => addTag('foo')(obj)), I get the expect type back. Any ideas what's happening? it's not my function either hm, if I define const addSuccess = <T extends Record<string, unknown>>(obj: T) => ({ ...obj, _tag: 'success' }); passing that into pipe also doesn't work without creating an arrow function inside the pipe call yeah, for some reason it's not actually providing a type to the generic when I use the generic function directly (gentle bump - is there a better way than piping on each schema that makes up the union? also, still not sure how to pipe to a named function variable rather than an inline arrow one in a way that typechecks)
Dimava
Dimava5mo ago
import { scope } from 'arktype'
import { distill, Out } from 'arktype/out/keywords/inference'
import { instantiateType } from 'arktype/out/methods/instantiate'

type addTag<tag> = {
<V>(o: V): V & {_tag: tag}
}

type applyTag<$> = {
[tag in keyof $]: (In: distill.brandable.In<$[tag]>) => Out<distill.Out<$[tag]> & {_tag: tag}>
}
type oneOfScope<$> = instantiateType<$[keyof $], $> extends infer r ? r : never

export function addTagAndUnionize<const def>(
def: scope.validate<def>
): oneOfScope<applyTag<scope.infer<def>>> {
return scope({
...Object.fromEntries(
Object.entries(def)
.map(([k, v]) => [k, [v, '=>', ((o: object) => ({...o, _tag: k}))]])
) as {},
_result: Object.keys(def).join(' | ') as 'unknown.any'
}).export()._result as any
}

let T = addTagAndUnionize({
foo: {x: 'number'},
bar: {x: 'string'}
})

let v = T.assert({})
import { scope } from 'arktype'
import { distill, Out } from 'arktype/out/keywords/inference'
import { instantiateType } from 'arktype/out/methods/instantiate'

type addTag<tag> = {
<V>(o: V): V & {_tag: tag}
}

type applyTag<$> = {
[tag in keyof $]: (In: distill.brandable.In<$[tag]>) => Out<distill.Out<$[tag]> & {_tag: tag}>
}
type oneOfScope<$> = instantiateType<$[keyof $], $> extends infer r ? r : never

export function addTagAndUnionize<const def>(
def: scope.validate<def>
): oneOfScope<applyTag<scope.infer<def>>> {
return scope({
...Object.fromEntries(
Object.entries(def)
.map(([k, v]) => [k, [v, '=>', ((o: object) => ({...o, _tag: k}))]])
) as {},
_result: Object.keys(def).join(' | ') as 'unknown.any'
}).export()._result as any
}

let T = addTagAndUnionize({
foo: {x: 'number'},
bar: {x: 'string'}
})

let v = T.assert({})
francis
francisOP5mo ago
wow, you're amazing we should collect all this stuff in an arktype recipes resource somewhere. Maybe not included in the package, but helpful reusable bits
Dimava
Dimava5mo ago
Yes I am Would be useful I guess
francis
francisOP5mo ago
the 'make a tagged union from a list of definitions', 'require at least one key from an object', etc... I suspect these are not problems only I will have especially the tagged union from definition list which I am using to deal with some horrid APIs where an error is indicated by the presence of a key on an otherwise identical object

Did you find this page helpful?