-
-
Notifications
You must be signed in to change notification settings - Fork 135
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
How to check if a key of unknown type exists on an object? #222
Comments
Well right after writing that all up I found a solution, by using type guards: type Example<Value> = Bar<Value> | Foo<Value>
type Bar<Value> = { bar: Value }
type Foo<Value> = { foo: Value }
const isBar = <Value>(e: Example<Value>): e is Bar<Value> => 'bar' in e
const isFoo = <Value>(e: Example<Value>): e is Foo<Value> => 'foo' in e
const getValue = <Value>(e: Example<Value>): Value =>
match(e)
.when(isBar, ({ bar }) => bar as Value)
.when(isFoo, ({ foo }) => foo as Value)
.exhaustive() The problem is those type guards aren't type-safe. If one of the keys are mistyped, or if one of the keys in the types change, or if one of the types are updated so that there's overlap, the compiler won't notice. Not sure how to fix that. |
The issue you are facing will only come up if you are matching on an unknown type parameter ( import { match, P } from 'ts-pattern'
type Example<Value> =
| { foo: Value }
| { bar: Value }
// We instantiate `Example` with the `number` type.
// 👇
const getValue = (e: Example<number>): number => match(e)
.with({ foo: P.any }, ({ foo }) => foo) // ✅ works
.with({ bar: P.any }, ({ bar }) => bar) // ✅ works
.exhaustive() The reason why is that TypeScript's type inference gets stuck on unknown type parameter. The type-level algorithm that ts-pattern uses to narrow the input type can't complete because expressions like the fact that TS-Pattern doesn't support generic types is a known limitation, but it's really a limitation of the language unfortunately. My recommendation here is to use function signature overloads separate the public facing API of your function from the types it uses internally: function getValue<T>(e: Example<T>): T;
function getValue(e: Example<unknown>): unknown {
return match(e)
.with({ foo: P.any }, ({ foo }) => foo)
.with({ bar: P.any }, ({ bar }) => bar)
.exhaustive()
} I wrote about the reason I think this is the best option in this blog post: https://type-level-typescript.com/articles/making-generic-functions-pass-type-checking |
Thanks for the suggestion! That's a better solution than the type guards I came up with. Not sure if you prefer to close this or leave it open in case others come looking the same issue. |
Is your feature request related to a problem? Please describe.
Say I have a union of two object types holding an unknown
Value
in different keys:I'd like to be able to match on either case with something like:
This gives the following TS errors respectively, on each destructure:
I'm not sure why it thinks the matched type could be
{}
. I suppose sinceValue
is unknown it could beundefined
. I assume that's also why replacingP.any
with either ofP.not(undefined)
orP.not(P.nullish)
doesn't work either.Describe the solution you'd like
I feel we could side-step the concern of whether
Value
extendsundefined
by only checking whether the key exists, since these two are not equivalent to the compiler:If there were something like a
keyExists
pattern, the above could become:Describe alternatives you've considered
I've also tried adding a constraint on the type to make sure it's not
undefined
with:In addition to that, I've tried checking that the key isn't there with something like:
The logic being that if
foo
is optional or undefined,bar
must exist, and vice versa. Didn't work either.The text was updated successfully, but these errors were encountered: