| name | standardizing-typescript |
| disable-model-invocation | true |
| description | TypeScript code standards enforced across all skills. Loaded by other skills, not invoked directly. |
| allowed-tools | Read |
TypeScript code standards enforced by tsc strict mode, eslint, and manual review. Defines what `/coding-typescript` must follow and `/auditing-typescript` enforces.
<quick_start>
Reference this skill when coding or reviewing TypeScript. Standards grouped by category with eslint rule codes. All examples show correct (✅) and incorrect (❌) patterns.
</quick_start>
<success_criteria>
Code follows these standards when tsc strict mode and eslint checks pass. See summary table at the end for the complete rejection criteria with rule codes.
</success_criteria>
<reference_note>
This is a reference skill. Other TypeScript skills reference these standards. You typically don't invoke this directly—invoke /coding-typescript, /testing-typescript, or /auditing-typescript instead.
These standards apply to ALL TypeScript code: production and test code alike.
</reference_note>
<type_safety>
TypeScript strict mode enforces type safety. All violations are caught by tsc at compile time.
function process(data: any): any {
return data;
}
function process(data: Record<string, string>): ProcessResult {
return new ProcessResult(data);
}
const result = someFunction();
const result = someFunction();
function getValue(x: string | number): string {
return x as string;
}
function getValue(x: string | number): string {
if (typeof x === "string") {
return x;
}
return x.toString();
}
function process<T>(value: T): T {
return value.toString();
}
function process<T extends { toString(): string }>(value: T): string {
return value.toString();
}
function handle(value: string | number): void {
console.log(value.toUpperCase());
}
function handle(value: string | number): void {
if (typeof value === "string") {
console.log(value.toUpperCase());
} else {
console.log(value.toString());
}
}
tsconfig.json strict mode settings:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true
}
}
ESLint rules enforced:
| Rule | What it catches |
|---|
| @typescript-eslint/no-explicit-any | Unqualified any usage |
| @typescript-eslint/ban-ts-comment | @ts-ignore instead of @ts-expect-error |
| @typescript-eslint/consistent-type-assertions | Type assertions without justification |
</type_safety>
<named_constants>
Test values and configuration must use named constants, not inline literals.
const VALID_SCORE = 85;
const MIN_SCORE = 0;
const MAX_SCORE = 100;
const VALID_INPUT = "simple";
const EXPECTED_RESULT = 42;
describe("ScoreValidation", () => {
it("accepts valid score", () => {
expect(validateScore(VALID_SCORE)).toBe(true);
});
it("rejects above maximum", () => {
expect(validateScore(MAX_SCORE + 1)).toBe(false);
});
});
describe("ScoreValidationBad", () => {
it("accepts valid score", () => {
expect(validateScore(85)).toBe(true);
});
it("rejects above maximum", () => {
expect(validateScore(101)).toBe(false);
});
});
Why named constants matter:
- Sharing between tests and production code
- Clear documentation of what values mean
- Easy updates when requirements change
- Self-documenting test intent
ESLint rules enforced:
| Rule | What it catches |
|---|
| no-magic-numbers | Literal numbers in code |
| @typescript-eslint/no-magic-numbers | TypeScript-specific literals |
Rule exemptions: ESLint already exempts common idiomatic values: 0, 1, -1 in array indexes, and enum values. You don't need constants for these.
expect(results.length).toBe(0);
expect(count).toBe(1);
const first = items[0];
const last = items[items.length - 1];
</named_constants>
<error_handling>
try {
process();
} catch (err) {
}
async function bad(): Promise<void> {
fetchData();
}
try {
process();
} catch (err) {
if (err instanceof ValidationError) {
log.error("Invalid input:", err.message);
throw err;
}
throw new ProcessingError("Unexpected error during processing", { cause: err });
}
async function good(): Promise<void> {
await fetchData();
fetchData().catch(err => handleError(err));
}
class ValidationError extends Error {
constructor(
message: string,
public readonly field: string,
public readonly value: unknown,
) {
super(message);
this.name = "ValidationError";
}
}
throw new ValidationError(
`Score must be between ${MIN_SCORE} and ${MAX_SCORE}, got ${score}`,
"score",
score,
);
ESLint rules enforced:
| Rule | What it catches |
|---|
| no-empty | Empty catch blocks |
| @typescript-eslint/no-floating-promises | Unhandled promise rejections |
</error_handling>
const API_KEY = "sk-1234567890";
const password = "hunter2";
const result = eval(userInput);
const fn = new Function("x", "return x + 1");
import { exec } from "child_process";
exec(`grep ${userInput} file.txt`);
import { execFile } from "child_process";
execFile("grep", [userInput, "file.txt"]);
const apiKey = process.env.API_KEY;
if (!apiKey) {
throw new Error("API_KEY environment variable required");
}
Context matters for security rules—a CLI tool invoked by the user has different trust boundaries than a web service. See /auditing-typescript for false positive handling.
ESLint rules enforced:
| Rule | What it catches |
|---|
| no-eval | Use of eval() |
| no-new-func | Use of new Function() |
| (manual) | Hardcoded secrets |
| (manual) | child_process.exec() with untrusted input |
<code_hygiene>
import { processData } from "./utils";
const unusedVariable = 42;
function process(data: string): void {
console.log("Processing:", data);
}
import { logger } from "./logger";
function process(data: string): void {
logger.debug("Processing:", data);
}
import { processData } from "./utils";
function handle(input: string): void {
processData(input);
}
ESLint rules enforced:
| Rule | What it catches |
|---|
| @typescript-eslint/no-unused-vars | Unused variables and imports |
| no-console | console.log in production code |
| (manual) | Dead code or commented-out code |
</code_hygiene>
<import_hygiene>
Depth Rules
| Depth | Syntax | Verdict | Rationale |
|---|
| Same dir | import { x } from './y' | OK | Module-internal, same package |
| 1 level | import { x } from '../y' | REVIEW | Is this truly module-internal? |
| 2+ levels | import { x } from '../../..' | REJECT | Use path alias — crosses package boundary |
Module-Internal vs. Infrastructure
Module-internal files live in the same package and move together. Relative imports are acceptable:
import { Position } from "./position";
import { tokenize } from "./tokens";
Infrastructure is stable code that doesn't move when your feature moves. Must use path aliases:
import { createTree } from "../../../../../../tests/helpers";
import { createTree } from "@testing/helpers";
Anti-Patterns
import { helper } from "../../../../lib/utils";
import { helper } from "lib/utils";
Required Project Setup
1. Configure tsconfig.json with path aliases:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@testing/*": ["tests/*"],
"@lib/*": ["src/lib/*"]
}
}
}
2. Common path alias patterns:
| Alias | Maps to | Purpose |
|---|
@/* | src/* | Main application code |
@testing/* | tests/* | Test helpers and utilities |
@lib/* | src/lib/* | Shared library code |
3. Usage with path aliases:
import { processData } from "@/features/processing";
import { Logger } from "@lib/logger";
import { createMockContext } from "@testing/helpers";
import { Position } from "./position";
import { tokenize } from "./tokens";
</import_hygiene>
<rejection_criteria_summary>
| Issue | Example | Rule/Tool |
|---|
Unqualified any | function f(x: any): any | @typescript-eslint/no-explicit-any |
@ts-ignore without reason | // @ts-ignore | @typescript-eslint/ban-ts-comment |
| Type assertion without narrowing | return x as string | @typescript-eslint/consistent-type-assertions |
| Unconstrained generic | function f<T>(x: T) | tsc strict mode |
| Union without narrowing | value.toUpperCase() on string | number | tsc strict mode |
| Magic numbers in tests | expect(result).toBe(42) | @typescript-eslint/no-magic-numbers |
| Empty catch block | catch (err) {} | no-empty |
| Unhandled promise | fetchData(); without await | @typescript-eslint/no-floating-promises |
| Missing error context | throw new Error('failed') | manual review |
| Hardcoded secrets | const API_KEY = "sk-..." | manual review |
eval() usage | eval(userInput) | no-eval |
new Function() usage | new Function('x', 'return x') | no-new-func |
exec() with untrusted input | exec(\grep ${input} file`)` | manual review |
| Unused variables/imports | import { x } from 'y'; // never used | @typescript-eslint/no-unused-vars |
console.log in production | console.log('debug') | no-console |
| Dead/commented code | // function old() { ... } | manual review |
| Deep relative imports | from '../../../lib' | manual review |
</rejection_criteria_summary>