Functional TypeScript Code Review
A guide for reviewing server-side TypeScript code against the kamae-ts principles (see index). Each checklist item maps one-to-one to a chapter of the principles.
This guide is a human-readable version of the checklist used internally by the
kamae-reviewskill in the kamae-ts plugin. Use it as a reference when reviewing manually without a coding agent.
Review Procedure
- Read the principle knowledge base first. Before looking at code, read the following so you can cite canonical principles in your findings:
- index.md — principle index
- error-handling.md
- boundary-defense.md
- state-modeling.md
- The validation-library guide matching the project’s
package.json(validation-libraries/ —zod.md/valibot.md/arktype.md) - The Result-library guide matching the project’s
package.json(result-libraries/ —neverthrow.md/byethrow.md/fp-ts.md/option-t.md)
- Read the files under review.
- Scan through the checklist items below in principle order (matching the chapter structure of index.md).
- When you find a violation, report it with the principle, the reason it matters, and a suggested fix.
- When something is not a violation but could be improved, present it as a suggestion.
Checklist
The checklist directly mirrors the structure of index.md. Each item links back to the canonical chapter.
1. Type-Driven Domain Modeling
1.1 Domain state modeled as a Discriminated Union
Reference: ./index.md §1 “Express domain state with Discriminated Unions”
Signal: a single type with many optional properties and a string status field (e.g. { state: string; driverId?: string; startTime?: Date }). Suggest splitting into per-state types combined into a union, making state-specific properties required.
1.2 Discriminant unified as kind
Reference: ./index.md §1 “Use kind as the unifying discriminant”
Signal: a discriminant named type, status, state, _tag, or anything other than kind. Suggest renaming to kind for codebase consistency.
1.3 No use of class for domain models
Reference: ./index.md §1 “Express domain state with Discriminated Unions” and the Companion Object pattern.
When class is used to define a domain entity or value object, suggest migrating to a Discriminated Union + Companion Object pattern. Class inheritance required by an external library is a justified deviation.
1.4 Companion Object pattern followed
Reference: ./index.md §1 “Companion Object pattern”
Verify:
- Operations related to a type are consolidated in a
constwith the same name as the type. - A Branded Type’s validation schema is exposed as a
.schemaproperty on the companion object, not as a standaloneXxxSchemaexport. - Domain logic that belongs on the companion object is not scattered as free-standing functions like
xxxAssignDriver.
1.5 No interface for domain types
Reference: ./index.md §1 “Use type, not interface”
Declaration merging can silently alter a type’s shape. Define domain types with type. interface is only acceptable for library type augmentation.
1.6 No method notation in type definitions
Reference: ./index.md §1 “Use function-property notation, not method notation”
Method notation (save(task: Task): Promise<void>) makes parameters bivariant, allowing a narrower implementation (save(task: DoingTask): …) to pass type-checking at the injection site. Suggest switching to function-property notation (save: (task: Task) => Promise<void>).
1.7 Branded Types applied to semantically distinct primitives
Reference: ./index.md §1 “Use Branded Types to distinguish meaning”. Also see the project’s validation-library guide (./validation-libraries/).
Signal: IDs or semantically distinct values (UserId, OrderId, Email, monetary amounts, etc.) represented as plain string / number. If a validation library is present, verify that its branding feature is used (no as cast needed); otherwise confirm the unique symbol pattern is in place.
1.8 Domain objects wrapped in Readonly<>
Reference: ./index.md §1 “Use Readonly<> to guarantee immutability”
Signal: domain object type definitions not protected by Readonly<…> (or per-property readonly). State changes should be expressed by constructing new objects.
1.9 One-concept-per-file layout
Reference: ./index.md §1 “File layout: one concept per file”
Signal: catch-all files such as types.ts, models.ts, or domain.ts that aggregate many domain types, especially when companion objects live in separate files. Barrel files (index.ts) should contain re-exports only.
2. State Transitions via Pure Functions
Reference: ./index.md §2 and ./state-modeling.md
2.1 Transition functions constrain the source state via argument types
Signal: a transition function’s argument type is the full union (TaxiRequest) rather than an individual state (Waiting). Accepting a wide type allows calls from invalid source states.
2.2 assertNever present in switch over a Discriminated Union
Reference: ./index.md §2 “Exhaustiveness check”
Signal: a switch branching on kind without default: return assertNever(x). Adding a new variant will not produce a compile error.
3. Error Handling — Railway Oriented Programming
Reference: ./index.md §3, ./error-handling.md, and the project’s Result-library guide (./result-libraries/).
3.1 No throw in the domain layer
Signal: throw inside an entity, value object, or use case. Suggest converting to a Result type. Acceptable exceptions: throw inside assertNever (unreachable), and unexpected infrastructure failures in the infrastructure layer.
Also flag: ResultAsync.fromSafePromise (or equivalent “safe” wrapper in other libraries) wrapping a Promise that can reject — database calls, network I/O, external API calls. fromSafePromise is a contract stating the Promise never rejects; violating it bypasses the Result error channel and produces an unhandled rejection at runtime. Suggest fromPromise with an explicit error mapper, and include the mapped error type in the function’s error union. Reference: ./error-handling.md §fromSafePromise Misuse
3.2 Error types defined as Discriminated Unions
Signal: Error subclasses, free-form string error codes, or Result<T, string>. Suggest a Discriminated Union ({ kind: "DriverNotAvailable"; driverId } | { kind: "RequestAlreadyAssigned" }) so callers can handle errors exhaustively.
Also flag: error DU variants where contextual data (IDs, codes, values that caused the error) exists only in a message: string field and is not exposed as typed fields. A message field itself is fine for logging or display, but when callers must parse it to extract values for branching or retry logic, the typed error has lost its purpose. Suggest adding the relevant context as named fields alongside message. Reference: ./error-handling.md §Designing Error Types
3.3 Result chains used for composition (no premature unwrap)
Verify that the project’s Result-library API (.map, .andThen, Result.do, etc.) is used for chained composition. If the code immediately unwraps into if/else, cite the relevant guide under ./result-libraries/ and suggest an appropriate combinator.
Also flag: andThen / map callbacks exceeding ~5 lines or containing multi-branch if/else logic. This is procedural code wrapped in a Result combinator, not Railway Oriented Programming. Suggest extracting each logical step into a named function so the chain reads as a flat pipeline of operations. Reference: ./error-handling.md §Composing Operations
4. Boundary Defense
Reference: ./index.md §4, ./boundary-defense.md, and the project’s validation-library guide (./validation-libraries/).
4.1 Schema validation at every external boundary
Signal: API handlers, DB result mappings, queue/message handlers, file/config reads, or environment-variable reads where raw data is treated as a domain type without being parsed through a validation library (Zod / Valibot / ArkType).
4.2 No as type assertions
Reference: ./index.md §4 “Do not use type assertions (as)”
The only permitted as forms are as const and as const satisfies Type. Enumerate every other as and verify it falls into one of:
- External or unknown-typed data: should be replaced with schema parsing.
asdoes not give the guarantee its type claims. asinside a Branded Type constructor: tolerated only as a last-resort fallback when no validation library is present (unique symbolpattern). When flagged, recommend introducing a validation library and rewriting the brand withz.brand()/v.brand()/.brand()so theascan be removed.- Internal data: should be resolvable via type inference. If not, the type design is likely wrong.
4.3 PII fields wrapped in Sensitive<T>
Reference: ./index.md §4 “PII defense”, ./boundary-defense.md
Signal: fields that may contain personal information (name, email address, phone number, address, various IDs, payment information, health/diagnostic information, IP address, etc.) represented as plain string / number. Pay particular attention to objects that may appear in logs or error messages. Also check that the validation schema automatically wraps PII fields with Sensitive.of.
5. Declarative Style
Reference: ./index.md §5, ./state-modeling.md
5.1 Array operations written declaratively
Signal: transformations expressible with filter / map / reduce being built imperatively with for / for…of loops. Suggest defining predicate functions on the companion object and writing tasks.filter(Task.isActive).
5.2 Domain events published as immutable records
Signal: state-mutation code directly mutating a shared event log, or domain events not being published where the state-modeling guide requires them. Events should be recorded as Readonly<{ eventId; eventAt; eventName; payload; aggregateId }>, separated from the repository.
5.3 Companion-object predicates free of redundant x is Y annotations
Signal: predicate functions over a discriminated union that carry an explicit : x is Y return-type annotation when the body is just kind === "..." comparisons (or their !== negation). TypeScript 5.5+ infers the type predicate from such bodies and Array.prototype.filter consumes the inferred predicate, so the annotation adds nothing — and falsely implies that discriminated union narrowing alone is insufficient. Suggest dropping the annotation.
6. Test Data
Reference: ./index.md §6
6.1 Fixtures defined with as const satisfies Type
Signal: test fixtures typed with : Type = or as Type, causing discriminant literal types to widen to string. Suggest as const satisfies Type to preserve the kind literal type.
How to Write Findings
Each finding should include:
- What is wrong: the specific code location (
path:line). - Why it matters: the principle (with a reference link to
./...) and the risk introduced by the violation. - How to fix it: a code example showing the suggested fix.
### Method notation used
`src/repository/task-repository.ts:15`
`save(task: Task): Promise<void>` uses method notation.
Per [`./index.md` §1 "Use function-property notation"](/kamae-ts/en/),
method notation makes parameters bivariant, so a narrower implementation
`save(task: DoingTask): Promise<void>` would pass type-checking at the injection site.
Suggested fix:
\`\`\`typescript
type TaskRepository = {
save: (task: Task) => Promise<void>;
};
\`\`\`
Severity
| Severity | Item | Reason |
|---|---|---|
| [High] | as type assertions (4.2) | Direct cause of runtime errors |
| [High] | Unprotected PII (4.3) | Compliance violation risk |
| [High] | Missing schema validation at external boundaries (4.1) | Direct cause of runtime errors |
| [High] | Missing Branded Types for semantically distinct primitives (1.7) | Mixed-up IDs surface at runtime |
| [Medium] | Use of class (1.3) | Degrades type safety during extension |
| [Medium] | State modeled with optional properties (1.1) | Invalid states become representable |
| [Medium] | throw in the domain layer (3.1) | Inconsistent error handling |
| [Medium] | Error type not a Discriminated Union (3.2) | Callers cannot handle errors exhaustively |
| [Medium] | Missing assertNever (2.2) | New variants go undetected at compile time |
| [Medium] | Transition function accepts the full union type (2.1) | Invalid transitions compile without error |
| [Medium] | Catch-all type files (1.9) | Circular dependencies; type/behavior separation |
| [Medium] | Companion Object pattern violated; schema exported standalone (1.4) | Leaks implementation details |
| [Low] | Method notation (1.6) | Only problematic under specific conditions |
| [Low] | interface for domain types (1.5) | Declaration-merging accidents are rare |
| [Low] | Domain types missing Readonly<> (1.8) | Mutations are often caught in review |
| [Low] | Discriminant is not kind (1.2) | Style inconsistency rather than a bug |
| [Low] | Imperative array loops (5.1) | Readability, not correctness |
| [Low] | Domain events not published (5.2) | Depends on whether event sourcing is adopted |
| [Low] | Redundant x is Y predicate annotation (5.3) | Wastes characters; misleads about discriminated union narrowing |
| [Low] | Fixtures missing as const satisfies (6.1) | Typically caught by tests in practice |