本文へ移動
cccskills
無料GitHub で公開

typescript-patterns

TypeScript discipline for any JavaScript/TypeScript project (frontend + backend): strict mode, type design, generics, narrowing, error types, module resolution, tsconfig hygiene. Apply when the project has `tsconfig.json` and `typescript` in devDependencies. Stack-agnostic — referenced by every JS/TS framework plugin in the marketplace. Use this skill to: - Write types that catch bugs at compile time, not runtime. - Use generics, conditional types, and discriminated unions correctly. - Avoid `any`, `unknown`, and unsafe casts. - Match the project's tsconfig strictness level. - Type third-party libraries (with @types/* or declaration files). Do NOT use this skill for: - Plain JavaScript projects (no tsconfig.json). - Framework-specific type idioms (React component props, Vue defineProps, Angular signals — those live in framework plugins' own conventions skills). - tRPC/Zod runtime-validation specifics — handled by validation libs at the boundary.

インストール方法を見る

含まれるファイル(1)

  • SKILL.md11.3 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

TypeScript Patterns (stack-agnostic)

This skill encodes idioms that catch real bugs in any TypeScript codebase — backend or frontend, Node.js or browser. Apply alongside the active framework plugin's conventions skill (e.g., react-conventions, nest-conventions) and npm-patterns.

Detection

Project is TypeScript when all hold:

  • tsconfig.json exists in project root.
  • typescript is in devDependencies (or dependencies — rare but valid).

Read tsconfig.json first to learn the strictness level. The agent's behavior depends on it.

Tsconfig hygiene

A modern TypeScript project should have these flags. If the project's tsconfig is laxer, do not silently tighten it — match project conventions, but flag in DECISIONS that strictness could be improved.

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "exactOptionalPropertyTypes": true,
    "noFallthroughCasesInSwitch": true,
    "moduleResolution": "node16",
    "module": "node16",
    "target": "es2022",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "dist",
    "rootDir": "src"
  }
}

Frontend projects (Vite/Next.js/Angular) often use "module": "ESNext" + "moduleResolution": "bundler" instead — match the bundler's expectations.

Key flags and what they catch:

FlagCatches
strict: trueMaster switch — enables noImplicitAny, strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, alwaysStrict.
noUncheckedIndexedAccessarr[i] is T | undefined, not T. Forces explicit handling of out-of-bounds.
exactOptionalPropertyTypesDistinguishes { x?: number } (key may be absent) from { x: number | undefined } (key present, may be undefined).
isolatedModulesEach file must be independently transpilable — required for SWC/esbuild/Babel/Vite.

If you must add a new tsconfig flag for the feature, justify in DECISIONS and isolate it in a new tsconfig (tsconfig.feature.json extends base).

Strictness rules

Never use any

any opts out of type-checking entirely. If you genuinely don't know the type:

  • External input (HTTP body, DB row, URL params): type as unknown, then narrow via runtime validator (zod.parse, z.infer<T>).
  • Library without types: declare in src/types/<lib>.d.ts with the minimal shape you use.
  • Generic constraint: use unknown extends T ? ... : ... patterns, not any.

If you must use any (real-world example: working with eval-like dynamic dispatch), // eslint-disable-next-line @typescript-eslint/no-explicit-any with a one-line reason comment.

Prefer unknown over any

unknown is the safe top type. Forces explicit narrowing before use:

function parseJson(raw: string): unknown {
  return JSON.parse(raw);
}

const data = parseJson(input);
if (typeof data === 'object' && data !== null && 'id' in data) {
  // narrow further or validate
}

In production, prefer a runtime validator over hand-rolled narrowing.

Type assertions are last resort

x as Foo is a lie to the compiler. Use only when:

  • You know more than the compiler (e.g., after instanceof check that TS can't trace).
  • You're bridging to typed code from unknown after explicit validation.

Never use as any as Foo to shut up the compiler. Find the real type.

Non-null assertion x! — almost never

x! says "I promise this isn't null." TS can't verify. Use real narrowing:

// ❌ Don't
const user = users.find(u => u.id === id)!;

// ✅ Do
const user = users.find(u => u.id === id);
if (!user) throw new NotFoundError(`user ${id}`);

Type design

Discriminated unions for state

Don't use optional fields for state machines:

// ❌ Easy to misuse — what does { loading: true, data: foo } mean?
type Result = { loading?: boolean; data?: T; error?: Error };

// ✅ Discriminated — exhaustively checkable
type Result<T> =
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

Use switch (r.status) and exhaustiveness check via never:

function handle<T>(r: Result<T>) {
  switch (r.status) {
    case 'loading': return spinner();
    case 'success': return render(r.data);
    case 'error': return showError(r.error);
    default: {
      const _exhaustive: never = r;
      throw new Error(`unhandled: ${_exhaustive}`);
    }
  }
}

Branded types for IDs

Plain string IDs are interchangeable — UserId and OrderId mix. Brand them:

type Brand<T, B> = T & { __brand: B };
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;

const asUserId = (s: string): UserId => s as UserId;  // controlled cast at boundary

Use for primary keys, tokens, secrets — anything that's a string but shouldn't mix with other strings.

Readonly by default

Mutability is opt-in:

function summarize(items: ReadonlyArray<Item>): Summary { ... }
type Config = Readonly<{ port: number; host: string }>;

Mutable signatures are a permission slip — use them only when the function genuinely needs to mutate.

Avoid enums; use as const unions

// ❌ Old-school — generates runtime code, weird semantics
enum Role { Admin = 'admin', User = 'user' }

// ✅ String literal union — zero runtime cost, standard behavior
const ROLES = ['admin', 'user'] as const;
type Role = typeof ROLES[number];

Exception: const enum for hot paths where inlining matters (rare).

Generics

Constrain, don't widen

// ❌ T can be anything — defeats the point
function first<T>(xs: T[]): T | undefined { return xs[0]; }

// ✅ T extends a sensible shape
function getId<T extends { id: string }>(item: T): string {
  return item.id;
}

Infer return types from inputs

function pick<T, K extends keyof T>(obj: T, keys: readonly K[]): Pick<T, K> {
  // ...
}

The caller gets a precisely typed result without explicit annotation.

Conditional types — sparingly

Conditional types (T extends U ? A : B) are powerful but hard to read. Use for:

  • Library API ergonomics (e.g., ReturnType<F>).
  • Removing null from a generic: NonNullable<T>.

Avoid 3+ nested conditionals. If you reach for it, consider whether a simpler API works.

Error handling

Type errors as objects, not strings

class ValidationError extends Error {
  constructor(public readonly field: string, message: string) {
    super(message);
    this.name = 'ValidationError';
  }
}

class NotFoundError extends Error {
  constructor(public readonly resource: string) {
    super(`${resource} not found`);
    this.name = 'NotFoundError';
  }
}

Catch with instanceof:

try {
  await something();
} catch (err) {
  if (err instanceof ValidationError) return res.status(400).json({ field: err.field });
  if (err instanceof NotFoundError) return res.status(404).end();
  throw err;
}

catch (err: unknown)

In strict mode, caught errors are unknown. Narrow before access:

catch (err: unknown) {
  if (err instanceof Error) logger.error({ err: err.message, stack: err.stack });
  else logger.error({ err: String(err) });
}

Module resolution

ESM in modern projects

If package.json has "type": "module" (Node) or the project uses a bundler (Vite/Webpack/esbuild/Rollup):

  • File extensions in imports per project convention:
    • Node ESM: required (import { x } from './util.js' — .js even from a .ts source).
    • Bundler-driven (Vite/Webpack/Next): no extension needed, bundler resolves.
  • Use module: "node16" / "nodenext" / "ESNext" in tsconfig per environment.
  • For Node ESM: __dirname / __filename don't exist; use import.meta.url + fileURLToPath.

CJS on older Node or by choice

  • Imports without extension: import { x } from './util'.
  • Use module: "commonjs".
  • __dirname available natively.

Path aliases — match the project

If tsconfig has paths, use them via the configured prefix (@/util etc.). Note: TS path aliases don't transpile away — if the project bundles, ensure the bundler resolves them; if it doesn't, use a runtime resolver (tsconfig-paths) or stick to relative imports.

Validation at the boundary

Type the input as unknown, validate with a runtime schema, derive the type from the schema:

import { z } from 'zod';

const CreateUserSchema = z.object({
  email: z.string().email(),
  age: z.number().int().min(0),
});
type CreateUserInput = z.infer<typeof CreateUserSchema>;

// In a route handler / Server Action / form submit / event handler
const parsed = CreateUserSchema.safeParse(rawInput);
if (!parsed.success) return { errors: parsed.error.issues };
const input: CreateUserInput = parsed.data;  // fully typed and validated

Use whichever validator is in the project (zod, joi, yup, valibot, ajv + JSON Schema, class-validator). Don't introduce a new one without justification.

Verification before commit

After implementing, ALWAYS run:

npx tsc --noEmit

For Vue projects: npx vue-tsc --noEmit (understands .vue SFC types). For Angular projects: rely on ng build (Angular Compiler validates templates that plain tsc cannot).

If it errors, fix or report — never commit code that doesn't type-check. If the project has npm run typecheck or equivalent, prefer that.

For tests + types together:

npm test && npx tsc --noEmit

If superpowers:verification-before-completion is available, invoke it to systematically verify the change against the spec. Falls back to manual checklist (compile, run tests, re-read diff) if not.

Anti-patterns

  • ❌ as any to silence the compiler.
  • ❌ // @ts-ignore / // @ts-expect-error without a comment explaining why.
  • ❌ Disabling strict mode for one file (it lies to readers about safety guarantees).
  • ❌ Casting JSON.parse result to a specific type without runtime validation.
  • ❌ Using Function or Object types — too broad.
  • ❌ Empty interface (interface Foo {}) — same as {}, matches almost anything.
  • ❌ Type | undefined field on a Class without ? (means "must assign undefined", not "may omit").
  • ❌ Triple-slash directives in modern code (/// <reference path="..." />) — use imports.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Angular 18-21 project structure, standalone components vs NgModule, control flow (@if/@for/@switch + *ngIf/*ngFor legacy), decorators, dependency injection (inject() function), lifecycle hooks, pipes, Angular Universal SSR pointer. Use this skill to: - Detect project style (standalone vs NgModule) and apply matching patterns. - Pick correct decorators and DI approach. - Use modern control flow (@if/@for/@switch) in Angular 17+ projects. - Apply `inject()` function over constructor injection where appropriate. - Wire bootstrap correctly (bootstrapApplication for standalone, AppModule for legacy). Do NOT use this skill for: - State management (see angular-state-and-rx). - Routing (see angular-routing). - Forms (see angular-forms). - Testing (see angular-testing).

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

Angular forms: Reactive Forms (preferred — typed FormGroup/FormControl since Angular 14, FormBuilder, custom + async validators, FormArray, multi-step) and Template-driven (`[(ngModel)]` + FormsModule). Validation strategies, server error mapping, accessibility. Use this skill to: - Build Reactive Forms with typed FormGroup/FormControl. - Use FormBuilder для concise syntax. - Implement custom synchronous and async validators. - Wire FormArray for dynamic field lists. - Map server errors back to form fields. - Pick Reactive vs Template-driven (prefer Reactive). Do NOT use this skill for: - General conventions (see angular-conventions). - State management beyond forms (see angular-state-and-rx). - Routing (see angular-routing). - Testing forms (see angular-testing).

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

Angular Router (built-in `@angular/router`) — route configuration for standalone and NgModule projects, functional guards (Angular 14.1+), lazy loading, route resolvers, typed params via signals/observables, programmatic navigation, route data and meta. Use this skill to: - Configure routes (standalone-style or NgModule-style). - Use functional guards (canActivate as function, preferred over class-based in 17+). - Lazy-load components or feature modules. - Implement auth guards via route meta + functional guards. - Read params/queries via `inject(ActivatedRoute)` + signals or RxJS. Do NOT use this skill for: - General conventions (see angular-conventions). - State management (see angular-state-and-rx). - Forms (see angular-forms). - Testing routes (see angular-testing).

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

State management for Angular 18-21: signals (signal/computed/effect), services-as-state, NgRx Store + Effects + Selectors, NgRx Component Store, NgRx Signals (newer signal-based store). RxJS essentials — operators, async pipe, takeUntilDestroyed, signal/observable interop. Use this skill to: - Pick the right state tool (signals / services / NgRx variant / vue-query equivalent). - Use signals correctly (signal/computed/effect — when each). - Build a Pinia-style service-as-state singleton. - Set up NgRx Store + Effects + Selectors. - Use RxJS without leaking subscriptions (async pipe, takeUntilDestroyed, Subject patterns). - Bridge signals ↔ observables via toSignal / toObservable. Do NOT use this skill for: - General Angular conventions (see angular-conventions). - Routing state (see angular-routing). - Form state (see angular-forms). - Testing state (see angular-testing).

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

Testing Angular 18-21: TestBed, component harnesses (@angular/cdk/testing), Karma+Jasmine (default historical) vs Jest (jest-preset-angular, modern), Angular Testing Library (RTL-style). HttpClient mocking via HttpTestingController. NgRx Effects testing. Cypress / Playwright e2e. Use this skill to: - Detect runner (Karma+Jasmine vs Jest) and configure correctly. - Write component tests with TestBed. - Use component harnesses for Material / custom UI components. - Mock HttpClient via provideHttpClientTesting + HttpTestingController. - Test signal-based inputs with componentRef.setInput(). - Test NgRx Effects with provideMockActions. Do NOT use this skill for: - General Angular conventions (see angular-conventions). - Routing patterns broadly (see angular-routing — covers testing routes briefly). - Form patterns broadly (see angular-forms).

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

Shared conventions for every SDLC development-phase architect agent: hard rules, code quality bar, workflow steps (superpowers invocation, spec reading, codebase exploration, verification), and the report/compact-summary contract. Architects load this skill first, then apply their stack-specific instructions on top.

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

AratKruglik のスキルをすべて見る

このスキルの問題を報告する