Skip to content
Development
Skill

/nw-fp-typescript

TypeScript language-specific patterns with fp-ts/Effect, discriminated unions, and railway-oriented error handling

From plugin
nwave
635183 skills37 agents

Claude Code

$ npx -y skills add nWave-ai/nWave --skill nw-fp-typescript --agent claude-code

SKILL.md

nw-fp-typescript.SKILL.md
name: nw-fp-typescript
agent: nw-functional-software-crafter
description: TypeScript language-specific patterns with fp-ts/Effect, discriminated unions, and railway-oriented error handling
user-invocable: false
disable-model-invocation: true

FP in TypeScript -- Functional Software Crafter Skill

Cross-references: [fp-principles](../nw-fp-principles/SKILL.md) | [fp-domain-modeling](../nw-fp-domain-modeling/SKILL.md) | [pbt-typescript](../nw-pbt-typescript/SKILL.md)

When to Choose TypeScript

  • Best for: teams already on the Node/browser ecosystem wanting FP DISCIPLINE without a new runtime | full-stack

projects sharing domain types between client and server | the most common first encounter with FP for many engineers

  • Not ideal for: teams wanting the compiler to ENFORCE purity (TS purity is a convention, `any`/`as` can always

escape it) | CPU-bound work needing a stricter type system's exhaustiveness guarantees

[STARTER] Quick Setup

mkdir order-service && cd order-service && npm init -y
npm install typescript fp-ts --save
npm install --save-dev fast-check vitest
npx tsc --init --strict

**Test runner**: Vitest or Jest. `"strict": true` in `tsconfig.json` is load-bearing -- without it, `null`/ `undefined` silently widen every type and most patterns below lose their guarantee.

[STARTER] Type System for Domain Modeling

Choice Types (Discriminated Unions)

type PaymentMethod =
  | { readonly _tag: "CreditCard"; readonly cardNumber: string; readonly expiryDate: string }
  | { readonly _tag: "BankTransfer"; readonly accountNumber: string }
  | { readonly _tag: "Cash" };

The shared `_tag` field is the discriminant: a `switch` on `_tag` narrows the union per-branch, and with `"strict": true` a missing case is a compile error via the `never` exhaustiveness check below.

Record Types and Branded Domain Wrappers

interface Customer {
  readonly customerId: CustomerId;
  readonly customerName: CustomerName;
  readonly customerEmail: EmailAddress;
}

type OrderId = number & { readonly __brand: "OrderId" };
type EmailAddress = string & { readonly __brand: "EmailAddress" };

TypeScript has no true newtype -- a branded intersection type is the idiomatic substitute: `OrderId` is structurally a `number` at runtime (zero cost) but the `__brand` phantom field stops the compiler from accepting a bare `number` where an `OrderId` is expected.

[INTERMEDIATE] Smart Constructors

import { Either, left, right } from "fp-ts/Either";

function mkEmailAddress(raw: string): Either<ValidationError, EmailAddress> {
  return raw.includes("@")
    ? right(raw as EmailAddress)
    : left({ _tag: "InvalidEmail", raw });
}

The brand cast (`as EmailAddress`) happens ONLY inside the smart constructor -- callers never cast directly, so every `EmailAddress` in scope has passed `mkEmailAddress`.

[INTERMEDIATE] Composition Style

`pipe` for Left-to-Right Composition

import { pipe } from "fp-ts/function";
import * as E from "fp-ts/Either";

const placeOrder = (raw: RawOrder): Either<OrderError, Confirmation> =>
  pipe(raw, validateOrder, E.chain(priceOrder), E.chain(confirmOrder));

`E.chain` is `Either`'s `bind` -- short-circuits on `Left`, threads the `Right` value forward.

Effect (the newer alternative to fp-ts)

import { Effect } from "effect";

const placeOrder = (raw: RawOrder): Effect.Effect<Confirmation, OrderError> =>
  Effect.gen(function* () {
    const validated = yield* validateOrder(raw);
    const priced = yield* priceOrder(validated);
    return yield* confirmOrder(priced);
  });

Generator-based `Effect.gen` reads imperatively while remaining a pure DESCRIPTION of the computation until `Effect.runPromise`/`runSync` actually executes it -- the same "build a value, run it at the edge" discipline Haskell's `IO` enforces at the type level.

Accumulating Validation Errors

import { validate } from "fp-ts/Apply";

const validateCustomer = (raw: RawCustomer) =>
  pipe(
    { name: validateName(raw.name), email: validateEmail(raw.email) },
    ({ name, email }) => E.Do.pipe(E.apS("name", name), E.apS("email", email)),
  );

`apS`/Applicative validation collects ALL failures via `Either`'s `Semigroup` on the error side, unlike `chain` which stops at the first.

[INTERMEDIATE] Effect Management

Plain TypeScript has no purity enforcement -- `Effect`'s own `Effect<A, E, R>` type is the closest analogue to Haskell's `IO`: a VALUE describing a computation, not the computation itself, so a function returning `Effect<Order, never, never>` is provably side-effect-free by its type alone (no ambient `R` requirement, no `E` to fail with) in a way a bare `Order` return never was.

// Pure domain logic -- ordinary function, no Effect wrapper needed
const calculateDiscount = (order: Order): Discount =>
  order.lines.length > 10 ? 0.1 : 0.0;

// Effectful boundary -- Effect<A, E, R> makes the DB dependency explicit in R
const saveOrder = (order: Order): Effect.Effect<void, DbError, OrderRepository> =>
  Effect.gen(function* () {
    const repo = yield* OrderRepository;
    yield* repo.save(order);
  });

[ADVANCED] Ports as Interfaces, Adapters as Layers (Hexagonal Architecture)

class OrderRepository extends Context.Tag("OrderRepository")<
  OrderRepository,
  { readonly findOrder: (id: OrderId) => Effect.Effect<Option<Order>>; readonly save: (o: Order) => Effect.Effect<void> }
>() {}

const PostgresOrderRepository = Layer.succeed(OrderRepository, {
  findOrder: (id) => Effect.tryPromise(() => db.query("SELECT * FROM orders WHERE id = $1", [id])),
  save: (order) => Effect.tryPromise(() => db.insert("orders", order)),
});

`Context.Tag` is the port; `Layer` provides the adapter. Tests substitute a `Layer.succeed` backed by an in-memory map, with no framework or DI container needed.

[INTERMEDIATE] Testing

**Fr

Read more
@nwave-ai
Ships withnwave

nWave supplies specialist roles and a host-led delivery workflow for software teams.

635183 skills37 agents
Get the whole plugin

Stars on GitHub

Momentum

stars in total
635
Star history of nWave-ai/nWave on GitHub, a chart by star-history.com

Other skills on nwave.

nw-ab-agent-template

nw-ab-agent-template

KNOWLEDGE — the canonical agent-spec template (frontmatter + body skeleton). Reference loaded by the create/migrate procedures;…

@nwave-ai@nwave-aiInstall Skill
nw-ab-anti-patterns

nw-ab-anti-patterns

KNOWLEDGE — agent/skill/command anti-pattern catalog with fixes. Reference scanned by validate-spec; no sequence.

@nwave-ai@nwave-aiInstall Skill
nw-ab-create-agent

nw-ab-create-agent

PROCEDURE — create a NEW agent via the 5-phase workflow (ANALYZE→DESIGN→CREATE→VALIDATE→REFINE). Trigger: build a new AI agent.…

@nwave-ai@nwave-aiInstall Skill
nw-ab-examples

nw-ab-examples

KNOWLEDGE — canonical worked examples for agent creation, migration, and command optimization. Reference for the relevant…

@nwave-ai@nwave-aiInstall Skill
nw-ab-house-style

nw-ab-house-style

KNOWLEDGE — caveman-native authoring house style + by-construction guarantees (Reasoning Mandate injection, measured-gain…

@nwave-ai@nwave-aiInstall Skill

More skills from other plugins.

brainstorming
Auto-invoked

brainstorming

You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…

@obra@obraInstall Skill
ask-matt

ask-matt

Ask which skill or flow fits your situation. A router over the skills in this repo.

skills13d309k
@mattpocock@mattpocockInstall Skill
accessibility
Auto-invoked

accessibility

Design, implement, and audit accessible UI to WCAG 2.2 Level AA across Web, iOS, and Android — semantic ARIA roles and labels,…

ecc2w293k
@affaan-m@affaan-mInstall Skill
compiler-commit

compiler-commit

Use when you want to verify compiler changes and commit with the correct convention. Runs tests, lint, and format, then commits…

react3d252k
@facebook@facebookInstall Skill

Questions about nw-fp-typescript.

Which coding agents can use nw-fp-typescript?
nw-fp-typescript installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.