AgentSkillsCN

typescript

LMS Next.js/Convex 项目的 TypeScript 高级模式与最佳实践。提供泛型、条件类型、映射类型、实用类型、类型守卫,以及专属于项目的特定模式。当您在 **/*.ts、**/*.tsx、convex/**/*.ts 文件中进行开发,或在创建类型、定义接口、解决 TypeScript 错误,或提升类型安全时,可使用此技能。关键词触发:generics、utility types、type guard、narrowing、conditional type、mapped type、infer、extends、Partial、Pick、Omit、ReturnType、Convex Id、unknown、never、discriminated union、exhaustive check。

SKILL.md
--- frontmatter
name: typescript
description: "TypeScript advanced patterns and best practices for the LMS Next.js/Convex project. Provides generics, conditional types, mapped types, utility types, type guards, and project-specific patterns. Use when working on files in **/*.ts, **/*.tsx, convex/**/*.ts, when creating types, defining interfaces, resolving TypeScript errors, or improving type safety. Triggers on keywords: generics, utility types, type guard, narrowing, conditional type, mapped type, infer, extends, Partial, Pick, Omit, ReturnType, Convex Id, unknown, never, discriminated union, exhaustive check."

TypeScript Advanced Patterns

Stack

  • TypeScript 5.x, strict mode enabled
  • Config: noUncheckedIndexedAccess, noImplicitReturns, noFallthroughCasesInSwitch
  • ESLint: @typescript-eslint with no-explicit-any, explicit-function-return-type

Quick Start

Convex ID Types

typescript
import { Id } from "convex/_generated/dataModel";

interface Course {
  _id: Id<"courses">;
  creatorId: Id<"users">;
  status: "draft" | "published" | "archived";
  title: string;
}

// Function with Convex ID parameter
function getCourse(id: Id<"courses">): Promise<Course | null> {
  // ...
}

Generic Higher-Order Component

typescript
import { ComponentProps, ElementType, ReactNode } from "react";

type WithTooltipProps<T extends ElementType> = {
  tooltip?: ReactNode;
} & ComponentProps<T>;

function withTooltip<T extends ElementType>(Component: T) {
  return function ExtendedComponent(props: WithTooltipProps<T>): ReactNode {
    const { tooltip, ...rest } = props;
    // ...
  };
}

Generic Form Field (React Hook Form)

typescript
import { ControllerProps, FieldPath, FieldValues } from "react-hook-form";

const FormField = <
  TFieldValues extends FieldValues = FieldValues,
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
  ...props
}: ControllerProps<TFieldValues, TName>): ReactNode => {
  return (
    <FormFieldContext.Provider value={{ name: props.name }}>
      <Controller {...props} />
    </FormFieldContext.Provider>
  );
};

Decision Trees

interface vs type?

code
Defining shape of an object?
├─ YES → interface (declaration merging, extends)
│   └─ Objects, classes, React props
└─ NO → type
    ├─ Union types → type Status = "draft" | "published"
    ├─ Tuple types → type Pair = [string, number]
    ├─ Mapped types → type Readonly<T> = { readonly [K in keyof T]: T[K] }
    └─ Utility compositions → type CourseWithAuthor = Course & { author: User }

Type Guard vs Type Assertion?

code
Need to verify type at runtime?
├─ YES → Type Guard (is keyword)
│   ├─ function isUser(x: unknown): x is User
│   └─ Returns boolean, narrows type in branches
└─ NO → Type Assertion (as keyword)
    ├─ Use ONLY when you have external guarantees
    └─ PREFER: unknown + type guard over any + assertion

Which Utility Type?

code
Want to transform a type?
├─ Make all props optional → Partial<T>
├─ Make all props required → Required<T>
├─ Make all props readonly → Readonly<T>
├─ Pick subset of props → Pick<T, "prop1" | "prop2">
├─ Exclude some props → Omit<T, "prop1" | "prop2">
├─ Get function return type → ReturnType<typeof fn>
├─ Get function params → Parameters<typeof fn>
├─ Extract from union → Extract<T, U>
├─ Exclude from union → Exclude<T, U>
├─ Remove null/undefined → NonNullable<T>
└─ Deep optional (custom) → See utility-types.md

Strict Rules

ContextRule
Return typesALWAYS explicit: function foo(): ReturnType
anyNEVER use. Use unknown + type guard or create proper type
GenericsALWAYS constrain: T extends SomeType not bare T
ExhaustiveALWAYS in switch: default: { const _: never = value; throw new Error(...) }
Convex IDsALWAYS use Id<"table">, never string
Unused varsPrefix with _: _unusedVar
Index accessHandle undefined (noUncheckedIndexedAccess enabled)
AssertionsAVOID as - use type guards for runtime safety

Common Patterns

Exhaustive Switch Check

typescript
type Status = "draft" | "published" | "archived";

function handleStatus(status: Status): string {
  switch (status) {
    case "draft": return "Draft mode";
    case "published": return "Published";
    case "archived": return "Archived";
    default: {
      const _exhaustive: never = status;
      throw new Error(`Unhandled status: ${status}`);
    }
  }
}

Safe Array Access

typescript
// With noUncheckedIndexedAccess
const items = ["a", "b", "c"];
const first = items[0]; // string | undefined

// Safe access patterns
const safeFirst = items[0] ?? "default";
const checkedFirst = items.at(0) ?? "fallback";

// When you know index is valid
if (items.length > 0) {
  const definitelyFirst = items[0]!; // Only with length check
}

Discriminated Union

typescript
type ApiResult<T> =
  | { success: true; data: T }
  | { success: false; error: string };

function handleResult<T>(result: ApiResult<T>): T | null {
  if (result.success) {
    return result.data; // TypeScript knows data exists
  }
  console.error(result.error); // TypeScript knows error exists
  return null;
}

References

Load these as needed for detailed patterns:

TopicFileWhen to Read
Advanced Typesreferences/advanced-types.mdGenerics, conditional types, mapped types, infer
Utility Typesreferences/utility-types.mdBuilt-in utilities + custom (DeepPartial, etc.)
Type Guardsreferences/type-guards.mdGuards, narrowing, discriminated unions, assertions