@detaditya/yamin
v0.1.0
Published
Easy yet powerful functional data type utilities
Maintainers
Readme
yamin
Easy yet powerful functional data type utilities.
Motivation
TypeScript has discriminated unions built into its type system, but building them by hand is tedious — you write the type, then the constructors, then the type guards, then the matcher, all separately and all by hand. One renamed variant and you're fixing four different spots.
yamin collapses that boilerplate into a single Union call:
- Constructors — one per variant, derived automatically from the definition.
- Type guards —
isVariantName(value)predicates generated for every variant. match— exhaustive pattern matching with an optional_catch-all, enforced at the type level.
Installation
npm install @detaditya/yamin
# or
bun add @detaditya/yaminUnion
Union creates a discriminated union from a variant definition. Each variant is either a plain null (no payload) or data<T>() (carries a typed payload).
Creating a Union
import { Union, type InferUnion } from "@detaditya/yamin";
const DataStatuses = Union(data => ({
idle: null,
loading: data<{ progress: number }>(),
done: data<{ result: string }>(),
failed: data<{ code: number; message: string }>(),
}))
type DataStatus = InferUnion<typeof DataStatuses>Constructors
Each variant key becomes a constructor on the union object. Null variants take no arguments; data variants require an object payload.
const idle = DataStatuses.idle()
const loading = DataStatuses.loading({ progress: 50 })
const done = DataStatuses.done({ result: "all good" })
const failed = DataStatuses.failed({ code: 404, message: "not found" })Type Guards
A isVariantName predicate is generated for every variant and narrows the type when used in a conditional.
const status: DataStatus = DataStatuses.loading({ progress: 50 })
if (DataStatuses.isLoading(status)) {
console.log(status.payload.progress) // TypeScript knows this is the loading variant
}
DataStatuses.isIdle(status) // false
DataStatuses.isDone(status) // false
DataStatuses.isFailed(status) // falsePattern Matching
match dispatches to the handler for the active variant. Either cover every variant (exhaustive) or cover a subset and supply a _ catch-all.
// Exhaustive — all variants handled
const message = DataStatuses.match(status, {
idle: () => "Waiting...",
loading: ({ progress }) => `Loading ${progress}%`,
done: ({ result }) => `Done: ${result}`,
failed: ({ code, message }) => `Error ${code}: ${message}`,
})
// Partial with catch-all
const label = DataStatuses.match(status, {
loading: ({ progress }) => `${progress}%`,
_: () => "Not loading",
})Inferring the Union Type
Use InferUnion to derive the instance type from a union object so you only define the shape once.
const Actions = Union(data => ({
changeName: data<{ name: string }>(),
increment: null,
decrement: null,
}))
type Action = InferUnion<typeof Actions>
// { kind: "changeName"; payload: { name: string } }
// | { kind: "increment"; payload: null }
// | { kind: "decrement"; payload: null }Real-world Example: Reducer
type State = { name: string; count: number }
const Actions = Union(data => ({
changeName: data<{ name: string }>(),
increment: null,
decrement: null,
}))
type Action = InferUnion<typeof Actions>
const reducer = (state: State, action: Action): State =>
Actions.match(action, {
changeName: ({ name }) => ({ ...state, name }),
increment: () => ({ ...state, count: state.count + 1 }),
decrement: () => ({ ...state, count: state.count - 1 }),
})Real-world Example: View Rendering
const DataStatuses = Union(data => ({
loading: null,
success: data<{ message: string }>(),
error: data<{ code: number; message: string }>(),
}))
type DataStatus = InferUnion<typeof DataStatuses>
const render = (status: DataStatus): string =>
DataStatuses.match(status, {
loading: () => "<p>Loading...</p>",
success: ({ message }) => `<p>Success: ${message}</p>`,
error: ({ code, message }) => `<p>Error ${code}: ${message}</p>`,
})Roadmap
- [ ] Additional data type utilities
License
MIT
