Struct.ts overview
Extension to the effect/Struct module providing prototype-safe struct merging, field overriding, one-key construction, derived-field enrichment, and a refined evolve.
Mental model
- This module must be used for objects whose property names are known. When only the property types are known, use
MRecord. - Compared to the spread operator, the helpers here intentionally do not copy the prototype: the result is always a fresh
Data-shaped object. This makes the loss of prototype explicit instead of hidden behind syntax sugar. - Mutating variants (
mutableSet,mutableEnrichWith) write in place and should be reserved for hot paths where allocation cost is unacceptable.
Common tasks
- Merge / append:
prepend,append - Override existing fields:
set,mutableSet - Construct:
make - Add derived fields:
enrichWith,mutableEnrichWith - Transform fields:
evolve
Quickstart
Example (Append a field then derive another from self)
import { pipe } from "effect"
import * as MStruct from "@parischap/effect-lib/MStruct"
const built = pipe(
{ firstName: "Ada" } as const,
MStruct.append({ lastName: "Lovelace" } as const),
MStruct.enrichWith({ fullName: ({ firstName, lastName }) => `${firstName} ${lastName}` })
)
console.log(built) // { firstName: 'Ada', lastName: 'Lovelace', fullName: 'Ada Lovelace' }
Exports Grouped by Category
Constructors
make
Builds a single-field struct { [key]: value }.
Example (Single-key constructor)
import { pipe } from "effect"
import * as MStruct from "@parischap/effect-lib/MStruct"
console.log(pipe(42, MStruct.make("answer"))) // { answer: 42 }
Signature
declare const make: <K extends PropertyKey>(key: K) => <V>(value: V) => { readonly [key in K]: V }
Models
Type (type alias)
Type of a struct. Ideally, we would have chosen MTypes.Object. But it does not cover class instances on which we want this module to operate
Signature
type Type = MTypes.NonPrimitive
Utility types
Append (type alias)
Utility type computing the result of { ...first, ...second }.
- When a field exists in both
FirstandSecond, the resulting type follows JavaScript’s spread semantics:Second’s value wins, except when it is optional, in which case the result is the union ofSecond[k](withoutundefined) andFirst[k].
Signature
type Append<First extends Type, Second extends Type> = {
readonly [k in keyof First | keyof Second]: k extends keyof Second
? k extends keyof First
? // Is Second[k] not optional
Extract<Second[k], undefined> extends never
? Second[k]
: Exclude<Second[k], undefined> | First[k]
: Second[k]
: First[k]
}
Utils
append
Builds { ...self, ...that } — fields in that override fields in self.
- Use instead of the spread operator when you want to make explicit that the prototype of the left-hand side is dropped.
- Same as
effect/Struct.assignbut handles optional values more cleanly.
Example (New fields override)
import { pipe } from "effect"
import * as MStruct from "@parischap/effect-lib/MStruct"
console.log(pipe({ a: 1, b: 2 } as const, MStruct.append({ b: 3, c: 4 } as const)))
// { a: 1, b: 3, c: 4 }
See
prepend— symmetric variant where existing fields win
Signature
declare const append: <O1 extends Type>(that: O1) => <O extends Type>(self: O) => MTypes.Data<Append<O, O1>>
enrichWith
Computes a record of new fields where each value is f(self) for the corresponding key, then appends them to self.
- Use when several derived fields all depend on the same source struct.
- Existing fields with the same key are overwritten.
Example (Adding derived fields)
import { pipe } from "effect"
import * as MStruct from "@parischap/effect-lib/MStruct"
console.log(
pipe(
{ firstName: "Ada", lastName: "Lovelace" } as const,
MStruct.enrichWith({
fullName: ({ firstName, lastName }) => `${firstName} ${lastName}`,
initials: ({ firstName, lastName }) => `${firstName[0]}.${lastName[0]}.`
})
)
)
// { firstName: 'Ada', lastName: 'Lovelace', fullName: 'Ada Lovelace', initials: 'A.L.' }
See
mutableEnrichWith— in-place variant
Signature
declare const enrichWith: <O extends Type, O1 extends { [x: PropertyKey]: MTypes.OneArgFunction<O, unknown> }>(
fields: O1
) => (self: O) => MTypes.Data<Omit<O, keyof O1> & { readonly [key in keyof O1]: ReturnType<O1[key]> }>
evolve
Same as Struct.evolve but the return type drops any property carried by the prototype, since those are not copied by spreading. Transformations whose key is missing in self are ignored.
Example (Evolving selected fields)
import { pipe } from "effect"
import * as MStruct from "@parischap/effect-lib/MStruct"
console.log(
pipe({ firstName: "ada", lastName: "lovelace" } as const, MStruct.evolve({ firstName: (s) => s.toUpperCase() }))
)
// { firstName: 'ADA', lastName: 'lovelace' }
Signature
declare const evolve: {
<S extends Type, E extends Evolver<S>>(e: E): (self: S) => MTypes.Data<Evolved<S, E>>
<S extends Type, E extends Evolver<S>>(self: S, e: E): MTypes.Data<Evolved<S, E>>
}
mutableEnrichWith
Same as enrichWith but writes the new fields into self in place using Object.assign.
- Use only when allocation cost matters and
selfis not shared.
Signature
declare const mutableEnrichWith: <
O extends MTypes.NonPrimitive,
O1 extends { [x: PropertyKey]: MTypes.OneArgFunction<O, unknown> }
>(
fields: O1
) => (self: O) => Omit<O, keyof O1> & { readonly [key in keyof O1]: ReturnType<O1[key]> }
mutableSet
Same as set but writes the new fields into self in place using Object.assign.
- Use only when allocation cost matters and
selfis not shared.
Signature
declare const mutableSet: <O extends Type, O1 extends Partial<O>>(that: O1) => (self: O) => Omit<O, keyof O1> & O1
prepend
Builds { ...that, ...self } — fields in self override fields in that.
- Use instead of the spread operator when you want to make explicit that the prototype of the left-hand side is dropped.
- Same as
effect/Struct.assignbut handles optional values more cleanly.
Example (Self overrides defaults)
import { pipe } from "effect"
import * as MStruct from "@parischap/effect-lib/MStruct"
const defaults = { theme: "light", font: "serif" } as const
console.log(pipe({ font: "mono" } as const, MStruct.prepend(defaults)))
// { theme: 'light', font: 'mono' }
See
append— symmetric variant where the new fields override
Signature
declare const prepend: <O1 extends Type>(that: O1) => <O extends Type>(self: O) => MTypes.Data<Append<O1, O>>
set
Like append but that may only override fields that already exist in self. New keys are rejected at the type level.
Example (Override only declared fields)
import { pipe } from "effect"
import * as MStruct from "@parischap/effect-lib/MStruct"
type User = { readonly name: string; readonly age: number }
const user: User = { name: "Ada", age: 30 }
console.log(pipe(user, MStruct.set({ age: 31 }))) // { name: 'Ada', age: 31 }
See
mutableSet— in-place variant
Signature
declare const set: <O extends Type, O1 extends Partial<O>>(that: O1) => (self: O) => MTypes.Data<Omit<O, keyof O1> & O1>