skills/typescript/SKILL.md
TypeScript strict patterns and best practices. Trigger: When implementing or refactoring TypeScript in .ts/.tsx (types, interfaces, generics, const maps, type guards, removing any, tightening unknown).
npx skillsauth add 333-333-333/agents typescriptInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
any - Use unknown + type guards or genericsimport type for type-only imports// ✅ ALWAYS: Create const object first, then extract type
const STATUS = {
ACTIVE: "active",
INACTIVE: "inactive",
PENDING: "pending",
} as const;
type Status = (typeof STATUS)[keyof typeof STATUS];
// Result: "active" | "inactive" | "pending"
// ✅ Use the const for runtime, type for annotations
function setStatus(status: Status) {
if (status === STATUS.ACTIVE) { /* ... */ }
}
// ❌ NEVER: Direct union types (no runtime values)
type Status = "active" | "inactive" | "pending";
Why? Single source of truth, runtime values, autocomplete, easier refactoring.
const HTTP_STATUS = {
OK: 200,
CREATED: 201,
BAD_REQUEST: 400,
NOT_FOUND: 404,
} as const;
type HttpStatus = (typeof HTTP_STATUS)[keyof typeof HTTP_STATUS];
// Result: 200 | 201 | 400 | 404
// With display labels
const STATUS_LABELS: Record<Status, string> = {
[STATUS.ACTIVE]: "Active",
[STATUS.INACTIVE]: "Inactive",
[STATUS.PENDING]: "Pending",
};
// ✅ ALWAYS: One level depth, nested objects → dedicated interface
interface UserAddress {
street: string;
city: string;
zipCode: string;
}
interface User {
id: string;
name: string;
email: string;
address: UserAddress; // Reference, not inline
}
// ✅ Extend for variations
interface Admin extends User {
permissions: string[];
role: "admin";
}
interface Guest extends Pick<User, "id" | "name"> {
sessionId: string;
}
// ❌ NEVER: Inline nested objects
interface User {
address: { street: string; city: string }; // NO!
}
// ❌ NEVER: Deep nesting
interface User {
profile: {
settings: {
notifications: { email: boolean }; // NO!
};
};
}
any// ✅ Use unknown for truly unknown types
function parseJSON(input: string): unknown {
return JSON.parse(input);
}
// ✅ Narrow with type guards
function processUser(input: unknown): User {
if (isUser(input)) return input;
throw new Error("Invalid user data");
}
// ✅ Use generics for flexible types
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
// ✅ For event handlers, type the event
function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
// ...
}
// ❌ NEVER use any
function parse(input: any): any { } // NO!
const data: any = fetchData(); // NO!
any// ✅ Wrap and type external APIs
interface ExternalApiResponse {
data: User[];
meta: { total: number };
}
async function fetchUsers(): Promise<ExternalApiResponse> {
const response = await externalApi.get("/users");
return response as ExternalApiResponse; // Type assertion at boundary
}
// ✅ Use type assertion only at boundaries, validate if critical
function fromExternalApi(data: unknown): User {
if (!isUser(data)) {
throw new TypeError("Invalid user from API");
}
return data;
}
// ✅ Basic type guard
function isString(value: unknown): value is string {
return typeof value === "string";
}
// ✅ Object type guard
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value &&
typeof (value as User).id === "string" &&
typeof (value as User).name === "string"
);
}
// ✅ Array type guard
function isUserArray(value: unknown): value is User[] {
return Array.isArray(value) && value.every(isUser);
}
// ✅ Discriminated union guard
interface Success { status: "success"; data: User }
interface Error { status: "error"; message: string }
type Result = Success | Error;
function isSuccess(result: Result): result is Success {
return result.status === "success";
}
// ✅ Assert and throw if invalid
function assertIsUser(value: unknown): asserts value is User {
if (!isUser(value)) {
throw new TypeError("Expected User");
}
}
// Usage - type is narrowed after assertion
function process(input: unknown) {
assertIsUser(input);
console.log(input.name); // TypeScript knows it's User
}
// Pick specific fields
type UserPreview = Pick<User, "id" | "name">;
// Omit specific fields
type UserWithoutId = Omit<User, "id">;
// Combine for API payloads
type CreateUserPayload = Omit<User, "id" | "createdAt">;
type UpdateUserPayload = Partial<Omit<User, "id">>;
// All fields optional
type PartialUser = Partial<User>;
// All fields required
type RequiredUser = Required<User>;
// All fields readonly
type ReadonlyUser = Readonly<User>;
// Deep readonly (custom)
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
// Record for dictionaries
type UserById = Record<string, User>;
type StatusCount = Record<Status, number>;
// Index signatures
interface Cache {
[key: string]: User | undefined; // Always include undefined
}
type Status = "active" | "inactive" | "pending" | "deleted";
// Extract subset
type ActiveStatus = Extract<Status, "active" | "pending">;
// Result: "active" | "pending"
// Exclude subset
type VisibleStatus = Exclude<Status, "deleted">;
// Result: "active" | "inactive" | "pending"
// Remove null/undefined
type NonNullUser = NonNullable<User | null | undefined>;
function createUser(name: string, email: string): User {
return { id: crypto.randomUUID(), name, email };
}
// Extract return type
type CreateUserReturn = ReturnType<typeof createUser>;
// Result: User
// Extract parameters
type CreateUserParams = Parameters<typeof createUser>;
// Result: [string, string]
// First parameter
type FirstParam = Parameters<typeof createUser>[0];
// Result: string
// ✅ Generic function
function identity<T>(value: T): T {
return value;
}
// ✅ Generic with constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// ✅ Generic interface
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
// Usage
type UserResponse = ApiResponse<User>;
type UsersResponse = ApiResponse<User[]>;
// Must have id property
interface HasId {
id: string;
}
function findById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
// Must be object
function merge<T extends object, U extends object>(a: T, b: U): T & U {
return { ...a, ...b };
}
interface PaginatedResponse<T, M = { total: number }> {
data: T[];
meta: M;
}
// Uses default meta
type UserPage = PaginatedResponse<User>;
// Custom meta
type UserPageWithCursor = PaginatedResponse<User, { cursor: string }>;
// ✅ Use literal type as discriminant
interface LoadingState {
status: "loading";
}
interface SuccessState<T> {
status: "success";
data: T;
}
interface ErrorState {
status: "error";
error: Error;
}
type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState;
// ✅ Exhaustive handling
function handleState<T>(state: AsyncState<T>): string {
switch (state.status) {
case "loading":
return "Loading...";
case "success":
return `Got ${state.data}`;
case "error":
return `Error: ${state.error.message}`;
default:
// Exhaustiveness check
const _exhaustive: never = state;
return _exhaustive;
}
}
// ✅ Type-only imports (removed at compile time)
import type { User, Status } from "./types";
// ✅ Mixed imports
import { createUser, type Config } from "./utils";
// ✅ Re-export types
export type { User, Status } from "./types";
export { createUser } from "./utils";
src/
├── types/
│ ├── index.ts # Re-exports all types
│ ├── user.ts # User-related types
│ ├── api.ts # API response types
│ └── constants.ts # Const maps and derived types
├── utils/
│ └── guards.ts # Type guard functions
└── features/
└── users/
└── types.ts # Feature-specific types
// Re-export all public types
export type { User, Admin, Guest } from "./user";
export type { ApiResponse, PaginatedResponse } from "./api";
export { STATUS, HTTP_STATUS } from "./constants";
export type { Status, HttpStatus } from "./constants";
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"forceConsistentCasingInFileNames": true,
"verbatimModuleSyntax": true
}
}
| Option | Why |
|--------|-----|
| strict | Enables all strict checks |
| noUncheckedIndexedAccess | arr[0] returns T \| undefined |
| exactOptionalPropertyTypes | { x?: string } doesn't accept undefined |
| verbatimModuleSyntax | Enforces import type syntax |
| Pattern | Use |
|---------|-----|
| const X = {} as const | Enum-like values with types |
| (typeof X)[keyof typeof X] | Extract union from const |
| interface A extends B | Inheritance |
| Pick<T, K> | Select fields |
| Omit<T, K> | Exclude fields |
| Partial<T> | All optional |
| Record<K, V> | Object/dictionary type |
| value is Type | Type guard return |
| asserts value is Type | Assertion function |
| T extends Constraint | Generic constraint |
testing
Review Flutter components and screens for UX/UI compliance. Trigger: When user invokes /ux-review command or requests UX audit.
testing
Testing philosophy and strategy for every feature: test pyramid, mandatory levels per change type, completion checklist, and skill delegation. Trigger: When planning tests for a feature, reviewing test coverage, defining acceptance criteria, or asking what tests a change needs.
development
Terraform security practices: sensitive variables, secret management, state protection, .gitignore patterns, and CI/CD credential handling. Trigger: When handling secrets in Terraform, configuring state backends, reviewing .gitignore for Terraform, or setting up CI/CD pipelines for infrastructure.
development
Terraform code conventions: file organization, naming, variables, outputs, formatting, and the plan/apply workflow. Trigger: When writing Terraform files, reviewing HCL code style, organizing .tf files, or adding new Terraform modules.