effect icon indicating copy to clipboard operation
effect copied to clipboard

add `Predicate.hasProperties` for composing predicates for structs with multiple properties

Open skoshx opened this issue 6 months ago • 5 comments

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

skoshx avatar Jun 24 '25 15:06 skoshx

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
    })
  )
)

KhraksMamtsov avatar Jun 24 '25 16:06 KhraksMamtsov

That only works for two properties, if theres 4 or in my case 5+, it becomes really cumbersome / im not sure if even possible

skoshx avatar Jun 24 '25 18:06 skoshx

Using bare predicates is fine for basic use cases, but once things get more complex, it's better to use Schema.is.

gcanti avatar Jun 24 '25 19:06 gcanti

In my case, performance matters a lot, Schema is too slow. Either ways, this seems like an improvement, no?

skoshx avatar Jun 24 '25 20:06 skoshx

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.

gcanti avatar Jun 24 '25 21:06 gcanti