Interface Eq<T>

An interface that provides evidence of an equivalence relation.

Remarks

Properties

Implementors of Eq must implement an equality comparison that is:

  • reflexive: eq(x, x);
  • symmetric: eq(x, y) implies eq(y, x); and
  • transitive: eq(x, y) and eq(y, z) implies eq(x, z)

for all values x, y, and z.

Implementing Eq

Eq requires an implementation for [Eq.eq].

The most common implementation strategies are writing classes and patching existing prototypes. Implementation is implicit and does not require an implements clause. TypeScript uses structural subtyping to determine whether a value implements Eq.

Conditional implementation

Working with generic types requires additional consideration: in some cases, a generic type implements Eq only when one or more of its generic parameters implement Eq; in these cases, we must require an Eq implementation from the parameter(s). In other cases, there are no such requirements.

Writing classes

Classes and objects can implement Eq. This strategy works best for types that:

  • are already modeled using classes or objects.
  • provide direct access to their implementation.
  • have a single, specific behavior as an equivalence relation.

Additionally, classes can easily wrap existing types to provide a variety of Eq implementations. These "helper" classes are useful for types that:

  • have more than one behavior as an equivalence relation, or already have a default implementation for Eq but can have alternative implementations.
  • do not provide access to their implementation, and where patching the implementation is undesireable.

Patching existing prototypes

Existing types can be patched to implement Eq. This strategy works well for types that:

  • are built-in or imported from external modules.
  • do not provide access to their implementation.
  • have a single, specific behavior as an equivalence relation, or where the programmer wishes to implement a default behavior.

Patching a type in TypeScript requires two steps:

  1. an augmentation for a module or the global scope that patches the type-level representation; and
  2. a concrete implementation for [Eq.eq].

The concrete implementation logic is similar to writing a method body for a class or object, and the same practices apply for requiring generic parameters to implement Eq.

Example

Non-generic implementation

Consider a Book type that determines equality by comparing ISBNs:

import { Eq } from "@neotype/prelude/cmp.js";

enum BookFormat { HARD_BACK, PAPER_BACK, DIGITAL }

class Book {
constructor(readonly isbn: number, readonly fmt: BookFormat) {}

[Eq.eq](that: Book): boolean {
return this.isbn === that.isbn;
}
}

If desired, we can also require the same book format for two books to be considered equal:

import { Eq } from "@neotype/prelude/cmp.js";

enum BookFormat { HARD_BACK, PAPER_BACK, DIGITAL }

class Book {
constructor(readonly isbn: number, readonly fmt: BookFormat) {}

[Eq.eq](that: Book): boolean {
return this.isbn === that.isbn && this.fmt === that.fmt;
}
}

Example

Generic implementation with no Eq requirements

Consider a type that determines equality by comparing the lengths of arrays:

import { Eq } from "@neotype/prelude/cmp.js";

class Len<out T> {
constructor(readonly val: T[]) {}

[Eq.eq](that: Len<T>): boolean {
return this.val.length === that.val.length;
}
}

Notice how Len is generic, but there are no special requirements for implementing [Eq.eq].

Example

Generic implementation with an Eq requirement

Consider a type that determines equality for arrays by comparing their elements lexicographically, which requires that the elements implement Eq:

import { Eq, ieq } from "@neotype/prelude/cmp.js";

class Arr<out T> {
constructor(readonly val: T[]) {}

[Eq.eq]<T extends Eq<T>>(this: Arr<T>, that: Arr<T>): boolean {
return ieq(this.val, that.val);
}
}

Notice the extra syntax when implementing [Eq.eq]. We introduce a method-scoped generic parameter T and require that it has an Eq implementation by writing T extends Eq<T> (the name T is arbitrary).

Then, we require that this and that are Arr<T> where T extends Eq<T>. This allows us to use ieq to implement our desired behavior.

Example

Generic implementation with multiple Eq requirements

Consider a Pair type that determines equality for two distinct values, which requires that each value has a distinct implementation for Eq:

import { Eq, eq } from "@neotype/prelude/cmp.js";

class Pair<out A, out B> {
constructor(readonly fst: A, readonly snd: B) {}

[Eq.eq]<A extends Eq<A>, B extends Eq<B>>(
this: Pair<A, B>,
that: Pair<A, B>,
): boolean {
return eq(this.fst, that.fst) && eq(this.snd, that.snd);
}
}

The syntax is similar to the Arr implementation above. Notice there are now two method-scoped generic parameters that are each required to implement Eq.

Example

Non-generic augmentation

Consider a module augmentation for an externally defined Book type:

import { Eq } from "@neotype/prelude/cmp.js";
import { Book } from "path_to/book.js";

declare module "path_to/book.js" {
interface Book {
[Eq.eq](that: Book): boolean
}
}

Book.prototype[Eq.eq] = function (that: Book): boolean {
return this.isbn === that.isbn;
};

Example

Generic augmentation

Consider a global augmentation for the Array prototype:

import { Eq, ieq } from "@neotype/prelude/cmp.js";

declare global {
interface Array<T> {
[Eq.eq]<T extends Eq<T>>(this: T[], that: T[]): boolean
}
}

Array.prototype[Eq.eq] = function <T extends Eq<T>>(
this: T[],
that: T[],
): boolean {
return ieq(this, that);
};

Type Parameters

  • in T

Hierarchy

Methods

Methods

  • Test whether this and that Eq are equal.

    Parameters

    • that: T

    Returns boolean

Generated using TypeDoc