| name | typescript-fieldguide |
| description | This skill should be used when writing TypeScript, eliminating any types, implementing Zod validation, or when strict type safety is needed. Covers modern TS 5.5+ features and runtime validation patterns. |
| metadata | {"version":"1.0.1"} |
TypeScript Development
Type-safe code = compile-time errors = runtime confidence.
<when_to_use>
- Writing new TypeScript code
- Eliminating
any types
- Using modern TypeScript 5.5+ features
- Validating API inputs/outputs with Zod
- Implementing Result types and discriminated unions
- Creating branded types for domain concepts
NOT for: runtime-only logic unrelated to types, non-TypeScript projects
</when_to_use>
tsconfig.json strict settings:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true,
"verbatimModuleSyntax": true,
"isolatedModules": true,
"skipLibCheck": false
}
}
Version requirements: TS 5.2+ (using), TS 5.4+ (NoInfer), TS 5.5+ (inferred predicates)
Core Patterns
<eliminating_any>
any defeats the type system. Use unknown + guards.
function process(data: any) {
return data.value;
}
function process(data: unknown): string {
if (!hasValue(data)) throw new TypeError("Invalid");
return data.value.toString();
}
function hasValue(v: unknown): v is { value: unknown } {
return typeof v === "object" && v !== null && "value" in v;
}
Validate at boundaries:
async function fetchUser(id: string): Promise<User> {
const data: unknown = await fetch(`/api/users/${id}`).then((r) => r.json());
return UserSchema.parse(data);
}
</eliminating_any>
<result_types>
Exceptions hide errors from types. Result makes them explicit.
type Result<T, E = Error> =
| { readonly ok: true; readonly value: T }
| { readonly ok: false; readonly error: E };
type UserError =
| { readonly type: "not-found"; readonly id: string }
| { readonly type: "network"; readonly message: string };
async function getUser(id: string): Promise<Result<User, UserError>> {
try {
const response = await fetch(`/api/users/${id}`);
if (response.status === 404)
return { ok: false, error: { type: "not-found", id } };
if (!response.ok)
return {
: ,
: { : , : response. },
};
{ : , : response.() };
} (e) {
{ : , : { : , : (e) } };
}
}
result = (id);
(!result.) {
(result..) {
:
(result..);
:
(result..);
}
}
(result.);
See result-pattern.md for utilities (map, flatMap, combine).
</result_types>
<discriminated_unions>
Prevent illegal state combinations.
type Request = { status: 'idle'|'loading'|'success'|'error'; data?: User; error?: string; };
type RequestState =
| { readonly status: 'idle' }
| { readonly status: 'loading' }
| { readonly status: 'success'; readonly data: User }
| { readonly status: 'error'; readonly error: string };
function render(state: RequestState): JSX.Element {
switch (state.status) {
case 'idle': return <div>Ready</div>;
case 'loading': return <div>Loading...;
: ;
: ;
: (state);
}
}
(): {
();
}
</discriminated_unions>
<branded_types>
Prevent mixing incompatible primitives.
declare const __brand: unique symbol;
type Brand<T, B extends string> = T & { readonly [__brand]: B };
type UserId = Brand<string, "UserId">;
type ProductId = Brand<string, "ProductId">;
function createUserId(value: string): UserId {
if (!/^user-\d+$/.test(value)) throw new TypeError(`Invalid: ${value}`);
return value as UserId;
}
const userId = createUserId("user-123");
getUser(userId);
Security:
type SanitizedHtml = Brand<string, "SanitizedHtml">;
function sanitize(raw: string): SanitizedHtml {
return escapeHtml(raw) as SanitizedHtml;
}
function render(html: SanitizedHtml): void {
element.innerHTML = html;
}
See branded-types.md for advanced patterns.
</branded_types>
Modern TypeScript (5.2+)
<resource_management>
using for automatic cleanup (TS 5.2+):
class DatabaseConnection implements Disposable {
[Symbol.dispose]() {
this.close();
}
}
function query() {
using conn = new DatabaseConnection();
return conn.query("SELECT * FROM users");
}
async function asyncWork() {
await using resource = new AsyncResource();
}
Use for: connections, file handles, locks, transactions.
</resource_management>
<satisfies_operator>
Validate type without widening (TS 4.9+):
const config = {
port: 3000,
host: "localhost",
} satisfies Record<string, string | number>;
config.port;
const routes = {
home: "/",
user: "/user/:id",
} as const satisfies Record<string, string>;
type HomeRoute = typeof routes.home;
</satisfies_operator>
<const_type_parameters>
Preserve literals through generics (TS 5.0+):
function makeTuple<const T extends readonly unknown[]>(...args: T): T {
return args;
}
const result = makeTuple("a", "b", "c");
</const_type_parameters>
<inferred_predicates>
TS 5.5+ auto-infers type predicates:
function isString(x: unknown) {
return typeof x === "string";
}
const strings = values.filter(isString);
</inferred_predicates>
<template_literals>
Pattern matching at type level:
type Route = `/${string}`;
type ApiRoute = `/api/v${number}/${string}`;
type ExtractParams<T extends string> =
T extends `${string}:${infer P}/${infer R}`
? P | ExtractParams<`/${R}`>
: T extends `${string}:${infer P}`
? P
: never;
type Params = ExtractParams<"/user/:id/post/:postId">;
See modern-features.md for TS 5.5-5.8.
</template_literals>
Zod Validation
Schema = runtime validation + TypeScript type.
<zod_core>
import { z } from "zod";
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string().min(1).max(100),
});
type User = z.infer<typeof UserSchema>;
const result = UserSchema.safeParse(data);
if (!result.success) {
console.error(result.error.issues);
return;
}
const user = result.data;
</zod_core>
<zod_patterns>
Discriminated unions (preferred over z.union):
const ApiResponse = z.discriminatedUnion("type", [
z.object({ type: z.literal("success"), data: z.unknown() }),
z.object({ type: z.literal("error"), code: z.string(), message: z.string() }),
]);
Environment variables:
const EnvSchema = z.object({
NODE_ENV: z
.enum(["development", "production", "test"])
.default("development"),
DATABASE_URL: z.string().url(),
PORT: z.coerce.number().int().positive().default(3000),
});
const env = EnvSchema.parse(process.env);
API validation (Hono):
import { zValidator } from "@hono/zod-validator";
app.post("/users", zValidator("json", UserSchema), (c) => {
const user = c.req.valid("json");
return c.json(user);
});
See:
</zod_patterns>
Type Guards
function isString(v: unknown): v is string {
return typeof v === "string";
}
function assertString(v: unknown): asserts v is string {
if (typeof v !== "string") throw new TypeError("Expected string");
}
const users: User[] = getUsers();
const first = users[0];
if (first !== undefined) processUser(first);
See advanced-types.md for utilities.
TSDoc
Types show structure. TSDoc shows intent. Critical for AI agents.
export async function authenticate(
credentials: Credentials
): Promise<SessionToken>;
Document: all exports, parameters with constraints, thrown errors, non-obvious returns.
See tsdoc-patterns.md for comprehensive guide.
ALWAYS:
- Strict TypeScript config enabled
- Type-only imports:
import type { User } from './types'
- Const assertions for literal types
- Exhaustive matching with
assertNever
- Runtime validation at boundaries (Zod)
- Branded types for domain/sensitive data
- Result types for error-prone operations
satisfies for literal inference
using for resources with cleanup
- TSDoc on all exports
NEVER:
any (use unknown + guards)
@ts-ignore (fix types or document)
- TypeScript enums (use const assertions or z.enum)
- Non-null assertions
! (use guards)
- Loose state (use discriminated unions)
- Hidden errors (use Result)
PREFER:
- safeParse over parse
- z.discriminatedUnion over z.union
- Inferred predicates (TS 5.5+)
- Const type parameters for literals
Type Patterns:
Modern Features:
Zod:
TSDoc:
Examples: