Skip to content
Back to skills

Functional Javascript

ASecurity

Use when applying functional design in plain JavaScript, without a type checker, including immutability, tagged unions, and Result-style error handling.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 21, 2026
ai-agentsjavascripttypescriptjavanodeexpress

Security analysis

A100/100

Pro scans all 4 files and shows the line behind each finding

Scanned October 21, 2026

npx -y skills add yutna/functional-design-skills --skill functional-javascript --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Functional Javascript?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Functional Javascript
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/yutna-functional-javascript/badge)](https://www.skillsdirectory.com/skills/yutna-functional-javascript)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: functional-javascript
description: Use when applying functional design in plain JavaScript, without a type checker, including immutability, tagged unions, and Result-style error handling.
license: MIT
metadata:
  pack: functional-design-skills
  version: 3.3.0
---

# Functional JavaScript

## Overview

JavaScript has first-class functions, closures, and object literals,
which is everything the core skills need. What it lacks is a compiler to
enforce the designs, so the enforcement moves to three places:
constructor functions, freezing, and tests that assert exhaustiveness.

Read the core skill for the rule; read this for how to express it.

## When to use

- A project written in plain JavaScript, with no type checker
- Node services, scripts, or browser code without TypeScript
- Adding JSDoc types to an existing JavaScript codebase

Not for: TypeScript projects, which have
[functional-typescript](../functional-typescript/SKILL.md).

## Notation mapping

| Neutral notation    | JavaScript                                      |
| ------------------- | ----------------------------------------------- |
| Record type         | Frozen object literal                           |
| Choice type         | Object with a `tag` field                       |
| Single-case wrapper | Frozen `{ tag, value }`                         |
| `Result<T, E>`      | `{ ok: true, value }` or `{ ok: false, error }` |
| `Option<T>`         | `undefined`, or a `Maybe` object                |
| `A -> B -> C`       | Arrow function returning a function             |
| `\|>` pipe          | `pipe` helper, or nested calls                  |
| `>>` composition    | Arrow function that pipes its argument          |
| `Async<T>`          | `Promise<T>`                                    |
| Exhaustive match    | `switch` with a `default` that throws           |

## Core rules

1. Default. **Freeze what you export.** `Object.freeze` at construction, on the
   value and on what it holds, turns accidental mutation into a visible failure
   in strict mode. A local that never leaves its function may stay unfrozen
   where a measurement shows that freezing it costs time that matters; anything
   exported, or stored for longer than one call, is frozen.
2. Rule. **Construct through functions.** Never build a tagged value with a
   literal outside its module. The constructor is where rules live.
3. Rule. **One tag key across the codebase.** Pick `tag` and never mix in `type`
   or `kind`. That is for the domain's own values: a wire shape keeps the key
   its contract gives it, and the mapping is where the two meet.
4. Rule. **Never mutate an argument.** Return a new object; the spread operator
   makes it cheap to read.
5. Rule. **Return values for expected failures.** Reserve `throw` for programmer
   errors.
6. Default. **Add JSDoc types and check them.** A type checker over JSDoc costs
   a comment per function and catches most shape mistakes.
7. Default. **Test exhaustiveness.** Every dispatch gets a test driven by the
   list of tags. Where `checkJs` is on and the `default` branch hands its value
   to a parameter typed `never`, the checker reports a missing case, and the
   test is kept only for values that arrive at run time.

## Pattern

```js
// payment.js — the only module that knows these shapes
const TAGS = Object.freeze({
  cash: "Cash",
  card: "Card",
  transfer: "Transfer",
});

const cash = () => Object.freeze({ tag: TAGS.cash });

const card = (number, expiry) =>
  isValidCard(number)
    ? ok(Object.freeze({ tag: TAGS.card, number, expiry }))
    : err(Object.freeze({ tag: "InvalidCard", number }));

const transfer = (bank) => Object.freeze({ tag: TAGS.transfer, bank });

const describe = (payment) => {
  switch (payment.tag) {
    case TAGS.cash:
      return "cash";
    case TAGS.card:
      return maskCard(payment.number);
    case TAGS.transfer:
      return payment.bank;
    default:
      throw new Error(`unhandled payment: ${payment.tag}`);
  }
};

export { TAGS, cash, card, transfer, describe };
```

The `default` branch is not a fallback; it is an assertion that the tag
set is what this function believes, and it fails loudly in test when a
case is added.

## Result without a library

```js
const ok = (value) => Object.freeze({ ok: true, value });
const err = (error) => Object.freeze({ ok: false, error });

const map = (f) => (r) => (r.ok ? ok(f(r.value)) : r);
const bind = (f) => (r) => (r.ok ? f(r.value) : r);
const mapError = (f) => (r) => (r.ok ? r : err(f(r.error)));

const pipe = (x, ...fns) => fns.reduce((acc, f) => f(acc), x);
```

These few lines cover the short-circuiting half of
[functional-handling-errors-with-results](../functional-handling-errors-with-results/SKILL.md).
[result-and-async.md](references/result-and-async.md) has `traverse`, the
form that accumulates, and async.

## How much of this to type-shape

Without a checker, a wrapper buys a name and a constructor, not an
enforced guarantee. That makes the temptation to skip modelling stronger
here than anywhere else in this pack, and the rule is unchanged: model
the shape, and leave it a plain object only on the observable facts in
[functional-choosing-types-or-plain-data](../functional-choosing-types-or-plain-data/SKILL.md).

What changes in JavaScript is the enforcement, not the decision. A
modelled shape here means a constructor function that is the only place
its object literal appears, a frozen result, and a coverage test where a
compiler would have checked exhaustiveness. When you do take the plain
route, it still owes a boundary parse, key constants, and accessors — a
bare object with none of those is not the data-first school, it is the
absence of any design.

## Red flags

- `Object.assign(target, ...)` where `target` is a parameter
- `array.push`, `sort`, `reverse`, or `splice` on a value from a caller
- A tag compared against a string literal outside its module
- `switch` on a tag with no `default`
- `async` functions that both decide and write
- A class whose methods mutate `this` and return `undefined`
- `null` and `undefined` both used, with different meanings

## Common mistakes

- **Shallow freezing.** `Object.freeze` does not freeze nested objects.
  Freeze at construction, all the way down, or build the value with
  `immer`, whose `produce` returns it frozen all the way down.
- **`sort` and `reverse` mutating in place.** Copy first:
  `[...xs].sort(cmp)`.
- **Equality on objects.** `===` is reference equality. Compare by a key,
  or write an explicit equality function.
- **Mixing `null` and `undefined`.** Pick one to mean absent; say which
  in the project's instructions.
- **Losing the error type.** `catch (e)` gives you anything. Classify it
  immediately at the boundary.
- **Relying on parameter defaults for domain decisions.** A default
  currency or timezone hides a choice the caller must make.

## Related skills

- [functional-modeling-with-algebraic-types](../functional-modeling-with-algebraic-types/SKILL.md)
- [functional-handling-errors-with-results](../functional-handling-errors-with-results/SKILL.md)
- [functional-managing-state-immutably](../functional-managing-state-immutably/SKILL.md)
- [functional-typescript](../functional-typescript/SKILL.md)

## Further reading

- [immutability.md](references/immutability.md) covers freezing,
  copying, nested updates, and which library to reach for when.
- [tagged-unions.md](references/tagged-unions.md) covers constructors,
  dispatch, and exhaustiveness tests.
- [result-and-async.md](references/result-and-async.md) covers the
  Result helpers, promises, and boundary conversion.

Files in this skill

  • SKILL.md7.5 KB
  • references/immutability.md6.7 KB
  • references/result-and-async.md6.5 KB
  • references/tagged-unions.md6.5 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.