"Review this TypeScript" is the format that separates fluency from familiarity. The code compiles; the question is what will bite. This page lists the patterns that make a codebase pleasant and the smells that predict incidents, so you can name them quickly and explain the fix.
kind property drives exhaustive switches and narrows in templates.const ROLE = { admin: 'admin', editor: 'editor' } as const; type Role = typeof ROLE[keyof typeof ROLE]; zero runtime, erasable, iterable (Object.values(ROLE)).keyof typeof, typeof x[number], ReturnType<typeof createStore>, z.infer; types written once, in one direction.Result and typed errors for expected failures; exceptions for the unexpected; never for exhaustiveness.Parameters, ReturnType, Awaited, NoInfer, satisfies for tables and configs, labelled tuples for emits and callbacks.isolatedDeclarations), internal helpers infer; interfaces for object shapes that may be extended, type aliases for unions and utilities.readonly T[]), which also documents intent.expectTypeOf (Vitest) or // @ts-expect-error lines next to the code, so a typing regression fails CI.@typescript-eslint with strict-type-checked: no-explicit-any, no-unsafe-*, no-floating-promises, no-non-null-assertion, consistent-type-imports, switch-exhaustiveness-check.as casts on data you did not construct (as Product, as HTMLInputElement without checking, as any, as unknown as T): each one is a place where the type and the runtime can disagree silently. Replace with guards, schemas, or satisfies.!) sprinkled to silence strictNullChecks: a crash waiting for the one path where the value is null. Narrow explicitly or restructure so the value cannot be missing.any leakage: an any return from a helper (JSON.parse, an untyped SDK, a catch (e: any)) spreads into everything downstream. Wrap the seam in a typed function that returns unknown or a validated type.{ loading?: boolean, error?: string, data?: T } allows eight combinations, four of them impossible. Discriminated union.variant: string, event: string. Literal unions or template literal types.deepMerge<T, U, V, W> with conditional types nobody can read, or a component with <T, K extends keyof T, F extends (x: T[K]) => unknown> for one caller. Simplify or specialise.Function, object, {} as types: Function accepts anything callable and disables call checking; {} means "any non-nullish". Use precise signatures, Record<string, unknown>, or unknown.tsc that takes minutes. Diagnose with --extendedDiagnostics/--generateTrace, move complexity into libraries or replace with runtime code.@ts-ignore instead of @ts-expect-error: the former hides the error forever, the latter fails when the error disappears.A review exercise: the same module before and after.
export async function loadUser(id: any) {
const res: any = await $fetch('/api/users/' + id)
const user = res.data as { name: string, email: string, role: string }
if (user.role == 'admin' || user.role == 'editor') {
localStorage.setItem('user', JSON.stringify(user))
}
return { loading: false, error: undefined, data: user, isAdmin: user.role === 'admin' }
}
export function greet(user: any) {
return 'Hello ' + user!.name.toUpperCase()
}
import { z } from 'zod'
const ROLE = { admin: 'admin', editor: 'editor', viewer: 'viewer' } as const
type Role = typeof ROLE[keyof typeof ROLE]
const userSchema = z.object({ name: z.string(), email: z.string().email(), role: z.enum([ROLE.admin, ROLE.editor, ROLE.viewer]) })
type User = z.infer<typeof userSchema>
type UserId = string & { readonly __brand: 'UserId' }
type LoadState = { status: 'loading' } | { status: 'error', message: string } | { status: 'success', user: User }
export async function loadUser(id: UserId): Promise<LoadState> {
try {
const raw: unknown = await $fetch(`/api/users/${id}`) // template literal keeps the route type; `unknown` is honest for the demo
const user = userSchema.parse(raw) // validated once, at the boundary
if (canEdit(user.role)) persistUser(user)
return { status: 'success', user }
}
catch (e) {
return { status: 'error', message: e instanceof Error ? e.message : 'Unknown error' }
}
}
const EDITOR_ROLES = new Set<Role>([ROLE.admin, ROLE.editor])
const canEdit = (role: Role) => EDITOR_ROLES.has(role)
export function greet(user: Pick<User, 'name'>): string {
return `Hello ${user.name.toUpperCase()}`
}
Type-level tests that guard the contract:
import { expectTypeOf } from 'vitest'
import type { LoadState } from '~/utils/user'
expectTypeOf<Extract<LoadState, { status: 'success' }>['user']['role']>().toEqualTypeOf<'admin' | 'editor' | 'viewer'>()
// @ts-expect-error a plain string is not a UserId: the brand is enforced
loadUser('123')
When the compiler complains, the quickest fix is often a cast or a !, and it is usually wrong: the complaint is telling you about a runtime path you have not handled. Treat every cast in a diff as a question in review: "what makes this true?"
expectTypeOf(x).toMatchTypeOf<Foo>() passes for anything assignable to Foo, including wider types; toEqualTypeOf is the strict check. And // @ts-expect-error without a following expression is itself an error, which is the point.
grep -rn " as \| as any\|!\." src on a project and classify each hit: boundary cast, laziness, or genuinely unavoidable. Fix the laziness ones.@typescript-eslint/strict-type-checked on one directory and fix its findings; note which rules found real bugs.expectTypeOf and a @ts-expect-error for a wrong prop; make both run in CI."Places where the type and the runtime can disagree: as casts on data the code did not construct, non-null assertions that silence a real null path, any leaking out of a helper or a catch, and DTO interfaces written by hand next to a schema. Then modelling: booleans and optionals that allow impossible states where a discriminated union would not, string where a literal union documents the options, Function or {} as types. Then proportion: a generic helper with four type parameters for one caller, or a recursive conditional type in application code that slows the editor. My fixes are boring on purpose: guards and schemas at boundaries, derive types from values, keep public signatures explicit, and let the strict lint rules enforce it so review does not have to."
Nothing yet. Notes filed here show up on this page and the section overview.
Runtime validation and type safety
Types are erased, so safety comes from validating at boundaries — schemas with zod, valibot and ArkType under Standard Schema, deriving types from schemas, sharing them between Nuxt server and client, branded types, exhaustive checks, typed errors and Result types.
CSS, layout & accessibility
Why senior frontend interviews still test CSS and accessibility, the 2026 platform baseline, and how the pages on the cascade and modern layout, CSS architecture with Tailwind v4, responsive design, WCAG 2.2 and accessible components in Vue fit together.