Write TypeScript and JavaScript code following Metabase coding standards and best practices. Use when developing or refactoring TypeScript/JavaScript code.
@./../_shared/development-workflow.md @./../_shared/typescript-commands.md @./../_shared/react-redux-patterns.md
any — hard ruleany, explicit or implicit. No any annotations, no as any / as unknown as, no untyped parameters or returns that infer any, no implicitly-any destructures or array/object literals.unknown + type guard, or a small typed wrapper) — never let any propagate inward.bun run type-check-pure. If TypeScript LSP tools are available, also inspect changed symbols with hover and go-to-definition; otherwise skip LSP check.unknown — fix the signature instead.Partial<T>, Pick<T, K>, Record<K, V>, and generics before reaching for a cast.<T>) when a value flows through unchanged and the caller knows the type.unknown over loose typing and narrow before use — an unknown value forces a guard at the point of use.satisfies for object literals that must conform without widening (config objects, lookup maps, discriminated literals) — better than : T (widens) or as T (unsafe).!). Prefer a guard, early return, or ?.. Use ! only when non-nullness is provably true and localized, with a comment.Number() / String() / Boolean().frontend/src/metabase-types/guards/. Do not redefine them locally.metabase/no-unjustified-type-casts rule accepts any preceding comment — state the actual reason the cast is safe. NEVER write the legacy // Unjustified type cast. FIXME placeholder; it exists only on casts that predated the rule, and copying it sneaks an unjustified cast past the linter. If you can't articulate why the cast is correct, the cast is wrong — fix the types.metabase-types/api (FieldId, TableId, ConcreteTableId, SchemaName, …) and key data structures by them (new Map<ConcreteTableId, …>()). Don't duplicate generated/API types — compose or derive (Pick, Omit, indexed access SomeType["field"], ReturnType).field?: T for a key that may be absent, field: T | undefined only when the key is always present but the value may be undefined, | null for explicit API nulls. Prefer domain unions over broad string / number / loose Record..exhaustive() so adding a variant becomes a compile error:import { match } from "ts-pattern";
const result = match(status)
.with({ type: "loading" }, () => <Spinner />)
.with({ type: "error", error: P.select() }, (error) => <Error message={error.message} />)
.with({ type: "success", data: P.select() }, (data) => <Content data={data} />)
.exhaustive(); // Compile-time guarantee all cases handled
as const + typeof/keyof) so the type and the values can't drift.readonly / immutability where mutation isn't intended — component props, shared constants, exported config. Prefer readonly T[] / ReadonlyArray<T> for inputs you don't mutate.undefined through every layer — guard at the producer.?. and ?? at the consumer.X != null) when X can never be null — use a strict check or narrow the type. Use checkNotNull where necessary.Base, New, Old, Initial need a real semantic distinction, otherwise drop them.v, n, $n) for domain values; short names are fine only in tiny conventional contexts (loop index i, coordinates x/y, generic params T/K/V).what.why is non-obvious: a workaround, a hidden invariant, a subtle ordering constraint, a clever reduction. Never document the actual implementation, focus on the intent and the why.