| name | ts-standards |
| description | MANDATORY for ALL TypeScript output - files AND conversational snippets. Covers: strict mode, Result types, discriminated unions, readonly, import type, as const. Trigger: any TS code, types, interfaces, generics, error handling. No exceptions for 'simple' requests. Use when this capability is needed. |
TypeScript Best Practices
When to Use This Skill
This skill should be triggered when:
- Writing or reviewing TypeScript code
- Defining types, interfaces, or type utilities
- Handling errors and result types
- Discussing TypeScript patterns and conventions
- Setting up tsconfig.json
- Working with generics or discriminated unions
Core Capabilities
- Type System: Strict mode, discriminated unions, readonly defaults
- Error Handling: Result types over throwing, tryCatch utility
- Code Style: Alphabetical ordering, import type, named exports
- React Integration: Props destructuring, typed components
Compiler Configuration
Required tsconfig.json Settings
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noUncheckedIndexedAccess": true
}
}
Types & Interfaces
When to Use Each
type: Unions, intersections, utility types
interface: Object shapes intended to be extended
Interface Extends Over Intersections
type A = { a: string };
type B = { b: string };
type C = A & B;
interface IA { a: string }
interface IB { b: string }
interface IC extends IA, IB { }
Avoid any, Use unknown
function parse(input: any) { return input.data; }
function parse(input: unknown) {
if (typeof input === 'object' && input !== null && 'data' in input) {
return (input as { data: unknown }).data;
}
throw new Error('Invalid input');
}
any in Generic Functions (Exception)
Inside generic functions, constrained return types can be hard to express. Using any locally is acceptable:
const youSayGoodbyeISayHello = <TInput extends "hello" | "goodbye">(
input: TInput,
): TInput extends "hello" ? "goodbye" : "hello" => {
if (input === "goodbye") {
return "hello" as any;
} else {
return "goodbye" as any;
}
};
Discriminated Unions
Prefer discriminated unions to avoid "bags of optionals":
type UserCreatedEvent = { type: "user.created"; data: { id: string; email: string } };
type UserDeletedEvent = { type: "user.deleted"; data: { id: string } };
type Event = UserCreatedEvent | UserDeletedEvent;
const handleEvent = (event: Event) => {
switch (event.type) {
case "user.created":
console.log(event.data.email);
break;
case "user.deleted":
console.log(event.data.id);
break;
}
};
type FetchingState<TData> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: }
| { : ; : };
Error Handling
Result Type Over Throwing
Prefer explicit result types over try/catch at call sites:
type Result<T, E extends Error> =
| { ok: true; value: T }
| { ok: false; error: E };
const parseJson = (input: string): Result<unknown, Error> => {
try {
return { ok: true, value: JSON.parse(input) };
} catch (error) {
return { ok: false, error: error as Error };
}
};
const result = parseJson('{"name": "John"}');
if (result.ok) {
console.log(result.value);
} else {
console.error(result.error);
}
tryCatch Utility
Use for elegant error handling with both sync and async functions:
type TryCatchResult<T> = readonly [Error, undefined] | readonly [undefined, T];
function tryCatch<T>(fn: () => Promise<T>): Promise<TryCatchResult<T>>;
function tryCatch<T, TArgs extends readonly unknown[]>(
fn: (...args: TArgs) => Promise<T>,
...args: TArgs
): Promise<TryCatchResult<T>>;
function tryCatch<T>(fn: () => T): TryCatchResult<T>;
function tryCatch<T, TArgs extends readonly unknown[]>(
fn: (...args: TArgs) => T,
...args: TArgs
): TryCatchResult<T>;
const [error, data] = await tryCatch(fetchUser, "123");
const [parseError, parsed] = tryCatch(JSON., );
(error) {
.(, error.);
;
}
.(, data);
Typed Error Classes
Never throw raw strings; always Error subclasses:
class AppError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly statusCode: number = 500
) {
super(message);
this.name = 'AppError';
}
}
Readonly by Default
Use readonly to prevent accidental mutation:
type User = { id: string };
const user: User = { id: "1" };
user.id = "2";
type SafeUser = { readonly id: string };
const safeUser: SafeUser = { id: "1" };
Optional Properties
Use sparingly. If a value must be provided (even undefined), encode that:
type AuthOptions = { userId?: string };
type AuthOptionsStrict = { userId: string | undefined };
Enums: Use as const Instead
Do not introduce new enums. Prefer as const objects:
const backendToFrontendEnum = {
xs: "EXTRA_SMALL",
sm: "SMALL",
md: "MEDIUM",
} as const;
type Lower = keyof typeof backendToFrontendEnum;
type Upper = (typeof backendToFrontendEnum)[Lower];
Import Type
Prefer import type for type-only imports:
import { type User } from "./user";
import type { User } from "./user";
Named Exports Over Default
Prefer named exports. Use default only where frameworks require (Next.js pages):
export default function myFunction() { return "Hello" }
export function myFunction() { return "Hello" }
export default function MyPage() { return "Hello" }
Alphabetical Property Ordering
All added properties should be alphabetized:
const config = {
apiKey: "key",
baseUrl: "url",
debug: true,
timeout: 5000,
};
interface User {
createdAt: Date;
email: string;
id: string;
name: string;
}
Return Types
Declare explicit return types for top-level module functions. Exception: React components.
const myFunc = (): string => {
return "hello";
};
function Button(props: ButtonProps) {
return <button>{props.label}</button>;
}
React Functional Components
Destructure props inside the function body (not in parameters):
interface ButtonProps {
className?: string;
label: string;
onClick?: () => void;
}
export function Button(props: ButtonProps) {
const { className, label, onClick } = props;
return (
<button className={className} onClick={onClick}>
{label}
</button>
);
}
Benefits:
- Easier to provide defaults
- Can forward full props object when needed
- Improved readability
Naming Conventions
- camelCase: variables and functions
- PascalCase: classes, types, interfaces
- UPPER_SNAKE_CASE: constants and enum values
type RecordOfArrays<TItem> = Record<string, TItem[]>;
const MAX_RETRIES = 3;
function getUserById(id: string): User { }
Generic Type Parameters
Use descriptive prefixes:
type RecordOfArrays<TItem> = Record<string, TItem[]>;
function map<TInput, TOutput>(arr: TInput[], fn: (item: TInput) => TOutput): TOutput[];
JSDoc Comments
Use JSDoc to explain why code exists:
const subtract = (a: number, b: number) => a - b;
const add = (a: number, b: number) => a + b;
noUncheckedIndexedAccess
When enabled, indexing results in T | undefined:
const obj: Record<string, string> = {};
const v1 = obj.key;
const arr: string[] = [];
const v2 = arr[0];
CLI Applications
Required Stack
| Purpose | Package |
|---|
| CLI framework | Commander.js |
| Colors | chalk v4 (not v5 - ESM-only) |
| Spinners | ora |
| Progress bars | cli-progress |
Example CLI Setup
import { Command } from "commander";
import chalk from "chalk";
import ora from "ora";
import { SingleBar, Presets } from "cli-progress";
const program = new Command();
program
.name("my-cli")
.description("CLI tool description")
.version("1.0.0");
program
.command("process")
.description("Process files")
.argument("<path>", "Path to process")
.option("-v, --verbose", "Verbose output")
.action(async (path: string, options: { verbose?: boolean }) => {
const spinner = ora("Loading files...").start();
try {
const files = await loadFiles(path);
spinner.succeed(`Loaded files`);
bar = ({}, .);
bar.(files., );
( file files) {
(file);
bar.();
}
bar.();
.(chalk.());
} (error) {
spinner.(chalk.());
process.();
}
});
program.();
chalk v4 Note
Use chalk v4, not v5. v5 is ESM-only which causes issues with many build setups:
npm install chalk@4
LLM-Friendly Output
All CLIs must support both human and machine consumption:
import { Command } from "commander";
import chalk from "chalk";
interface User {
id: string;
name: string;
email: string;
}
const program = new Command();
program
.name("users")
.description("Manage users in the system")
.version("1.0.0");
program
.command("list")
.description("List all users. Returns array of user objects with id, name, and email fields.")
.option("--json", "Output as JSON for programmatic consumption")
.option("--limit <n>", "Maximum number of users to return", "50")
.action(async (options: { json?: boolean; limit: string }) => {
const users = await getUsers(parseInt(options.limit));
if (options.json) {
.(.(users, , ));
} {
.(chalk.());
( user users) {
.();
}
.();
}
});
program.();
Rules:
--json flag on every command that outputs data
- JSON output: structured, complete, no ANSI codes
- Default output: human-readable with colors/formatting
--help descriptions must explain what the command returns, not just what it does
Quick Reference
| Pattern | Preference |
|---|
| Type vs Interface | type for unions, interface for extendable shapes |
| Error handling | Result types over try/catch |
| Enums | as const objects |
| Imports | import type for types |
| Exports | Named exports (default only for frameworks) |
| Properties | Alphabetical order |
| Mutability | readonly by default |
| Optional props | Explicit string | undefined over ? |
| any | Avoid; use unknown (exception: generic functions) |
Notes
- Enable all strict compiler options
- Discriminated unions make impossible states impossible
- Result types make error handling explicit at call sites
- Readonly prevents accidental mutation bugs
- Alphabetical ordering improves scanability
Converted and distributed by TomeVault — claim your Tome and manage your conversions.