Skip to content
Development
Skill

/typescript-write

Write TypeScript and JavaScript code following Metabase coding standards and best practices. Use when developing or refactoring TypeScript/JavaScript code.

From plugin
metabase
49k23 skills11 agents23 commands
Install
$ npx -y skills add metabase/metabase --skill typescript-write --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/typescript-write

Context preview

The summary Claude sees to decide when to auto-load this skill.

Write TypeScript and JavaScript code following Metabase coding standards and best practices. Use when developing or refactoring TypeScript/JavaScript code.

SKILL.md

typescript-write.SKILL.md
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.** 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.
  • **Untyped third-party / boundary values** must be typed at the boundary (a declared type, `unknown` + type guard, or a small typed wrapper) — never let `any` propagate inward.
  • **Mandatory type verification.** Before finishing a TS/TSX change, run `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.

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 `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).
  • **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.
  • **A cast you can't avoid needs a real justification comment.** The `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.

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 access `SomeType["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?: 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`.
  • **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. Prefer `readonly 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 `undefined` through 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`) when `X` can never be `null` — use a strict check or narrow the type. Use `checkNotNull` where 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`, `Initial` need 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 index `i`, coordinat
Read more
Ships withmetabase

Metabase is the easy, open-source way for everyone in your company to ask questions and learn from data.

Get the whole plugin