| name | functype-developer |
| description | Assist contributors working on the functype library codebase. Use this skill when creating new data structures, implementing functional interfaces (Functor, Monad, Foldable), adding tests, debugging library internals, or understanding functype's Scala-inspired architecture patterns including the Base pattern, HKT system, and Companion utilities. |
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
pnpm install
pnpm dev
pnpm validate
pnpm test
pnpm vitest run test/specific.spec.ts
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:
const option = Option(value)
option.map((x) => x + 1)
Option.none()
NOT class-based:
class Option<T> {
constructor(value: T) { ... }
}
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", {
map: <B>(f: (val: T) => B) => Option.none<B>(),
})
}
return Base("Some", {
map: <B>(f: (val: T) => B) => Option(f(value)),
})
}
Base provides:
Typeable interface (type metadata)
- Standard
toString() method
- Consistent object structure
Type System Foundation
Base constraint:
import type { Type } from "@/functor"
function process<T extends Type>(value: T): void {
}
Never use any:
- Use
unknown for uncertain types
- Use
Type for 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
2. Create module structure
mkdir -p src/mynewtype
touch src/mynewtype/index.ts
touch test/mynewtype.spec.ts
3. Use the 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:
import { Base } from "@/core/base"
import type { Functor } from "@/functor"
export type MyNewType<T> = Functor<T> & {
getValue(): T
}
export function MyNew<T>(value: T): MyNewType<T> {
return Base("MyNewType", {
map: <B>(f: (val: T) => B): MyNewType<B> => {
return MyNew(f(value))
},
getValue: () => value,
pipe: () => ({
map: (f: (val: T) => any) => MyNew(value).map(f),
}),
})
}
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
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)
})
})
})
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:
Applicative (extends Functor):
Monad (extends Applicative):
Foldable:
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", () => {
const input = createInput()
const result = performOperation(input)
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
import type { Type } from "@/functor"
import { Option } from "@/option"
Types
function process<T extends Type>(value: T): void
export type MyType<T> = {
value: T
}
function transform<T>(input: T): MyType<T> {
return { value: input }
}
Naming
type OptionType<T> = { ... }
const someValue = Option(5)
function mapOption<T>(opt: OptionType<T>) { ... }
Option(value)
Either(value)
Functional Style
const newList = list.append(item)
list.push(item)
function double(x: number): number {
return x * 2
}
function double(x: number): number {
console.log(x)
return x * 2
}
Pattern Matching
import { Cond } from "@/cond"
Cond.start<string>()
.case(x > 10, "big")
.case(x > 5, "medium")
.otherwise("small")
import { Match } from "@/match"
Match(status)
.case("success", () => handleSuccess())
.case("error", () => handleError())
.done()
Common Development Tasks
Adding a Helper Method
export type MyType<T> = {
isEmpty(): boolean
}
export function MyType<T>(value: T): MyType<T> {
return Base("MyType", {
isEmpty: () => value == null,
})
}
Implementing Serialization
import { createSerializable } from "@/core/serializable"
export function MyType<T>(value: T): MyType<T> {
return Base("MyType", {
serialize: () =>
createSerializable({
type: "MyType",
value: value,
}),
})
}
Adding Do-Notation Support
export const MyTypeCompanion = {
Do: <T>(gen: () => Generator<MyType<any>, T, any>): MyType<T> => {
},
}
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
pnpm vitest run test/mytype.spec.ts --reporter=verbose
pnpm test:watch
pnpm test:coverage
Build Issues
pnpm clean && pnpm build
pnpm compile
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) → accept
unknown. Equality with an unrelated value is a safe false at 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 with Widen<A, B>:
import { 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. Blocks list.reduce<UnrelatedType>(...)
at compile time.
- Recovery / fallback (
or, orElse, recover) → widen via
<T2>(...): T | T2 or C<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