name: typescript-write description: Write TypeScript and JavaScript code following Metabase coding standards and best practices. Use when developing or refactoring TypeScript/JavaScript code.
TypeScript/JavaScript Development Skill
@./../_shared/development-workflow.md @./../_shared/typescript-commands.md @./../_shared/react-redux-patterns.md
No any — hard rule
- New code must not introduce
any, explicit or implicit. Noanyannotations, noas any/as unknown as, no untyped parameters or returns that inferany, no implicitly-anydestructures or array/object literals. - Untyped third-party / boundary values must be typed at the boundary (a declared type,
unknown+ type guard, or a small typed wrapper) — never letanypropagate inward. - Mandatory LSP verification. After writing or editing any TS/TSX, inspect the changed symbols with the TypeScript Language Server (hover to read inferred types;
goToDefinitionto confirm sources) and run the project type-check.
Type tightening
- Avoid type casts and loose
unknown— fix the signature instead. - If a function only needs one field of a wide object, accept that field — not the wide object. The cast often disappears once the signature is right.
- Reach for
Partial<T>,Pick<T, K>,Record<K, V>, and generics before reaching for a cast. - Prefer making props/components generic (
<T>) when a value flows through unchanged and the caller knows the type. - Prefer
unknownover loose typing and narrow before use — anunknownvalue forces a guard at the point of use. satisfiesfor object literals that must conform without widening (config objects, lookup maps, discriminated literals) — better than: T(widens) oras T(unsafe).- Avoid non-null assertions (
!). Prefer a guard, early return, or?.. Use!only when non-nullness is provably true and localized, with a comment. - No redundant runtime coercion — don't wrap already-typed values in
Number()/String()/Boolean(). - Type guards belong in
frontend/src/metabase-types/guards/. Do not redefine them locally.
Type modeling
- Reuse existing types; don't re-declare them. Use canonical IDs and domain entity types from
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 accessSomeType["field"],ReturnType). - Use generics to allow TypeScript to infer correct types when creating functions and components that need to be reusable and type-safe. Don't hesitate to introduce complex generics if they allow to derive types automatically instead of manual narrowing.
- Model the actual data contract; keep types narrow. Optional
field?: Tfor a key that may be absent,field: T | undefinedonly when the key is always present but the value may be undefined,| nullfor explicit API nulls. Prefer domain unions over broadstring/number/ looseRecord. - Refer to API implementation when defining or refining types to ensure they match the actual data structure. When considering a type cast, first consider if the type should be refined to match the actual data structure.
- Discriminated unions for variant state, with exhaustive checks. Model "one of N shapes" as a union with a literal discriminant rather than a bag of optional fields, and exhaust it with ts-pattern's
.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 - Derive union types from constants (
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. Preferreadonly T[]/ReadonlyArray<T>for inputs you don't mutate.- Type async and error states explicitly (a discriminated union or the data-layer's typed result) — never leave loading/error/empty implicit.
Null and undefined
- Narrow at the source. If a value is optional only in a corner case, don't thread
undefinedthrough every layer — guard at the producer. - Sensible defaults for optional values. Use
?.and??at the consumer. - Lists should be filtered before being used in a map or other iteration.
- Avoid non-strict null comparisons (
X != null) whenXcan never benull— use a strict check or narrow the type. UsecheckNotNullwhere necessary. - Check actual nullability against API implementation. Find the API endpoint implementation and check if the field can actually be null.
Naming
- Names describe the entity, not the mechanism. A name must reflect what the value holds.
- Align sibling concepts: keep verb conventions consistent across a related API.
- No names that encode implementation history rather than current meaning. Suffixes like
Base,New,Old,Initialneed a real semantic distinction, otherwise drop them. - Avoid cryptic identifiers (
v,n,$n) for domain values; short names are fine only in tiny conventional contexts (loop indexi, coordinatesx/y, generic paramsT/K/V).
Code structure and organization
- Prioritize reusability over duplication. The codebase already has many utility functions — leverage them. If you introduce duplicated logic, extract it to a shared utility.
- Generic helpers do not belong in feature folders. Promote to a shared utility.
- Keep functions small and single-purpose. A 100+ line function is hard to review — split into focused named helpers, each with one responsibility and a minimal dependency surface. When necessary, cover with unit tests.
- Extract distinct complex JSX into named components. Choose same-file vs separate-file by reuse, coupling, testability, and readability.
Comments
- No comments by default. Well-named identifiers carry the
what. - Comments should be concise. Add a short, concise comment only when the
whyis 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.
TypeScript Migration
When touching existing JavaScript files, propose to convert them to TypeScript first. Create a separate PR for the conversion, then implement the changes.
Verify before done
- Run the project type-check when finished (see the shared TypeScript commands above).