Functype Library Developer
Overview
Guide for contributing to the functype TypeScript library. This skill provides architectural patterns, development workflows, and tooling for implementing new functional data structures following functype's Scala-inspired design.
When to Use This Skill
Trigger this skill when:
- •Creating new data structures or types
- •Adding methods to existing data structures
- •Implementing functional interfaces (Functor, Monad, Foldable, etc.)
- •Adding or fixing tests
- •Understanding library architecture
- •Debugging functional type implementations
- •Following the Base pattern and type class system
- •Working with the Feature Matrix
Development Workflow
Prerequisites
- •Node.js: ≥ 18.0.0
- •pnpm: 10.12.1
- •TypeScript: Strict mode enabled
Essential Commands
# Install dependencies pnpm install # Development (build with watch) pnpm dev # Before committing (MUST PASS) pnpm validate # Run tests pnpm test # Run specific test file pnpm vitest run test/specific.spec.ts # Check types without building pnpm compile
Pre-Commit Checklist
Always run before committing:
pnpm validate
This runs:
- •Format: Prettier formatting
- •Lint: ESLint checks
- •Test: All Vitest tests
- •Build: Production build
Core Architecture
Scala-Inspired Constructor Pattern
All types follow this pattern:
// Constructor function returns object with methods const option = Option(value) // Constructor option.map((x) => x + 1) // Instance methods Option.none() // Companion methods
NOT class-based:
// ❌ Don't do this
class Option<T> {
constructor(value: T) { ... }
}
// ✅ Do this
export function Option<T>(value: T | null | undefined): OptionType<T> {
return value == null ? none() : some(value)
}
Base Pattern
Use the Base function to add common functionality to all types:
import { Base } from "@/core/base"
export function Option<T>(value: T | null | undefined): OptionType<T> {
if (value == null) {
return Base("None", {
// methods here
map: <B>(f: (val: T) => B) => Option.none<B>(),
// ...
})
}
return Base("Some", {
// methods here
map: <B>(f: (val: T) => B) => Option(f(value)),
// ...
})
}
Base provides:
- •
Typeableinterface (type metadata) - •Standard
toString()method - •Consistent object structure
Type System Foundation
Base constraint:
import type { Type } from "@/functor"
// Use Type instead of any
function process<T extends Type>(value: T): void {
// ...
}
Never use any:
- •Use
unknownfor uncertain types - •Use
Typefor generic constraints - •Use proper type definitions
Functional Interfaces
Every container type should implement these when applicable:
Core interfaces (see references/architecture.md for details):
- •
Functor- map over values - •
Applicative- apply wrapped functions - •
Monad- flatMap for sequencing - •
Foldable- extract via pattern matching - •
Traversable- collection operations - •
Serializable- JSON/YAML/binary serialization
Reference the Feature Matrix:
Check docs/FUNCTYPE_FEATURE_MATRIX.md to see which interfaces each type implements and what methods are required.
Creating a New Data Structure
Step-by-Step Guide
1. Research existing patterns
# Use the Explore agent to understand the codebase # Look at Option, Either, or Try as reference implementations
2. Create module structure
mkdir -p src/mynewtype touch src/mynewtype/index.ts touch test/mynewtype.spec.ts
3. Use the template script
# Run the new-type-template script ./claude/skills/functype-developer/scripts/new-type-template.sh MyNewType
This generates:
- •Basic type structure
- •Required interface implementations
- •Test file boilerplate
4. Implement the type
Follow the constructor pattern:
// src/mynewtype/index.ts
import { Base } from "@/core/base"
import type { Functor } from "@/functor"
export type MyNewType<T> = Functor<T> & {
// Your methods here
getValue(): T
}
export function MyNew<T>(value: T): MyNewType<T> {
return Base("MyNewType", {
// Functor
map: <B>(f: (val: T) => B): MyNewType<B> => {
return MyNew(f(value))
},
// Your methods
getValue: () => value,
// Pipe for composition
pipe: () => ({
map: (f: (val: T) => any) => MyNew(value).map(f),
}),
})
}
// Companion methods
MyNew.empty = <T>() => MyNew<T>(null as any)
5. Add exports
Update src/index.ts:
export { MyNew } from "./mynewtype"
export type { MyNewType } from "./mynewtype"
Update package.json exports:
{
"exports": {
"./mynewtype": {
"import": "./dist/esm/mynewtype/index.js",
"require": "./dist/cjs/mynewtype/index.js",
"types": "./dist/types/mynewtype/index.d.ts"
}
}
}
6. Write comprehensive tests
// test/mynewtype.spec.ts
import { describe, expect, it } from "vitest"
import { MyNew } from "@/mynewtype"
describe("MyNewType", () => {
describe("Construction", () => {
it("should create from value", () => {
const value = MyNew(5)
expect(value.getValue()).toBe(5)
})
})
describe("Functor", () => {
it("should map over values", () => {
const result = MyNew(5).map((x) => x * 2)
expect(result.getValue()).toBe(10)
})
})
// More tests...
})
7. Update Feature Matrix
Add your type to docs/FUNCTYPE_FEATURE_MATRIX.md showing which interfaces it implements.
8. Validate
pnpm validate
Interface Implementation Checklist
When implementing interfaces, refer to this checklist:
Functor:
- •
map<B>(f: (value: A) => B): Functor<B>
Applicative (extends Functor):
- •
ap<B>(ff: Applicative<(value: A) => B>): Applicative<B>
Monad (extends Applicative):
- •
flatMap<B>(f: (value: A) => Monad<B>): Monad<B>
Foldable:
- •
fold<B>(onEmpty: () => B, onValue: (value: A) => B): B - •
foldLeft<B>(z: B): (op: (b: B, a: A) => B) => B - •
foldRight<B>(z: B): (op: (a: A, b: B) => B) => B
See references/architecture.md for complete interface definitions.
Testing Patterns
Test Structure
Use Vitest with describe/it pattern:
describe("TypeName", () => {
describe("Feature Group", () => {
it("should do specific thing", () => {
// Arrange
const input = createInput()
// Act
const result = performOperation(input)
// Assert
expect(result).toBe(expected)
})
})
})
Property-Based Testing
Use fast-check for property tests:
import { fc, test } from "@fast-check/vitest"
test.prop([fc.integer()])("should always return positive", (n) => {
const result = Math.abs(n)
expect(result).toBeGreaterThanOrEqual(0)
})
Edge Cases to Test
Always test:
- •Empty/null/undefined inputs
- •Type inference correctness
- •Method chaining
- •Error cases
- •Immutability (original unchanged)
Code Style Guidelines
Imports
// Type-only imports when possible
import type { Type } from "@/functor"
import { Option } from "@/option"
// Organized with simple-import-sort (automatic)
Types
// ✅ Use Type for constraints
function process<T extends Type>(value: T): void
// ✅ Prefer types over interfaces
export type MyType<T> = {
value: T
}
// ✅ Explicit type annotations
function transform<T>(input: T): MyType<T> {
return { value: input }
}
Naming
// PascalCase for types
type OptionType<T> = { ... }
// camelCase for functions/variables
const someValue = Option(5)
function mapOption<T>(opt: OptionType<T>) { ... }
// Constructor functions are PascalCase
Option(value)
Either(value)
Functional Style
// ✅ Immutability
const newList = list.append(item)
// ❌ Mutation
list.push(item)
// ✅ Pure functions
function double(x: number): number {
return x * 2
}
// ❌ Side effects
function double(x: number): number {
console.log(x) // side effect
return x * 2
}
Pattern Matching
// ✅ Use Cond for conditionals
import { Cond } from "@/cond"
Cond.start<string>()
.case(x > 10, "big")
.case(x > 5, "medium")
.otherwise("small")
// ✅ Use Match for switches
import { Match } from "@/match"
Match(status)
.case("success", () => handleSuccess())
.case("error", () => handleError())
.done()
// ❌ Avoid early returns
// Use Cond or Option instead
Common Development Tasks
Adding a Helper Method
// Add to type definition
export type MyType<T> = {
// existing methods...
// New helper
isEmpty(): boolean
}
// Implement in constructor
export function MyType<T>(value: T): MyType<T> {
return Base("MyType", {
// existing methods...
isEmpty: () => value == null,
})
}
Implementing Serialization
import { createSerializable } from "@/core/serializable"
export function MyType<T>(value: T): MyType<T> {
return Base("MyType", {
// other methods...
serialize: () =>
createSerializable({
type: "MyType",
value: value,
}),
})
}
Adding Do-Notation Support
export const MyTypeCompanion = {
Do: <T>(gen: () => Generator<MyType<any>, T, any>): MyType<T> => {
// Implementation here
// See Option or Either for reference
},
}
export const MyType = Object.assign(MyTypeConstructor, MyTypeCompanion)
Debugging Tips
TypeScript Errors
"Type instantiation is excessively deep"
- •Check for circular type references
- •Simplify nested generic types
- •Use type aliases to break up complex types
"Property 'X' does not exist on type 'Y'"
- •Ensure all interfaces are properly implemented
- •Check that types are correctly exported
- •Verify Base pattern includes all required methods
Test Failures
# Run specific test with verbose output pnpm vitest run test/mytype.spec.ts --reporter=verbose # Run tests in watch mode pnpm test:watch # Check test coverage pnpm test:coverage
Build Issues
# Clean and rebuild pnpm clean && pnpm build # Check TypeScript compilation only pnpm compile # Analyze bundle size pnpm analyze:size
Variance (covariance — <out T>)
Most functype containers are declared covariant in their type parameter(s) —
List<out A>, Either<out L, out R>, etc. When adding a new container or a
new method, follow these four rules and the variance declaration will hold:
- •Element-query / removal (
contains,indexOf,remove,has) → acceptunknown. Equality with an unrelated value is a safefalseat runtime. - •Additive (
add,prepend,concat,push,set) → widen via<B>(item: B): C<A | B>. The return union accurately reflects runtime. - •Aggregation (
reduce,reduceRight) → guard withWiden<A, B>:tsimport { type Widen, reduceWiden } from "@/typeclass" reduce: <B = A>(op: (b: Widen<A, B>, a: Widen<A, B>) => Widen<A, B>): Widen<A, B> => reduceWiden<A, Widen<A, B>>(array, op)Widen<A, B> = A extends B ? B : never. Blockslist.reduce<UnrelatedType>(...)at compile time. - •Recovery / fallback (
or,orElse,recover) → widen via<T2>(...): T | T2orC<T | T2>.
When to stay invariant: mutable cells (Ref<A>), record types with
keyof dependence (Obj<T>), equality-sensitive keys (Map<K, _>). Document
the choice in JSDoc with a brief "why".
When TS rejects <out>: the compiler names the offending method. Fix it
using the rules above and re-annotate.
Regression tests: add test/<path>/<type>-variance.spec.ts patterned after
test/either/either-variance.spec.ts (comprehensive) or test/list/list-variance.spec.ts
(minimal). Type-only assertions run in milliseconds and catch silent regressions.
Full recipe: docs/variance-guide.md.
Resources
scripts/
- •
new-type-template.sh- Generate boilerplate for new types - •
validate.sh- Run the full validation workflow
references/
- •
architecture.md- Detailed architecture and patterns - •
adding-types.md- Complete guide for adding new data structures - •
adding-methods.md- Checklist for adding methods to existing types - •
testing-patterns.md- Testing strategies and examples
External Links
- •GitHub: https://github.com/jordanburke/functype
- •Docs: https://jordanburke.github.io/functype/
- •Feature Matrix:
docs/FUNCTYPE_FEATURE_MATRIX.md - •Variance Guide:
docs/variance-guide.md