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.
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.
Trigger this skill when:
# 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
Always run before committing:
pnpm validate
This runs:
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)
}
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:
Typeable interface (type metadata)toString() methodBase constraint:
import type { Type } from "@/functor"
// Use Type instead of any
function process<T extends Type>(value: T): void {
// ...
}
Never use any:
unknown for uncertain typesType for generic constraintsEvery container type should implement these when applicable:
Core interfaces (see references/architecture.md for details):
Functor - map over valuesApplicative - apply wrapped functionsMonad - flatMap for sequencingFoldable - extract via pattern matchingTraversable - collection operationsSerializable - JSON/YAML/binary serializationReference the Feature Matrix:
Check docs/FUNCTYPE_FEATURE_MATRIX.md to see which interfaces each type implements and what methods are required.
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:
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
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): BfoldLeft<B>(z: B): (op: (b: B, a: A) => B) => BfoldRight<B>(z: B): (op: (a: A, b: B) => B) => BSee references/architecture.md for complete interface definitions.
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)
})
})
})
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)
})
Always test:
// Type-only imports when possible
import type { Type } from "@/functor"
import { Option } from "@/option"
// Organized with simple-import-sort (automatic)
// ✅ 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 }
}
// 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)
// ✅ 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
}
// ✅ 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
// 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,
})
}
import { createSerializable } from "@/core/serializable"
export function MyType<T>(value: T): MyType<T> {
return Base("MyType", {
// other methods...
serialize: () =>
createSerializable({
type: "MyType",
value: value,
}),
})
}
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)
"Type instantiation is excessively deep"
"Property 'X' does not exist on type 'Y'"
# 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
# Clean and rebuild
pnpm clean && pnpm build
# Check TypeScript compilation only
pnpm compile
# Analyze bundle size
pnpm analyze:size
<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:
contains, indexOf, remove, has) → accept
unknown. Equality with an unrelated value is a safe false at runtime.add, prepend, concat, push, set) → widen via
<B>(item: B): C<A | B>. The return union accurately reflects runtime.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.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.
new-type-template.sh - Generate boilerplate for new typesvalidate.sh - Run the full validation workflowarchitecture.md - Detailed architecture and patternsadding-types.md - Complete guide for adding new data structuresadding-methods.md - Checklist for adding methods to existing typestesting-patterns.md - Testing strategies and examplesdocs/FUNCTYPE_FEATURE_MATRIX.mddocs/variance-guide.mdSearch for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer