ワンクリックで
typescript
TypeScript-specific coding conventions and type system patterns. Always load this skill when writing or reviewing TypeScript code.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
TypeScript-specific coding conventions and type system patterns. Always load this skill when writing or reviewing TypeScript code.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
Implement a feature or fix based on a Linear issue
Create well-structured Linear issues, projects, project updates, or initiatives
Validate a filed bug against the source before fixing it — verify every claim, hunt the broader class, find the contract question, and scope the outcome
Disciplined per-commit workflow with Greybeard review, build gates, and Critique loops
Engineering philosophy and work culture principles. Load this skill when making architectural decisions or to understand the team's work principles.
General coding conventions for clean, maintainable code. Always load this skill when writing or reviewing code in any language.
| name | typescript |
| description | TypeScript-specific coding conventions and type system patterns. Always load this skill when writing or reviewing TypeScript code. |
TypeScript-specific guidelines for type safety and code organization.
import type for type-only imports{ cause } when re-throwing errorscreate* prefixnull from handlers when request doesn't matchconsole.logany type (use unknown and narrow)as Type) - they indicate interface problemsx!) - they hide nullability bugs| Type | Convention | Example |
|---|---|---|
| Regular modules | Lowercase, hyphens for multi-word | token-payment.ts, server.ts |
| Single-word modules | Lowercase | cache.ts, common.ts |
| Test files | {name}.test.ts | cache.test.ts |
| Pattern | Use Case | Example |
|---|---|---|
PascalCase | Interfaces, type aliases | PaymentHandler, RequestConfig |
*Args / *Opts | Function arguments | CreateHandlerOpts |
*Response | API responses | SettleResponse |
*Info | Data structures | ChainInfo, TokenInfo |
*Handler | Handler interfaces | PaymentHandler |
| Pattern | Use Case | Example |
|---|---|---|
camelCase | All functions | handleRequest |
create* | Factory functions | createHandler, createClient |
is* | Boolean predicates | isValidationError, isKnownType |
get* | Retrieval without side effects | getBalance, getConfig |
lookup* | Search/lookup operations | lookupToken, lookupNetwork |
generate* | Builder/generator functions | generateMatcher, generateConfig |
handle* | Event/request handlers | handleSettle, handleVerify |
| Pattern | Use Case | Example |
|---|---|---|
camelCase | Regular variables | paymentResponse, blockNumber |
SCREAMING_SNAKE_CASE | Constants, environment vars | API_BASE_URL, MAX_RETRIES |
_ prefix | Unused parameters | _ctx, _unused |
Acronyms are not words. Do not conform them to camelCase or PascalCase word boundaries. Preserve the acronym's natural capitalization:
// Good - types preserve acronyms
type JSONSchema = { ... }
type HTTPResponse = { ... }
type APIClient = { ... }
type XMLParser = { ... }
// Bad - don't camelCase acronyms in types
type JsonSchema = { ... } // Should be JSONSchema
type HttpResponse = { ... } // Should be HTTPResponse
type ApiClient = { ... } // Should be APIClient
// Good - functions and variables preserve acronyms too
getURLFromRequest
requestURL
parseHTTPHeaders
parseJSON
// Bad
getUrlFromRequest // Should be getURLFromRequest
requestUrl // Should be requestURL
parseJson // Should be parseJSON
Common acronyms: URL, HTTP, HTTPS, JSON, API, RPC, HTML, XML
Note: "ID" is an abbreviation, not an acronym, so use standard camelCase: userId, requestId, getId().
Use a validation library (e.g., arktype, zod, typebox) for runtime type validation. Define the validator and TypeScript type together:
import { type } from "arktype";
// Define runtime validator
export const PaymentRequest = type({
scheme: "string",
network: "string",
amount: "string.numeric",
resource: "string.url",
});
// Derive TypeScript type from validator
export type PaymentRequest = typeof PaymentRequest.infer;
If no existing validation library is installed, install arktype and use it.
This pattern should be used for all external data: API responses from fetch, file system reads, environment variables, user input, and third-party API responses.
Create type guards using validation functions:
export function isAddress(maybe: unknown): maybe is Address {
return !isValidationError(Address(maybe));
}
export function isKnownNetwork(n: string): n is KnownNetwork {
return knownNetworks.includes(n as KnownNetwork);
}
type: Use for data structures, unions, and validator-derived typesinterface: Use for behavioral contracts (objects with methods)// Type for data structure
export type RequestContext = {
request: RequestInfo | URL;
};
// Interface for behavioral contract
export interface PaymentHandler {
getSupported?: () => Promise<SupportedKind>[];
handleSettle: (requirements, payment) => Promise<SettleResponse | null>;
}
Use as const for exhaustive literal types:
const PaymentMode = {
Direct: "direct",
Deferred: "deferred",
} as const;
type PaymentMode = (typeof PaymentMode)[keyof typeof PaymentMode];
// TypeScript ensures all cases handled in switch
switch (mode) {
case PaymentMode.Direct:
// ...
break;
case PaymentMode.Deferred:
// ...
break;
}
Use import type for type-only imports:
import type { PaymentRequest } from "./types";
import type { Hex, Account } from "viem";
// Mixed imports
import {
type Transaction,
createTransaction, // value import
} from "./transactions";
Let TypeScript infer types when obvious:
// Good - return type is obvious
const createHandler = async (network: string) => {
const config = { network, enabled: true };
return {
getConfig: () => config,
isEnabled: () => config.enabled,
};
};
// Unnecessary - the return type is obvious
const createHandler = async (network: string): Promise<{
getConfig: () => { network: string; enabled: boolean };
isEnabled: () => boolean;
}> => { ... };
When to add explicit types:
When NOT to add explicit types:
any and Type AssertionsType assertions (as Type) only affect compile-time types. They provide zero runtime safety. A type assertion tells TypeScript "trust me, this is the shape" but does nothing at runtime.
This is especially critical for external data. Data from fetch, the filesystem, environment variables, user input, and third-party APIs always needs runtime validation because:
Use unknown instead of any when the type is truly unknown, then narrow with validation:
// Bad
function processData(data: any) {
return data.value;
}
// Good
function processData(data: unknown) {
const validated = MyDataType(data);
if (isValidationError(validated)) {
throw new Error(`Invalid data: ${validated.summary}`);
}
return validated.value;
}
Type assertions bypass type checking and often indicate interface problems. Prefer runtime validation:
// Bad
const data = (await response.json()) as UserData;
// Good
const raw = await response.json();
const data = UserData(raw);
if (isValidationError(data)) {
throw new Error(`Invalid response: ${data.summary}`);
}
The non-null assertion operator (x!) has the same problem as as Type: it's a compile-time lie. It tells TypeScript "trust me, this isn't null or undefined" when the compiler thinks it could be. If the compiler thinks a value might be null, there's usually a reason.
Instead of silencing the compiler, restructure the code so the value is provably non-null:
// Bad - hiding a potential bug
const user = users.find(u => u.id === id)!;
processUser(user);
// Good - handle the null case
const user = users.find(u => u.id === id);
if (!user) {
throw new Error(`User not found: ${id}`);
}
processUser(user);
// Bad - asserting map result exists
const handler = handlers.get(name)!;
// Good - check and provide a meaningful error
const handler = handlers.get(name);
if (!handler) {
throw new Error(`No handler registered for: ${name}`);
}
If you find yourself reaching for !, it means one of:
T | null when it shouldn't (fix the upstream function)Prefer generic type parameters with constraints over index signatures:
// Bad - index signature (too permissive)
export interface LoggingBackend {
configureApp(args: {
level: LogLevel;
[key: string]: unknown;
}): Promise<void>;
}
// Good - generic with constraint (type-safe)
export type BaseConfigArgs = { level: LogLevel };
export interface LoggingBackend<TConfig extends BaseConfigArgs = BaseConfigArgs> {
configureApp(args: TConfig): Promise<void>;
}
Use index.ts files to re-export from modules:
// packages/types/src/index.ts
// Namespaced exports for grouped functionality
export * as payments from "./payments";
export * as client from "./client";
// Flat exports for utilities
export * from "./validation";
export * from "./helpers";
// Good
export function createMiddleware(args: CreateMiddlewareArgs) { ... }
export const MAX_RETRIES = 3;
// Avoid
export default function createMiddleware(args: CreateMiddlewareArgs) { ... }
Order imports by category:
// External libraries
import { type } from "arktype";
import { Hono } from "hono";
// Internal packages
import { isValidationError } from "@myorg/types";
import type { Handler } from "@myorg/types/handler";
// Relative imports
import { isValidTransaction } from "./verify";
import { logger } from "./logger";
Omit file extensions in import paths when the module resolver can infer them:
// Good - no extension needed
import { createHandler } from "./handler";
import type { Config } from "../types";
// Bad - unnecessary extension
import { createHandler } from "./handler.ts";
import type { Config } from "../types.ts";
Note: Some environments (like Deno or Node.js with "type": "module") require explicit extensions. Follow project conventions when extensions are mandated by the runtime.
Dynamic import() expressions should be used sparingly. They exist for genuinely dynamic scenarios where the module to load is not known at authoring time (e.g., plugin systems where the module path is constructed from a variable) or where a module must be conditionally loaded at runtime (e.g., optional dependencies that may not be installed).
If you know which module you need, use a static import at the top of the file. Do not use await import() inline next to your code change because it is convenient — that is a static dependency with worse type safety and unnecessary indirection. Add the import statement to the top of the file where it belongs.
// Bad - lazy inline import of a known module
const { createHandler } = await import("./handler");
// Good - static import at the top of the file
import { createHandler } from "./handler";
// Good - genuinely dynamic: the module path is not known at authoring time
const plugin = await import(`./plugins/${pluginName}`);
// Good - conditional loading of an optional dependency
let sharp: typeof import("sharp") | undefined;
try {
sharp = await import("sharp");
} catch (err) {
logger.warn("sharp not installed, falling back to basic image handling", { cause: err });
}
Use async factory functions that return objects with async methods:
const createHandler = async (network: string, rpc: RpcClient, config?: HandlerOptions) => {
// Async initialization
const networkInfo = await fetchNetworkInfo(rpc);
// Return object with async methods
return {
getSupported,
handleVerify,
handleSettle,
};
};
Use Promise.all for independent parallel operations:
const [tokenName, tokenVersion] = await Promise.all([
client.readContract({ functionName: "name" }),
client.readContract({ functionName: "version" }),
]);
Use Promise.race for operations that need timeouts:
function timeout(timeoutMs: number, msg?: string) {
return new Promise((_, reject) =>
setTimeout(() => reject(new Error(msg ?? "timed out")), timeoutMs),
);
}
const result = await Promise.race([
fetchData(),
timeout(5000, "fetch timed out"),
]);
Implement retries with exponential backoff:
let attempt = (options.retryCount ?? 2) + 1;
let backoff = options.initialRetryDelay ?? 100;
let response;
do {
response = await makeRequest();
if (response.ok) {
return response;
}
await new Promise((resolve) => setTimeout(resolve, backoff));
backoff *= 2;
} while (--attempt > 0);
Check validation errors before proceeding:
const payload = parsePayload(input);
if (isValidationError(payload)) {
logger.debug(`couldn't validate payload: ${payload.summary}`);
return sendBadRequest();
}
// payload is now typed correctly
Create local helpers for consistent error responses:
const handleSettle = async (requirements, payment) => {
const errorResponse = (msg: string): SettleResponse => {
logger.error(msg);
return {
success: false,
error: msg,
txHash: null,
};
};
if (someConditionFails) {
return errorResponse("Invalid transaction");
}
// ...
};
Use { cause } when re-throwing errors:
try {
transaction = parseTransaction(input);
} catch (cause) {
throw new Error("Failed to parse transaction", { cause });
}
null for "Not My Responsibility"Handlers should return null when a request doesn't match their criteria:
const handleVerify = async (requirements, payment) => {
if (!isMatchingRequirement(requirements)) {
return null; // Let another handler try
}
// Handle the request...
};
Focus test coverage on logic specific to your codebase:
Do not write tests that merely verify functionality provided by external libraries. Trust well-maintained libraries to do their job.
import t from "tap";
await t.test("descriptiveTestName", async (t) => {
// Setup
const cache = new Cache({ capacity: 3 });
// Assertions
t.equal(cache.size, 0);
t.matchOnly(cache.get("key"), undefined);
t.end();
});
Inject time functions for deterministic time-based tests:
let theTime = 0;
const now = () => theTime;
const cache = new Cache({
maxAge: 1000,
now, // Inject time function
});
theTime += 500;
t.matchOnly(cache.get("key"), 42); // Still valid
theTime += 1000;
t.matchOnly(cache.get("key"), undefined); // Expired
Document public APIs with TSDoc:
/**
* Creates a handler for the payment scheme.
*
* @param network - The network identifier (e.g., "mainnet", "testnet")
* @param rpc - RPC client
* @param config - Optional configuration options
* @returns Promise resolving to a Handler
*/
export const createHandler = async (
network: string,
rpc: RpcClient,
config?: HandlerOptions,
): Promise<Handler> => { ... };