standards-typescript/SKILL.md
TypeScript strict-mode best practices. Auto-load when editing *.ts or *.tsx files. Covers type safety patterns, Zod integration, Prisma types, and React typing conventions.
npx skillsauth add paulund/skills standards-typescriptInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
tsconfig.json (no "strict": false, no loosening noImplicitAny)any — use unknown and narrow it, or find the actual type@ts-ignore — use @ts-expect-error with a comment justifying why, if truly unavoidableFunction, Object, or {} as types — use specific signatures/interfaces// BAD
function run(fn: Function) { fn() }
const data: Object = {}
// GOOD
function run(fn: () => void) { fn() }
const data: Record<string, unknown> = {}
| Use interface for... | Use type for... |
| ------------------------------------------------------------------- | ------------------------------- |
| Object shapes (component props, API response shapes, domain models) | Unions and intersections |
| When you want extendability (declaration merging) | Mapped types, conditional types |
| | Primitive aliases |
interface Project { id: string; name: string; createdAt: Date } // Object shape
type PostStatus = 'draft' | 'scheduled' | 'posted' // Union
type AdminUser = User & { role: 'admin' } // Intersection
Prefer string literal unions — simpler, tree-shakeable, and work well with Prisma/Zod.
// BAD
enum PostStatus { Draft = 'draft' }
// GOOD
type PostStatus = 'draft' | 'scheduled' | 'posted' | 'failed'
as const for Config Objectsconst PLATFORMS = ['x', 'linkedin', 'substack'] as const
type Platform = (typeof PLATFORMS)[number] // 'x' | 'linkedin' | 'substack'
type PostState =
| { status: 'draft'; content: string }
| { status: 'scheduled'; content: string; scheduledAt: Date }
| { status: 'posted'; content: string; postedAt: Date; url: string }
function handlePost(post: PostState) {
if (post.status === 'posted') {
console.log(post.url) // TypeScript knows url exists here
}
}
satisfies Instead of Type Assertions// BAD — loses inference
const config = { provider: 'anthropic', model: 'llama-3' } as AIConfig
// GOOD — validated AND inferred
const config = { provider: 'anthropic', model: 'llama-3' } satisfies AIConfig
Note: use Record<string, string> rather than satisfies for lookup tables indexed at runtime — satisfies narrows to a literal type that rejects arbitrary string keys.
Always derive TypeScript types from Zod schemas. Don't declare the same shape twice.
export const createPostSchema = z.object({
content: z.string().min(1).max(280),
projectId: z.string().uuid(),
scheduledAt: z.string().datetime().optional(),
})
export type CreatePostInput = z.infer<typeof createPostSchema>
Use Prisma.<Model>GetPayload<...> for query result shapes. Never re-declare them.
import type { Prisma } from '@prisma/client'
type Post = Prisma.PostGetPayload<Record<string, never>>
type PostWithProject = Prisma.PostGetPayload<{ include: { project: true } }>
Always type the resolved value. Never return Promise<any>.
async function getProject(id: string): Promise<Project | null> { ... }
import type { Project, Post } from '@prisma/client'
import type { CreatePostInput } from '@/lib/schemas/post'
interface ButtonProps { label: string; onClick: () => void; disabled?: boolean }
interface CardProps { children: React.ReactNode; className?: string }
function handleChange(e: React.ChangeEvent<HTMLInputElement>) { setValue(e.target.value) }
const inputRef = useRef<HTMLInputElement>(null)
function handleError(error: unknown) {
if (error instanceof Error) {
console.error(error.message)
} else if (typeof error === 'string') {
console.error(error)
} else {
console.error('Unknown error', error)
}
}
| Utility | Use |
| ---------------- | ------------------------------------ |
| Partial<T> | All properties optional |
| Required<T> | All properties required |
| Pick<T, K> | Keep only keys K |
| Omit<T, K> | Remove keys K |
| Record<K, V> | Object with key type K, value type V |
| ReturnType<F> | Return type of a function |
| Awaited<P> | Unwrap a Promise type |
| NonNullable<T> | Remove null and undefined |
When the same constant (lookup table, config object, union type) appears in more than one file, extract it to src/lib/ and import it. Client components can import shared constants directly from src/lib/ without receiving them as props.
// BAD — same map declared in multiple files
const statusBadge: Record<string, string> = { draft: 'bg-gray-100 text-gray-700' }
// GOOD — one source of truth in src/lib/status-badges.ts
export const postStatusBadge: Record<string, string> = { draft: 'bg-gray-100 text-gray-700' }
development
Use when implementing any logic, fixing any bug, or changing any behaviour. Use when you need to prove code works, when a bug report arrives, or when modifying existing functionality. Do NOT use for config changes, data migrations, or dependency updates.
development
Use when starting a new feature, when requirements are unclear, when asked to write code without a clear spec, or before any non-trivial implementation. Do NOT use for trivial bug fixes or one-line changes.
development
Use when you want authoritative, source-cited code free from outdated patterns. Use when building with any framework or library where correctness matters. Detects the stack from dependency files, fetches official documentation, implements following documented patterns, and cites sources for every framework-specific decision.
development
Use when preparing to ship a feature, release, or deployment. Use before merging to main, creating a release, or deploying to production. Do NOT use for CI-only changes or internal refactors that don't reach production.