A
arktypeā€¢4mo ago
aabad_ankit

Can I add a default value to a type if everything other satisfies in a union type

const type1 = type({name: "string"}) const type2 = type({city: "string"}) const union = type1.or(type2) const result1 = union({name:"ankit"}) const result2 = union({city:"Ajmer"}) I want result1 to be {name: "ankit",kind:"name"} I want result2 to be {city:"Ajmer",kind:"city"} or better const [kind,result] = union({name:"ankit"}) here kind will be "name" and result will be {name:"ankit"} and kind type will be "name"| "city" can I add these default value of kind in the type definitions so that if parsed with that validator from the union the kind is added
33 Replies
Dimava
Dimavaā€¢4mo ago
You should be able to union morphs If they have incompatible inputs What if you have {city, name}? šŸ¤” @aabad_ankit
import { type } from 'arktype'

let c = type({city: 'string', '+': 'reject'}).pipe(o => ({...o, type: 'city'}))
let n = type({name: 'string', '+': 'reject'}).pipe(o => ({...o, type: 'name'}))

let cn = type([c, '|', n])

console.log(
cn({city: 'foo'}),
cn({name: 'foo'}),
'\n' + cn({city: 'foo', name: 'foo'}) + '\n',
cn({}) + '',
)
import { type } from 'arktype'

let c = type({city: 'string', '+': 'reject'}).pipe(o => ({...o, type: 'city'}))
let n = type({name: 'string', '+': 'reject'}).pipe(o => ({...o, type: 'name'}))

let cn = type([c, '|', n])

console.log(
cn({city: 'foo'}),
cn({name: 'foo'}),
'\n' + cn({city: 'foo', name: 'foo'}) + '\n',
cn({}) + '',
)
{
city: "foo",
type: "city",
} {
name: "foo",
type: "name",
}
name must be removednull or city must be removednull
city must be a string (was missing) or name must be a string (was missing)
{
city: "foo",
type: "city",
} {
name: "foo",
type: "name",
}
name must be removednull or city must be removednull
city must be a string (was missing) or name must be a string (was missing)
Wait @ArkDavid wtf is removednull
aabad_ankit
aabad_ankitOPā€¢4mo ago
@Dimava the Monoreaper If I try this
const baseType = type({ id: "string" }).pipe((o) => {
return { userId: o.id };
});

const newType = baseType.and({name: "string"})

console.log(newType({name:"ankit",id:"102"}))
const baseType = type({ id: "string" }).pipe((o) => {
return { userId: o.id };
});

const newType = baseType.and({name: "string"})

console.log(newType({name:"ankit",id:"102"}))
1. merge is not available on baseType 2. I am not getting the name in the result, it seems the pipe operation is applied in the end
ssalbdivad
ssalbdivadā€¢4mo ago
You can't apply structural operations like merge on a morphed type. For now at least, I'd just create a unions of the inputs then manage the discrimination logic in the pipe implementation from the union
ssalbdivad
ssalbdivadā€¢4mo ago
GitHub
How should you .pipe to a type constructed with or? Ā· arktypeio...
I have the following: const objSchema = type({ action: "'scheduled' | 'rescheduled' | 'canceled' | 'changed'", id: "parse.integer", calendarID: &...
ssalbdivad
ssalbdivadā€¢4mo ago
It would be theoretically possible to discriminate using strict key presence but it's not implemented yet: https://github.com/arktypeio/arktype/issues/786
aabad_ankit
aabad_ankitOPā€¢4mo ago
I need to learn a lot šŸ™‚ . This library is just awesome.
ssalbdivad
ssalbdivadā€¢4mo ago
Thank you šŸ˜Š Working on more docs now!
aabad_ankit
aabad_ankitOPā€¢4mo ago
docs will certainly help. @ArkDavid How can I get better error msg for this
const type1 = type({"name": "string",age:"number"})
const type2 = type({"city": "string"})

const union = type([type1,"|",type2])
const result = union({"age":12})
console.log(result.summary)
const type1 = type({"name": "string",age:"number"})
const type2 = type({"city": "string"})

const union = type([type1,"|",type2])
const result = union({"age":12})
console.log(result.summary)
prints "name must be a string (was missing) or city must be a string (was missing)" since age is already there shouldn't it just say "name must be a string (was missing) "
ssalbdivad
ssalbdivadā€¢4mo ago
That would require the issue I mentioned before where key presence could be used as a discriminant If you want total control over how the error messages are handled you could use .narrow and create them yourself If you had a discriminant key like kind: "person" kind: "city" or similar, it would check that first, then give the error message based on the branch it is on It could theoretically do that based on key presence once that issue is implemented Oh I guess in those cases you don't have onUndeclaredKeys at all though So the reason it would give both is because it is true that {city: "foo", age: 12} would also be valid Seems to be missing an "or", will fix that But wait null isn't even allowed
ssalbdivad
ssalbdivadā€¢4mo ago
Whoops found it
No description
aabad_ankit
aabad_ankitOPā€¢4mo ago
I am getting name must be a string (was missing) or city must be a string (was missing) for both the code snippets below.
const type1 = type({"name": "string",age:"number","+": "delete"})
const type2 = type({"city": "string","+": "delete"})
const union = type([type1,"|",type2])
const result = union({"age":12})
console.log(result.summary)
const type1 = type({"name": "string",age:"number","+": "delete"})
const type2 = type({"city": "string","+": "delete"})
const union = type([type1,"|",type2])
const result = union({"age":12})
console.log(result.summary)
and
const type1 = type({ name: "string", age: "number", "+": "delete" }).pipe(
(o) => {
return { ...o, kind: "person" as const };
}
);
const type2 = type({ city: "string", "+": "delete" }).pipe((o) => {
return { ...o, kind: "city" as const };
});

const union = type([type1, "|", type2]);
const result = union({ age: 12, kind: "city" });
console.log(result.summary);
const type1 = type({ name: "string", age: "number", "+": "delete" }).pipe(
(o) => {
return { ...o, kind: "person" as const };
}
);
const type2 = type({ city: "string", "+": "delete" }).pipe((o) => {
return { ...o, kind: "city" as const };
});

const union = type([type1, "|", type2]);
const result = union({ age: 12, kind: "city" });
console.log(result.summary);
ssalbdivad
ssalbdivadā€¢4mo ago
That is expected, it's validating the input not the output. Adding a kind: "city" after it's already been validated wouldn't change anything
aabad_ankit
aabad_ankitOPā€¢4mo ago
Oh Yeah, Understood šŸ‘ @ArkDavid @Dimava the Monoreaper What will be the most idiomatic way to do the validation for such union types to get correct error msg. Do I need to check for the properties and do validation by creating a validator map
import { type } from "arktype";
import { keys } from "remeda";

const nameArk = type({ name: "string" }).onDeepUndeclaredKey("delete");
const cityArk = type({ city: "string" }).onDeepUndeclaredKey("delete");
const vm = {
"name": nameArk,
"city": cityArk
};
const input = { name: "ark" };
let result: typeof nameArk.infer | typeof cityArk.infer;
for (const key of keys(vm)) {
if (key in input) {
result = vm[key].assert(input);
break;
}
}

// peform pipe operation here
import { type } from "arktype";
import { keys } from "remeda";

const nameArk = type({ name: "string" }).onDeepUndeclaredKey("delete");
const cityArk = type({ city: "string" }).onDeepUndeclaredKey("delete");
const vm = {
"name": nameArk,
"city": cityArk
};
const input = { name: "ark" };
let result: typeof nameArk.infer | typeof cityArk.infer;
for (const key of keys(vm)) {
if (key in input) {
result = vm[key].assert(input);
break;
}
}

// peform pipe operation here
This is getting verbose. I am giving example of just one field but my inputs are larger and I have 5 different types of input. If I could have done union then I would have declared on deepUndeclaredkey("delete") on just the union as well as the pipe on the union
ssalbdivad
ssalbdivadā€¢4mo ago
What is the actual problem you are having?
aabad_ankit
aabad_ankitOPā€¢4mo ago
const nameArk = type({ name: "string", age: "number" }).onDeepUndeclaredKey(
"delete"
);
const cityArk = type({ city: "string" }).onDeepUndeclaredKey("delete");
const input = { name: "ark" };
const result = nameArk.or(cityArk)(input)
console.log(result.summary)
const nameArk = type({ name: "string", age: "number" }).onDeepUndeclaredKey(
"delete"
);
const cityArk = type({ city: "string" }).onDeepUndeclaredKey("delete");
const input = { name: "ark" };
const result = nameArk.or(cityArk)(input)
console.log(result.summary)
This prints age must be a number (was missing) or city must be a string (was missing). I just want the error msg to be age must be a number (was missing) to do that I am currently doing something like this
const vm = {
"name": nameArk,
"city": cityArk
};
const input = { name: "ark" };
let result: typeof nameArk.infer | typeof cityArk.infer;
for (const key of keys(vm)) {
if (key in input) {
result = vm[key].assert(input);
break;
}
}
const vm = {
"name": nameArk,
"city": cityArk
};
const input = { name: "ark" };
let result: typeof nameArk.infer | typeof cityArk.infer;
for (const key of keys(vm)) {
if (key in input) {
result = vm[key].assert(input);
break;
}
}
so I wanted to know if there is more idiomatic way to do this
ssalbdivad
ssalbdivadā€¢4mo ago
Until there is built-in discrimination based on key presence, I think I would write a custom narrow that checks which special key is in the input, then just invokes that type That seems somewhat like what you are doing but I'd probably write it as a wrapper that accepts a list of types then creates that transformation internally Also built-in key presence discrimnation I guess wouldn't work if you have delete for undelcared keys, only strict (not that it matters yet because it doesn't exist)
aabad_ankit
aabad_ankitOPā€¢4mo ago
but delete on undeclared key will work on union right once you have built in discrimination based on key presence
ssalbdivad
ssalbdivadā€¢4mo ago
I don't think it should. This is kind of fuzzy territory since it includes transformations but if I say I have some type it is allowed to have arbitrary additional keys and they will be removed, it doesn't seem safe to use arbitrary additional keys to discriminate Because keep in mind, discrimination means you are skipping all other branches of the union But those could be valid and just have some keys deleted Unless it's actually reject
Dimava
Dimavaā€¢4mo ago
@ArkDavid btw can you declare a prop as prop?: never?
ssalbdivad
ssalbdivadā€¢4mo ago
Yes
Dimava
Dimavaā€¢4mo ago
@aabad_ankit you should use it for now
ssalbdivad
ssalbdivadā€¢4mo ago
That is a good way to create a discriminated union on key presence that TS will respect
aabad_ankit
aabad_ankitOPā€¢4mo ago
a code sample will be super useful
ssalbdivad
ssalbdivadā€¢4mo ago
I don't think it will help you because it would still not be used as a discriminator even though it could
Dimava
Dimavaā€¢4mo ago
Why, it should
ssalbdivad
ssalbdivadā€¢4mo ago
Like in practice it would eliminate that branch of the union yes, but right now the only discriminants are two required paths that have differing literal values or typeof Because the cases I mentioned are wayyyy more common and they are also much more efficient to traverse
Dimava
Dimavaā€¢4mo ago
Never has typeof undefined?
ssalbdivad
ssalbdivadā€¢4mo ago
But it's an optiona lkey You can't use optional keys or paths including them to discriminate
Dimava
Dimavaā€¢4mo ago
Ah And you can't make it required
ssalbdivad
ssalbdivadā€¢4mo ago
Except in that special case for key presence Yeah there are several other cases where internally, we know branches are disjoint, but we don't actually transform them to discrimnated union because it's either very niche or a lot less efficient to extract or both Like I said about the ones we have now, typeof and literal values can be checked in a switch in constant time We could discriminate on ranges like type("number > 10 | number < 0") because they are disjoint, but we can't write a switch-like statement to traverse the union in constant time so we don't do it (yet) I'm not sure if it would ever be worth it to consider using range as a discriminant in practice, but key presence I definitely want to do eventually with reject
aabad_ankit
aabad_ankitOPā€¢4mo ago
@ArkDavid You were suggesting to use narrow, can I do it on union and solve my problem
ssalbdivad
ssalbdivadā€¢4mo ago
const nameArk = type({ name: "string", age: "number" }).onDeepUndeclaredKey(
"delete"
)
const cityArk = type({ city: "string" }).onDeepUndeclaredKey("delete")

const discriminants = {
name: nameArk,
city: cityArk
} as const

type Discriminants = typeof discriminants

type DiscriminatedObject = Discriminants[keyof Discriminants]["t"]

const myUnion = type.object.narrow((o, ctx): o is DiscriminatedObject => {
let k: keyof Discriminants
for (k in discriminants) {
if (k in o) {
const out = discriminants[k](o)
if (out instanceof type.errors) {
ctx.errors.merge(out)
return false
}
return true
}
}
return ctx.mustBe(
`an object including one of the following keys: ${Object.keys(discriminants).join(", ")}`
)
})
const nameArk = type({ name: "string", age: "number" }).onDeepUndeclaredKey(
"delete"
)
const cityArk = type({ city: "string" }).onDeepUndeclaredKey("delete")

const discriminants = {
name: nameArk,
city: cityArk
} as const

type Discriminants = typeof discriminants

type DiscriminatedObject = Discriminants[keyof Discriminants]["t"]

const myUnion = type.object.narrow((o, ctx): o is DiscriminatedObject => {
let k: keyof Discriminants
for (k in discriminants) {
if (k in o) {
const out = discriminants[k](o)
if (out instanceof type.errors) {
ctx.errors.merge(out)
return false
}
return true
}
}
return ctx.mustBe(
`an object including one of the following keys: ${Object.keys(discriminants).join(", ")}`
)
})
I have to focus on next release now though lol Actually wait let me improve the error message require quick Okay this should be good
Actually wait let me improve the error message require quick
See WTF is this I meant to write real quick and wrote require quick? CC @TheStockBroker
aabad_ankit
aabad_ankitOPā€¢4mo ago
Thank you so much for all the help
Want results from more Discord servers?
Add your server