add `Predicate.hasProperties` for composing predicates for structs with multiple properties
What is the problem this feature would solve?
Currently, we can match for simple objects like this:
const hasStringMessage = Predicate.compose(
Predicate.isRecord,
Predicate.compose(
Predicate.hasProperty("message"),
Predicate.struct({ message: Predicate.isString }),
),
);
But more complex ones are not possible, so if I wanna match for a struct like this { message: string, error: boolean } from a unknown type, it's not possible (AFAIK)
What is the feature you are proposing to solve the problem?
const PredicateHasProperties = <Keys extends Array<PropertyKey>>(keys: Keys) => (self: unknown): self is { [K in Keys[number]]: unknown } => {
return Predicate.isObject(self) && Object.keys(self).every((k) => keys.includes(k))
}
With this we could do
const hasErrorMessage = Predicate.compose(
Predicate.isRecord,
Predicate.compose(
PredicateHasProperties(["message", "error"] as const),
Predicate.struct({
message: Predicate.isString,
error: Predicate.isBoolean,
})
)
)
What alternatives have you considered?
Writing my own Predicate
we can use Predicate.and I think?
https://effect.website/play#a697fd29083f
import { Predicate } from "effect"
const hasErrorMessage = Predicate.compose(
Predicate.isRecord,
Predicate.compose(
Predicate.and(Predicate.hasProperty("message"), Predicate.hasProperty("error")),
Predicate.struct({
message: Predicate.isString,
error: Predicate.isBoolean
})
)
)
That only works for two properties, if theres 4 or in my case 5+, it becomes really cumbersome / im not sure if even possible
Using bare predicates is fine for basic use cases, but once things get more complex, it's better to use Schema.is.
In my case, performance matters a lot, Schema is too slow. Either ways, this seems like an improvement, no?
I'm not sure this is an improvement, there's quite a bit of repetition here, with the keys being written twice:
const hasErrorMessage = Predicate.compose(
Predicate.isRecord,
Predicate.compose(
PredicateHasProperties(["message", "error"] as const),
Predicate.struct({
message: Predicate.isString,
error: Predicate.isBoolean,
})
)
)
Note that at runtime this already works:
import { pipe, Predicate } from "effect"
const is = pipe(
// @ts-expect-error
Predicate.isRecord,
Predicate.compose(
Predicate.struct({
message: Predicate.isString,
error: Predicate.isBoolean
})
)
)
console.log(is({ message: "hello", error: true })) // true
console.log(is({ message: "hello" })) // false
console.log(is({ message: "hello", error: "true" })) // false
So it seems to be a type-level issue only.