A
arktype•2w ago
francis

New v2.1 arktype matcher: is there a way to match on string literal values?

An example from the docs:
type Data =
| {
id: 1
oneValue: number
}
| {
id: 2
twoValue: string
}
const discriminateValue = match
// .in allows you to specify the input TypeScript allows for your matcher
.in<Data>()
// .at allows you to specify a key at which your input will be matched
.at("id")
.match({
1: o => `${o.oneValue}!`,
2: o => o.twoValue.length,
default: "assert"
})
type Data =
| {
id: 1
oneValue: number
}
| {
id: 2
twoValue: string
}
const discriminateValue = match
// .in allows you to specify the input TypeScript allows for your matcher
.in<Data>()
// .at allows you to specify a key at which your input will be matched
.at("id")
.match({
1: o => `${o.oneValue}!`,
2: o => o.twoValue.length,
default: "assert"
})
If I try to switch this to a typical string-value based discriminated union, I get an error:
type Data =
| {
id: "one";
oneValue: number;
}
| {
id: "two";
twoValue: string;
};
const discriminateValue = match
// .in allows you to specify the input TypeScript allows for your matcher
.in<Data>()
// .at allows you to specify a key at which your input will be matched
.at("id")
.match({
one: (o) => `${o.oneValue}!`,
two: (o) => o.twoValue.length,
default: "assert",
});
type Data =
| {
id: "one";
oneValue: number;
}
| {
id: "two";
twoValue: string;
};
const discriminateValue = match
// .in allows you to specify the input TypeScript allows for your matcher
.in<Data>()
// .at allows you to specify a key at which your input will be matched
.at("id")
.match({
one: (o) => `${o.oneValue}!`,
two: (o) => o.twoValue.length,
default: "assert",
});
'one is unresolvable' and 'two is unresolvable'
29 Replies
ssalbdivad
ssalbdivad•2w ago
All the syntax is the same as with a normal definition so you'd need to surround the keys with quotes like "'one'"
francis
francisOP•2w ago
figured it out yep!@ haha, I just realized that myself. It's been a while since I wrote arktypes, have been extremely deep in the Effect ecosystem for a while because that's what we use for all of our services and dependency management
ssalbdivad
ssalbdivad•2w ago
Makes sense and it is definitely a change anyways to have a type parsed in a key rather than a value. Because the extra quotes are a bit of a pain here I may add a util at some point that just allows you to match against a block of string literals directly
francis
francisOP•2w ago
follow-up unrelated question: is there something similar to effect's Match.exhaustive that I can use to assert that the matcher is exhaustive?
francis
francisOP•2w ago
Effect's match has that "block of string literals" utility, btw, not sure if you have seen:
Match.when(
Match.is("keyOne", "keyTwo"),
() => "case_one_val",
),
Match.when(
"keyThree",
() => "case_two_val",
),
Match.when(
Match.is("keyOne", "keyTwo"),
() => "case_one_val",
),
Match.when(
"keyThree",
() => "case_two_val",
),
ssalbdivad
ssalbdivad•2w ago
Pretty easy to do a lot with the .case API, but I also want to optimize the record API to be as clean and useful as possibler most of the time because it is so concise compared to chaining
francis
francisOP•2w ago
another matching question: is there something similar to Effect's Match.withReturnType<T>() that allows me to constrain in an arktype Match the allowable return values for pattern expressions? e.g.
const discriminateValue = match
// .in allows you to specify the input TypeScript allows for your matcher
.in<Data>()
.out<string>() // <-- something like this
// .at allows you to specify a key at which your input will be matched
.at("id")
.match({
"'one'": (o) => `${o.oneValue}!`,
"'two'": (o) => o.twoValue,
default: "assert",
});
const discriminateValue = match
// .in allows you to specify the input TypeScript allows for your matcher
.in<Data>()
.out<string>() // <-- something like this
// .at allows you to specify a key at which your input will be matched
.at("id")
.match({
"'one'": (o) => `${o.oneValue}!`,
"'two'": (o) => o.twoValue,
default: "assert",
});
ssalbdivad
ssalbdivad•2w ago
The part I worry about a bit with exhaustiveness that would be unintuitive is stuff like whether number > 0 | number <= 0 should be exhaustive relative to number. If you really want to be thorough and accurate about that stuff it might be unintuitive for most people? No way to do that yet, a good suggestion that would be easy to add though!
francis
francisOP•2w ago
yeah, it unfortunately gets complicated with type expressions like that ... I am mostly only using it for matching on a discriminated union type, where exhaustiveness checking is easy
ssalbdivad
ssalbdivad•2w ago
Yeah, I just wonder how other matchers handle that stuff without it being unsound ArkType is very strict about that stuff not being hand-wavy so even if it would be easy to introduce simple shape checks for exhaustiveness, I want to make sure the solution is robust enough that it works well and hopefully intuitively across all constraints
ssalbdivad
ssalbdivad•2w ago
Created this for the few we discussed that I definitely want to implement https://github.com/arktypeio/arktype/issues/1334
GitHub
Additional match utilities · Issue #1334 · arktypeio/arktype
opt-in runtime exhaustiveness check (maybe default: "exhaustive"?) utility for matching against a record of string literals .out for constraining return type
francis
francisOP•2w ago
thank you! if it would be possible to add simple compile-time checks for exhaustiveness backed up by a runtime check, that would be amazing. (the compile time checks wouldn't be complete, but if they catch the obvious stuff, that would be helpful for development rather than needing you to run the code to see you have an obvious match case missing, e.g. for the simple discriminated union case)
ssalbdivad
ssalbdivad•2w ago
It's not possible to do soundly unfortunately without a huge amount of additional type complexity Well let me see Ehh Ok I mean I dunno
francis
francisOP•2w ago
I'm suggesting an explicitly not sound compile-time check, such that it compiling may not mean it is exhaustive at runtime, but it being not exhaustive at compile time definitely means it is obviously wrong I dunno if that is a confusing DX though
ssalbdivad
ssalbdivad•2w ago
Basically if the value we're using for the case is a unit type like a string literal that can't be narrowed it should be safe, but yeah in the general case no without inferring subtypes (which as mentioned is a ton of extra overhead I don't want) Maybe you're right and this does work That's essentially how parsing already is right? We try to give as much as we can statically and then the rest like nubmer > 0 & number < 0 is a runtime error
francis
francisOP•2w ago
ah gotcha. or for e.g. number, once you use subtypes such as number > 0, you are opting out of the compile time check, but for simple cases with simple types and no subtypes it could work exactly 🙂
ssalbdivad
ssalbdivad•2w ago
Ahhh right but I remember now the issue I was having The final exhaustiveness check heuristic would still work, but the sequential narrowing which would be really nice can't work, because applying a heuristic to that would mean you could need to map branches that have beeen incorrectly excluded
jack
jack•5d ago
can you do a suffix/prefix match without doing regex?
const ImageSource = match({
"string.url'=s96-c'": (url) =>
url.slice(0, url.length - constants.googleThumbnailSuffix.length),
"string.url": (url) => url,
default: "assert",
});
const ImageSource = match({
"string.url'=s96-c'": (url) =>
url.slice(0, url.length - constants.googleThumbnailSuffix.length),
"string.url": (url) => url,
default: "assert",
});
this doesn't work for the first key. basically trying to do like ['string${variableDefinedOutsideOfStringLiteral}']: url => slice(...) where inside the [], is backticks. was able to get it done with union and regex, but wondering the most "arktype-y" way to do it
ssalbdivad
ssalbdivad•5d ago
You can't do it without a regex as we don't support template literal types yet (not 100% we will as if we can support type-safe regex that will generally be more powerful). Not sure why you would need a union though Unfortunately inferring keys within [] is a limitation in TS So that may be causing part of your problem
jack
jack•5d ago
this is what i've got working
const ImageSource = match({
"string.url & /=s96-c$/": (url) =>
url.slice(0, url.length - constants.googleThumbnailSuffix.length),
"string.url": (url) => url,
default: "assert",
});
const ImageSource = match({
"string.url & /=s96-c$/": (url) =>
url.slice(0, url.length - constants.googleThumbnailSuffix.length),
"string.url": (url) => url,
default: "assert",
});
just unfortunate part is i can't embed my constant value, so if it changes i don't get that for free here
ssalbdivad
ssalbdivad•5d ago
This looks like it works:
const constants = {
googleThumbnailSuffix: `s96-c$`
} as const

const googleThumbnailSuffixMatcher =
`string.url & /${constants.googleThumbnailSuffix}$/` as const

const ImageSource = type.match({
[googleThumbnailSuffixMatcher]: url =>
url.slice(0, url.length - constants.googleThumbnailSuffix.length),
"string.url": url => url,
default: "assert"
})
const constants = {
googleThumbnailSuffix: `s96-c$`
} as const

const googleThumbnailSuffixMatcher =
`string.url & /${constants.googleThumbnailSuffix}$/` as const

const ImageSource = type.match({
[googleThumbnailSuffixMatcher]: url =>
url.slice(0, url.length - constants.googleThumbnailSuffix.length),
"string.url": url => url,
default: "assert"
})
I think just not being able to compute the value inline is a TS limitation, IIRC @Andarist has worked on some improvements for dynamic keys like that You could write a fancy mapper that would iterate over your object and map all of its k/v pairs to this format if you have lots of url endings or something
jack
jack•5d ago
ah i didn't think about pulling out the matcher. this looks good to me, thank you!! i don't really need anything more fancy, just didn't want to hardcode the suffix value
ssalbdivad
ssalbdivad•5d ago
Cool, I was just imagining if you had e.g. 20 other suffixes like that for different domains you'd probably want some pattern for converting them all
jack
jack•5d ago
yea that is true. will note down potentially for the future (side note: this match function is really cool, and making me rethink what runtime validation actually means a bit. didn't consider ever using zod for something like this when i've used zod in the past instead of arktype, but in hindsight it totally makes sense)
ssalbdivad
ssalbdivad•5d ago
It feels really different with how expressive type-syntax can be Mostly to get people to try ArkType now I have to position it as something that directly parallels an existing library like Zod. In reality, it handles validation like Zod, but because of the definition syntax and introspectable type system it can also do lots of other things I'm hoping other library authors especially will catch onto
Andarist
Andarist•4d ago
those computed properties are cached all over the place internally :v and it's hard-ish (without major~ changes to the code) to check them contextually somehow so I gave up on it, given I'm not sure if they would take that change and the time I'd have to wait for a review etc maybe there is already issue about it somewhere? with a stronger signal that they could be interested in it I could revive that work at some point i guess the problem could be that resolving types within such computed properties could lead to new circularities but it's already a possibility in other scenarios so it's probably fine
ssalbdivad
ssalbdivad•4d ago
Re: stuff that would let me do hacky match stuff with keys, do you think TS would ever consider allowing objects with a narrowed toString() type or something to have a narrowed interpolation? Maybe that is too niche for the team to care haha
Andarist
Andarist•4d ago
i'd say it's really niche ;p

Did you find this page helpful?