From predicate
Provides idiomatic TypeScript/JavaScript conventions: immutability, destructuring, arrow functions, and options objects. Useful when writing or reviewing TS/JS code.
How this skill is triggered — by the user, by Claude, or both
Slash command
/predicate:typescriptThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Work _with_ the language. Embrace its asynchronous nature, its structural type system, and its functional roots. Fight the urge to write Java-in-TypeScript.
Work with the language. Embrace its asynchronous nature, its structural type system, and its functional roots. Fight the urge to write Java-in-TypeScript.
const everything, spread to copy, never mutate in place| Keyword | Usage | Rationale |
|---|---|---|
const | Default for all bindings | Prevents reassignment, signals intent |
let | Only when reassignment is unavoidable | Loops, accumulators, state machines |
var | Prohibited | Function-scoped, hoists, leaks out of blocks |
Use destructuring to extract what you need. Use spread to copy without mutation:
// ✅ Destructure at the call site
const { name, age } = getUser();
// ✅ Shallow-copy with spread — never mutate the original
const updated = { ...config, timeout: 5000 };
const extended = [...items, newItem];
// ❌ Mutating the original
config.timeout = 5000;
items.push(newItem);
Use Object.hasOwn(obj, key) instead of obj.hasOwnProperty(key) — the latter can be shadowed.
Use arrow functions for callbacks and short expressions. They preserve lexical this:
// ✅ Arrow for callbacks
const sorted = items.sort((a, b) => a.name.localeCompare(b.name));
// ✅ Arrow for inline transforms
const names = users.map((u) => u.name);
// ❌ Legacy — don't use `function` for callbacks
const names = users.map(function (u) {
return u.name;
});
Use function declarations for top-level named functions (they hoist and have clear stack traces).
When a function's parameter list becomes unwieldy, use a single options object:
// ❌ Positional parameter overload
function createServer(
port: number,
host: string,
ssl: boolean,
timeout: number,
);
// ✅ Options object — extensible, self-documenting
interface ServerOptions {
port: number;
host?: string;
ssl?: boolean;
timeout?: number;
}
function createServer(options: ServerOptions);
Use rest syntax. The arguments object is prohibited:
// ✅ Rest gives you a real Array
function log(level: string, ...messages: string[]) {}
// ❌ arguments is not a real Array and has no type safety
function log() {
console.log(arguments);
}
Prefer functions that return values based solely on inputs. Reserve side effects for explicit boundaries (I/O, event handlers, logging).
Use map, filter, reduce to build declarative pipelines that produce new data:
// ✅ Declarative pipeline — new array, no mutation
const active = users.filter((u) => u.isActive).map((u) => u.email);
// ❌ Imperative mutation
const active: string[] = [];
users.forEach((u) => {
if (u.isActive) active.push(u.email);
});
forEach is acceptable for genuine side effects (logging, DOM, events) — not for data transformation.
JavaScript is async-first. Treat asynchrony as the normal case, not the exception.
async/awaitPrefer async/await over .then() chains:
// ✅ Linear, readable
async function fetchUser(id: string): Promise<User> {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new HttpError(res.status);
return res.json();
}
// ❌ Nested chains obscure flow
function fetchUser(id: string): Promise<User> {
return fetch(`/api/users/${id}`).then((res) => {
if (!res.ok) throw new HttpError(res.status);
return res.json();
});
}
| Pattern | Behavior | Use When |
|---|---|---|
Promise.all | Fails fast on first rejection | All must succeed |
Promise.allSettled | Waits for all, reports each | Partial failure is acceptable |
Promise.race | Resolves/rejects with first | Timeouts, fastest-wins |
Use AbortController for cancellable operations:
const controller = new AbortController();
const res = await fetch(url, { signal: controller.signal });
// Cancel from elsewhere
controller.abort();
Use import/export. CommonJS (require) is legacy.
Prefer named exports. They enable compile-time reference checks and explicit dependency graphs:
// ✅ Named — IDE catches broken imports immediately
export function parse(input: string): Token[] {
/* ... */
}
export interface Token {
type: string;
value: string;
}
// ⚠️ Default — permits arbitrary rename at import site
export default function parse(input: string): Token[] {
/* ... */
}
Default exports are acceptable for framework conventions (e.g., Vue SFCs, Next.js pages) but never preferred.
Use barrel files (index.ts) sparingly. They defeat tree-shaking and create circular dependency traps. Export directly from source modules when possible.
Enable strict: true in tsconfig.json. This is non-negotiable — it activates strictNullChecks, noImplicitAny, and other critical checks.
Let TypeScript infer when the type is obvious. Annotate at boundaries — function parameters, return types, and public APIs:
// ✅ Inferred — obvious from the literal
const count = 0;
const name = "alice";
// ✅ Annotated — boundary, not obvious
function parseConfig(raw: string): Config {
/* ... */
}
unknown over anyany disables type checking. Use unknown and narrow explicitly:
// ✅ Forces narrowing before use
function handle(input: unknown) {
if (typeof input === "string") {
console.log(input.toUpperCase());
}
}
// ❌ Silently accepts anything — bugs hide here
function handle(input: any) {
console.log(input.toUpperCase()); // runtime bomb
}
Model variant state with a literal discriminant. This enables exhaustive switch checking:
type Result<T> = { ok: true; value: T } | { ok: false; error: Error };
function handle(result: Result<string>) {
if (result.ok) {
console.log(result.value); // narrowed to { ok: true; value: string }
} else {
console.error(result.error); // narrowed to { ok: false; error: Error }
}
}
satisfiesValidate that a value conforms to a type without widening:
// ✅ Validates shape, preserves literal types
const routes = {
home: "/",
about: "/about",
users: "/users",
} satisfies Record<string, string>;
// TypeScript still knows routes.home is "/" (literal), not just string
as constUse as const for immutable literal types. Replaces enums in most cases:
// ✅ as const object — runtime value + narrow types
const Status = {
Active: "active",
Inactive: "inactive",
Pending: "pending",
} as const;
type Status = (typeof Status)[keyof typeof Status];
// "active" | "inactive" | "pending"
Use built-in utility types to derive types from existing ones:
| Type | Purpose | Example |
|---|---|---|
Partial<T> | All properties optional | Patch/update payloads |
Required<T> | All properties required | Validated config |
Readonly<T> | All properties readonly | Frozen state |
Pick<T, K> | Subset of properties | API response shaping |
Omit<T, K> | Exclude properties | Remove internal fields |
Record<K, V> | Map of key-value pairs | Lookup tables |
Simulate nominal typing for domain safety:
type UserId = string & { readonly __brand: unique symbol };
type PostId = string & { readonly __brand: unique symbol };
function createUserId(id: string): UserId {
return id as UserId; // validated assertion at construction
}
// Now UserId and PostId are incompatible at compile time
TypeScript types are stripped at build time. They cannot guard runtime execution.
as const objects or union types instead#field for private — not the private keyword, which is TypeScript-only// ✅ Native private — survives type erasure
class User {
#email: string;
constructor(email: string) {
this.#email = email;
}
}
// ❌ TypeScript private — erased, accessible at runtime
class User {
private email: string;
constructor(email: string) {
this.email = email;
}
}
Extend Error with domain-specific types. Always set name:
class HttpError extends Error {
constructor(
public readonly status: number,
message?: string,
) {
super(message ?? `HTTP ${status}`);
this.name = "HttpError";
}
}
Catch at the appropriate level. Don't swallow errors silently:
// ✅ Catch and handle or rethrow with context
try {
const user = await fetchUser(id);
} catch (err) {
if (err instanceof HttpError && err.status === 404) {
return null; // expected case
}
throw err; // unexpected — propagate
}
TypeScript's catch clause types as unknown. Narrow before accessing:
try {
await riskyOperation();
} catch (err) {
if (err instanceof Error) {
console.error(err.message);
} else {
console.error("Unknown error:", err);
}
}
| Scope | Style | Example |
|---|---|---|
| Classes, interfaces, type aliases | PascalCase | UserAccount, ServerOptions |
| Functions, variables, properties | camelCase | calculateTotal, isActive |
| Exported constants (true invariants) | SCREAMING_SNAKE | MAX_RETRIES, API_VERSION |
| Private class fields | #camelCase | #connectionPool |
| Generic type parameters | Single uppercase or T-prefix | T, K, TResult |
Use strict equality (=== / !==). Never rely on abstract coercion (==).
Use shortcuts for booleans (if (isValid)) but explicit comparisons for strings and numbers (if (name !== ""), if (count > 0)) to prevent coercion surprises.
Prefer ?? over || for defaults — || coerces 0, "", and false to falsy:
// ✅ Only falls through on null/undefined
const timeout = options.timeout ?? 3000;
// ❌ Falls through on 0, "", false
const timeout = options.timeout || 3000;
Use optional chaining (?.) for safe property access.
| Anti-Pattern | Description | Remedy |
|---|---|---|
any leakage | Using any to silence the compiler | Use unknown and narrow |
| Enum abuse | TypeScript enums that emit runtime code | as const objects or union types |
| Class-heavy OOP | Porting Java patterns (abstract classes, deep hierarchies) | Composition, interfaces, plain functions |
| Barrel file sprawl | Re-exporting everything through index.ts | Direct imports from source modules |
| Swallowed errors | Empty catch {} blocks | Handle, log, or rethrow |
| Type assertions | as Type to override the compiler | Annotations (const x: Type) or narrowing |
| Mutation in map/filter | Side effects inside declarative pipelines | forEach for effects, map for transforms |
tsc --noEmit # Type-check without emitting
npx eslint . # Lint
npx prettier --check . # Format check
npx prettier --write . # Auto-format
npx biome check . # All-in-one (alternative to eslint + prettier)
Formatting is not a debate. Pick prettier or biome and enforce it in CI.
const by default — let only when unavoidable, var neverstrict: true — always, no exceptionsunknown over any — narrow explicitlyasync/await — not .then() chainsas const — not enums#field — not private keyword=== — not ==?? — not || for defaultsThese idioms refine but are subordinate to the Code-Edit Constraints.
npx claudepluginhub nrdxp/predicate --plugin predicateEnforces TypeScript strict mode, ESLint rules, type safety, React patterns, naming conventions, and function length guidelines. Useful for writing or reviewing TypeScript/JavaScript frontend code.
Provides TypeScript best practices for type-safe code including strict mode, interfaces, discriminated unions, generics, async patterns, and null safety. Useful for type definitions and maintainable TS.
Core JavaScript language conventions, idioms, and modern practices. Invoke whenever task involves any interaction with JavaScript code — writing, reviewing, refactoring, debugging, or understanding .js/.jsx files and JavaScript projects.