| name | typescript-standards |
| description | TypeScript coding standards. Use when writing TypeScript, reviewing code, or refactoring. Enforces named exports, no dynamic imports, discriminated unions, proper type safety. |
| allowed-tools | ["Read","Edit","Grep","Glob"] |
TypeScript Standards
Named Exports Only
export default function myFunction() {}
export function myFunction() {}
Package Exports (Monorepo)
Only export what other packages actually consume:
export * from "./digest";
export {
fetchCompoundDigest,
buildCompoundDigestBlocks,
type CompoundDigestEnv,
} from "./digest";
Before adding exports, check what's actually imported by consumer packages.
Import Patterns
Prefer Static Imports
Always use static imports at the top of the file. Only use dynamic imports (await import()) when:
- Conditional loading is required (feature flags, environment checks)
- Code splitting is intentional for performance
- Circular dependency breaking is necessary
Never use dynamic imports just to import something mid-function when a static import would work.
Type-Only Imports
For values used only as types, use explicit type-only imports:
import { MyClass } from './my-class'
type Instance = MyClass
import { type MyClass } from './my-class'
type Instance = MyClass
Naming Conventions
- Files: kebab-case (
my-component.ts)
- Variables/functions: camelCase (
myVariable, myFunction())
- Classes/types/interfaces: PascalCase (
MyClass, MyInterface)
- Constants/enum values: ALL_CAPS (
MAX_COUNT)
- Generic type parameters: Prefix with
T (TKey, TValue)
Type Safety Rules
Derive Types from Zod Schemas
When a Zod schema exists, always derive the TypeScript type from it using z.infer. Never define a separate interface that duplicates the schema's shape:
export interface User {
id: string
name: string
email: string
}
import { type User } from "@rudel/api-routes"
import { type User as UserBase } from "@rudel/api-routes"
export interface User extends UserBase {
session_count: number
}
If the type needs fields beyond the schema, extend the inferred type rather than redefining it.
No Type Casts
Never use as or as unknown as type assertions. They hide type incompatibilities:
const env = context.env as unknown as ExecutorEnv
const env: ExecutorEnv = context.env
function isExecutorEnv(env: unknown): env is ExecutorEnv {
return typeof env === 'object' && env !== null && 'DB' in env
}
If you encounter code that seems to require a cast:
- Investigate why the types don't match
- Fix the source type definitions
- If copying from existing code with casts, note it needs cleanup
Prefer Discriminated Unions
type FetchingState<TData> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: TData }
| { status: 'error'; error: Error }
switch (state.status) {
case 'success':
console.log(state.data)
break
}
Optional Properties
Use sparingly. Prefer T | undefined over T? to force explicit passing:
type AuthOptions = {
userId: string | undefined
}
Prefer Interfaces for Extending
type C = A & B
interface C extends A, B {}
Don't Use Enums
Use as const objects instead:
const sizes = {
xs: 'EXTRA_SMALL',
sm: 'SMALL',
} as const
type Size = keyof typeof sizes
Installing Libraries
Never rely on training data for versions. Always install latest:
pnpm add -D @typescript-eslint/eslint-plugin
Async Operations
Use Promise.all for parallel operations:
const [users, posts] = await Promise.all([fetchUsers(), fetchPosts()])
Immutability
- Use readonly arrays
- Use const assertions
- Create new objects instead of mutating