There are cases where unwanted patterns match.
gitsunmin opened this issue · comments
Describe the bug
There are cases where unwanted patterns match.
CASE 1
import { match } from 'ts-pattern';
const user1 = { name: 'Gitsunmin' };
const result = match(user1)
.with({}, () => true)
.otherwise(() => false);
console.log('result:', result); // true
the expected value: false;
a resultant value: true
CASE 2
import { match, P } from 'ts-pattern';
const user1 = {};
const result = match(user1)
.with({ name: P.nullish }, () => true)
.otherwise(() => false);
console.log('result:', result); // false
the expected value: true;
a resultant value: false
Versions
- TypeScript version: 5.0.2
- ts-pattern version: 5.0.8
- environment: bun 1.0.30
Both behaviors are expected. TS-Pattern follows the semantic of corresponding object types:
- The
{}
type contains every object, including{ name: string }
- The
{ name: null | undefined }
has a required propertyname
, so{}
isn't assignable to it.
See:
const x: {} = { name: 'a' } // ✅ type-checks.
const y: { name: null | undefined } = {} // ❌ doesn't type-check.
if the {}
matches all object types, then is there any way to match {}
(an empty object) exactly?
You can write custom matchers with P.when:
const emptyObject = P.when(
(value: unknown) => value && typeof value === 'object' && Object.keys(value).length === 0
)
@gvergnaud
I've read the comments you've posted, and I have a suggestion I'd like to make.
How about creating "P.empty"?
.with(P.empty, () => true)
It could be used for more versatile applications by matching when "Array", "object", "Map", "Set" are empty.
If this seems like a good idea, I will go ahead and create a pull request.
Thank you for reading!
how about P.object.empty
for a more precise expression? A P.empty could be ambiguous in some situations:
const x:Array<string> | Set<number> | Partial<SomeObject> = {}
match(x)
.with(P.object.empty, ()=>"fallback")
.otherwise(()=>"Meh")
would be great to suggest this idea on the separated issue, since this issue's topic has been closed.
I would like to reopen this to handle the following Typescript cases:
type Bar = {
type: "bar";
value: "a" | "b";
};
type Foo = {
type: "foo";
value: "x" | "z";
};
declare const foobar: Bar | Foo;
match(foobar)
.with({ type: "bar", value: "a" }, () => {})
.with({ type: "bar", value: "b" }, () => {})
.with({ type: "bar", value: "x" }, () => {}) // does not make sense
.with({ type: "bar", value: "z" }, () => {}) // does not make sense
.with({ type: "foo", value: "a" }, () => {}) // does not make sense
.with({ type: "foo", value: "b" }, () => {}) // does not make sense
.with({ type: "foo", value: "x" }, () => {})
.with({ type: "foo", value: "z" }, () => {});
This looks like due to the fact that the type KnownPattern
simply relies on the key's type used, creating an union, without having knowledge of the full pattern itself.
This case could be fixed with the following:
match(foobar)
.with({ type: "bar" }, ({ value }) =>
match(value)
.with("a", () => {})
.with("b", () => {})
.exhaustive(),
)
.with({ type: "foo" }, ({ value }) =>
match(value)
.with("x", () => {})
.with("z", () => {})
.exhaustive(),
);
But this is way more verbose than what it could be.
@nullndr I guess maybe the topic of this issue was too ambiguous. since this issue thread handles P.object.empty case and runtime behaviors, I'd rather open a new one that fits yours.
By the way, I'm not sure it's right to filter out patterns that don't make sense.
as shown below:
also here's the code for you, you should try yourself
import { match, P } from "ts-pattern";
const x: string = "hello world";
match<string,string>(x)
.with(P.number,()=>"impossible")
.otherwise(()=> "always")
the pattern P.number doesn't make sense but it's still a valid pattern, whether it matters on runtime or not.