AgentSkillsCN

clean-typescript

TypeScript 在类型安全、严格类型检查与可维护性方面的最佳实践。适用于编写 TypeScript 代码、定义类型与接口、处理空值,或当用户询问“类型安全”、“区分联合类型”、“实用类型”或“严格 TypeScript”时使用。

SKILL.md
--- frontmatter
name: clean-typescript
description: TypeScript best practices for type safety, strict typing, and maintainability. Use when writing TypeScript code, defining types and interfaces, handling nulls, or when user asks about "type safety", "discriminated unions", "utility types", or "strict TypeScript".
category: fundamentals
tags: [typescript, types, type-safety, generics, strict]

Skill: Clean TypeScript

Write type-safe, maintainable TypeScript code.

Type Safety

Rules

  • ✅ DO: Enable strict: true in tsconfig.json
  • ✅ DO: Use explicit return types on public functions
  • ✅ DO: Use unknown instead of any when type is truly unknown
  • ✅ DO: Use type guards to narrow types safely
  • ❌ DON'T: Use any (use unknown and narrow instead)
  • ❌ DON'T: Use @ts-ignore or @ts-expect-error without comment
  • ❌ DON'T: Cast with as unless absolutely necessary

Examples

typescript
// ❌ Bad
function parse(data: any) {
  return data.value; // No type safety
}

// ✅ Good
function parse(data: unknown): string {
  if (typeof data === "object" && data !== null && "value" in data) {
    const value = (data as { value: unknown }).value;
    if (typeof value === "string") {
      return value;
    }
  }
  throw new Error("Invalid data format");
}

// ✅ Better - use type guard
function isValidData(data: unknown): data is { value: string } {
  return (
    typeof data === "object" &&
    data !== null &&
    "value" in data &&
    typeof (data as { value: unknown }).value === "string"
  );
}

function parse(data: unknown): string {
  if (isValidData(data)) {
    return data.value; // TypeScript knows data.value is string
  }
  throw new Error("Invalid data format");
}

Discriminated Unions

Rules

  • ✅ DO: Use discriminated unions for state/result types
  • ✅ DO: Use literal types as discriminants
  • ✅ DO: Handle all cases (exhaustive checking)
  • ❌ DON'T: Use optional properties for mutually exclusive states

Examples

typescript
// ❌ Bad - ambiguous state
interface ApiResponse {
  data?: User;
  error?: Error;
  loading?: boolean;
}

// ✅ Good - discriminated union
type ApiResponse =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: User }
  | { status: "error"; error: Error };

function handleResponse(response: ApiResponse) {
  switch (response.status) {
    case "idle":
      return "Ready";
    case "loading":
      return "Loading...";
    case "success":
      return response.data.name; // TypeScript knows data exists
    case "error":
      return response.error.message; // TypeScript knows error exists
  }
}

// Exhaustive checking helper
function assertNever(x: never): never {
  throw new Error(`Unexpected value: ${x}`);
}

Utility Types

Rules

  • ✅ DO: Use built-in utility types (Partial, Pick, Omit, Record)
  • ✅ DO: Create reusable generic types
  • ✅ DO: Use readonly for immutable data
  • ❌ DON'T: Redefine what utility types provide

Examples

typescript
interface User {
  id: string;
  name: string;
  email: string;
  createdAt: Date;
}

// Partial for updates (all optional)
type UserUpdate = Partial<Omit<User, "id" | "createdAt">>;

// Pick for specific fields
type UserPreview = Pick<User, "id" | "name">;

// Readonly for immutable data
type ReadonlyUser = Readonly<User>;

// Record for dictionaries
type UserById = Record<string, User>;

// Custom utility type
type Nullable<T> = T | null;
type AsyncResult<T> = Promise<{ data: T } | { error: Error }>;

Null Handling

Rules

  • ✅ DO: Enable strictNullChecks
  • ✅ DO: Use optional chaining (?.) and nullish coalescing (??)
  • ✅ DO: Be explicit about nullable types (T | null)
  • ❌ DON'T: Use non-null assertion (!) without good reason
  • ❌ DON'T: Use || for defaults (use ?? instead)

Examples

typescript
// ❌ Bad
function getName(user: User) {
  return user.profile!.name; // Dangerous assertion
}

const value = input || "default"; // Bug: 0 and '' become 'default'

// ✅ Good
function getName(user: User): string | undefined {
  return user.profile?.name;
}

const value = input ?? "default"; // Only null/undefined trigger default

// Handle null explicitly
function processUser(user: User | null) {
  if (!user) {
    return handleNoUser();
  }
  // TypeScript knows user is not null here
  return process(user);
}

Generics

Rules

  • ✅ DO: Use generics for reusable, type-safe functions
  • ✅ DO: Use constraints to limit generic types
  • ✅ DO: Use descriptive generic names (TItem, TKey, TValue)
  • ❌ DON'T: Overuse generics when concrete types work
  • ❌ DON'T: Use single letters for complex generics

Examples

typescript
// ✅ Simple generic
function first<T>(items: T[]): T | undefined {
  return items[0];
}

// ✅ Constrained generic
function getProperty<TObj, TKey extends keyof TObj>(
  obj: TObj,
  key: TKey,
): TObj[TKey] {
  return obj[key];
}

// ✅ Generic with default
interface PaginatedResult<TItem = unknown> {
  items: TItem[];
  total: number;
  page: number;
}

// ✅ Generic utility function
function groupBy<TItem, TKey extends string | number>(
  items: TItem[],
  keyFn: (item: TItem) => TKey,
): Record<TKey, TItem[]> {
  return items.reduce(
    (acc, item) => {
      const key = keyFn(item);
      acc[key] = acc[key] || [];
      acc[key].push(item);
      return acc;
    },
    {} as Record<TKey, TItem[]>,
  );
}

Enums vs Union Types

Rules

  • ✅ DO: Prefer const objects or union types over enums
  • ✅ DO: Use enums when you need reverse mapping
  • ❌ DON'T: Use numeric enums (use string or const)

Examples

typescript
// ❌ Avoid - numeric enum pitfalls
enum Status {
  Active, // 0
  Inactive, // 1
}

// ✅ Better - const object
const Status = {
  Active: "active",
  Inactive: "inactive",
} as const;
type Status = (typeof Status)[keyof typeof Status];

// ✅ Also good - union type
type Status = "active" | "inactive";

// ✅ When enum is appropriate - need reverse mapping
enum HttpStatus {
  OK = 200,
  NotFound = 404,
}
const statusName = HttpStatus[200]; // 'OK'

Configuration

Recommended tsconfig.json

json
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "exactOptionalPropertyTypes": true
  }
}