Typeclass Design Skill
Use this skill when implementing typeclasses that provide reusable abstractions across multiple types.
Pattern: Curried Typeclass Functions
All typeclass functions must be fully curried to enable partial application:
import { Duration } from 'effect';
declare interface Durable<A> {
readonly getDuration: (self: A) => Duration.Duration;
}
// Create a typeclass function that takes the typeclass instance first,
// then curries out all other parameters
export const isMoreThan =
<A>(D: Durable<A>) =>
(minimum: Duration.Duration) =>
(self: A): boolean => {
const current = D.getDuration(self);
return Duration.isGreaterThanOrEqualTo(current, minimum);
};
Pattern: Dual APIs
Provide both data-first (uncurried) and data-last (curried) variants using Function.dual:
import { Duration } from 'effect';
import * as Function from 'effect/Function';
declare interface Durable<A> {
readonly getDuration: (self: A) => Duration.Duration;
}
export const isMoreThan = <A>(D: Durable<A>) =>
Function.dual<
// Data-last (curried) - pipe-friendly
(minimum: Duration.Duration) => (self: A) => boolean,
// Data-first (uncurried) - direct call
(self: A, minimum: Duration.Duration) => boolean
>(
2, // Number of arguments for data-first form
(self: A, minimum: Duration.Duration): boolean => {
const current = D.getDuration(self);
return Duration.isGreaterThanOrEqualTo(current, minimum);
}
);
Usage Patterns
The dual API enables both styles:
import { pipe } from 'effect/Function';
import * as Duration from 'effect/Duration';
import * as Function from 'effect/Function';
declare interface Durable<A> {
readonly getDuration: (self: A) => Duration.Duration;
}
declare const isMoreThan: <A>(D: Durable<A>) => {
(minimum: Duration.Duration): (self: A) => boolean;
(self: A, minimum: Duration.Duration): boolean;
};
declare interface Appointment {
duration: Duration.Duration;
}
declare const Appointment: {
Durable: Durable<Appointment>;
};
declare const appointment: Appointment;
declare const appointments: Appointment[];
// Data-first: Direct function call
const hasMinimum = isMoreThan(Appointment.Durable)(
appointment,
Duration.hours(1)
);
// Data-last: Pipe-friendly
const hasMinimum2 = pipe(
appointment,
isMoreThan(Appointment.Durable)(Duration.hours(1))
);
// Partial application for filtering
const longAppointments = appointments.filter(
isMoreThan(Appointment.Durable)(Duration.hours(1))
);
Complete Typeclass Example
import { Duration, Order } from 'effect';
import * as Function from 'effect/Function';
/**
* Typeclass for types that have a duration.
*/
export interface Durable<A> {
readonly getDuration: (self: A) => Duration.Duration;
readonly setDuration: (self: A, duration: Duration.Duration) => A;
}
/**
* Create a Durable instance.
*/
export const make = <A>(
getDuration: (self: A) => Duration.Duration,
setDuration: (self: A, duration: Duration.Duration) => A
): Durable<A> => ({
getDuration,
setDuration
});
/**
* Check if duration is more than minimum.
*/
export const isMoreThan = <A>(D: Durable<A>) =>
Function.dual<
(minimum: Duration.Duration) => (self: A) => boolean,
(self: A, minimum: Duration.Duration) => boolean
>(2, (self: A, minimum: Duration.Duration): boolean =>
Duration.isGreaterThanOrEqualTo(D.getDuration(self), minimum)
);
/**
* Order by duration.
*/
export const OrderByDuration = <A>(D: Durable<A>): Order.Order<A> =>
Order.mapInput(Duration.Order, (self: A) => D.getDuration(self));
When to Use
- Creating reusable abstractions (Schedulable, Durable, Priceable)
- Implementing operations that work across multiple types
- Providing composable, pipe-friendly APIs
- Enabling partial application for filtering/mapping
Key Principles
- Curry everything - Enable partial application
- Dual APIs always - Support both usage styles
- Typeclass first - First parameter is always the typeclass instance
- Type lambda for HKT - Use TypeLambda pattern when needed