> For the complete documentation index, see [llms.txt](https://engineering.umh.app/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://engineering.umh.app/engineering/development-process/how-to-build/coding-standards/languages/typescript.md).

# TypeScript

The rules our TypeScript follows on top of Google's style guide.

Owner: Robert

Google's style guide already settles most questions about TypeScript. On top of it, we found a handful of patterns that keep our codebase easy to extend and maintain.

## Rules

* We follow [Google's TypeScript style guide](https://google.github.io/styleguide/tsguide.html). Google already settled formatting, imports, naming style, language features and type-system usage.
* The rules below add to Google's guide. Where they differ from it, the rules below apply.
* The rules apply to new and changed code. When we declare new rules, we generally don't enforce sweeping updates to our codebase, as that creates deep rabbit holes that slow down our operational velocity.

## How we use Google's style guide for TypeScript when reviewing code

When we review code, we reference sections from Google's style guide and cite them in the review comment.

We don't argue for or against the style guide during a review. We treat it as settled.

### We stay consistent within files and features

We don't have strict rules on `for…of` versus `forEach`, for named functions over arrow functions, and so on. But we make sure to pick one in our PRs, and we stick to it. For existing files, we either keep the existing pattern or refactor the file. We consider inconsistent usage of the same pattern as a violation.

### We handle errors differently from Google

Google [prefers throwing exceptions](https://google.github.io/styleguide/tsguide.html#exception-handling). We prefer the approach Go and Rust take: a function that can fail returns its error as a value, and the caller handles it. This forces a clean design from the start, because the developer has to think about possible sad paths and communicate them in the signature. We go into more detail with examples here: [Gracefully handling runtime errors with JavaScript](/engineering/development-process/how-to-build/coding-standards/languages/typescript/gracefully-handling-runtime-errors-with-javascript.md).

### Some of our rules are more specific than Google's

We don't enforce variable names, function names or return types beyond what Google asks. But in four cases we do have an opinion:

* Return types are omitted on local helpers (and if they can be obviously inferred)
* Absence is `null`
* Abbreviations are written out, including the familiar ones (within reasonable means: We keep abbreviations like API)
* A switch statement over a union leaves out the `default` group Google requires, so the compiler can report a missing case

## Control structures

### Early return statements

Prefer an `if` with an early return over a ternary or a chained boolean expression that selects a code path.

A ternary is fine for choosing between two literal values on a named boolean.

Discouraged:

```ts
const source = error instanceof AppError ? error.tag : 'unknown';
```

Preferred:

```ts
function errorSource(error: unknown) {
  if (error instanceof AppError) {
    return error.tag;
  }
  return 'unknown';
}
```

Acceptable:

```ts
const level = sessionEstablished ? 'error' : 'warning';
```

### Guard clauses

Prefer guard clauses over nested conditions. At two levels of nesting, extract a named helper. Prefer a named function over a callback defined inside another callback.

Discouraged:

```ts
function saveDraft(draft: Draft | null) {
  if (draft !== null) {
    if (draft.isDirty) {
      storage.write(draft.id, draft.body, (writeResult) => {
        if (writeResult.ok) {
          markClean(draft.id);
        }
      });
    }
  }
}
```

Preferred:

```ts
function saveDraft(draft: Draft | null) {
  if (draft === null) {
    return;
  }
  if (!draft.isDirty) {
    return;
  }
  storage.write(draft.id, draft.body, markCleanWhenWritten);
}
```

### Named intermediate results

Split a method-chain pipeline into named variables and keep the `return` to a name or one simple expression. Give two adjacent checks that look alike distinct names, such as `request.ok` and `response.ok`. A single `foo?.bar ?? fallback` is fine; write a chain of them as `if` branches.

Discouraged:

```ts
return `${Math.max(...rows.filter((row) => row.label).map((row) => row.label.length))}ch`;
```

Preferred:

```ts
const labelledRows = rows.filter((row) => row.label);
const labelLengths = labelledRows.map((row) => row.label.length);
const widestLabel = Math.max(...labelLengths);
return `${widestLabel}ch`;
```

### Immediately invoked function expressions

Prefer a named function over `(() => { ... })()`. In Svelte, use `$derived.by` for a multi-statement derivation.

Discouraged:

```ts
const width = (() => {
  if (columns.length === 0) {
    return DEFAULT_WIDTH;
  }
  return columns.length * COLUMN_WIDTH;
})();
```

Preferred:

```ts
function tableWidth(columns: Column[]) {
  if (columns.length === 0) {
    return DEFAULT_WIDTH;
  }
  return columns.length * COLUMN_WIDTH;
}

const width = tableWidth(columns);
```

### Switch statements

A `switch` where all cases have a return statement is fine. When the cases run logic of their own, such as nested `if`/`else` or helper calls, prefer `if` with early returns or one function per case. Both are easier to read and to document.

Google [requires a `default` group](https://google.github.io/styleguide/tsguide.html#switch-statements) in every `switch`. We leave it out when the `switch` covers every member of a union and each case returns. The compiler then reports a missing case as `TS2366`; an empty `default` group would report that error in every case, because it keeps the end of the function reachable.

Acceptable:

```ts
function healthBadge(category: HealthCategory): HealthBadge {
  switch (category) {
    case 'healthy':
      return { label: 'healthy', destructive: false };
    case 'degraded':
      return { label: 'attention', destructive: false };
    case 'failed':
      return { label: 'attention', destructive: true };
  }
}
```

Discouraged:

```ts
switch (category) {
  case 'healthy':
    if (instance.isPaused) {
      badge = pausedBadge(instance);
    } else {
      badge = healthyBadge(instance);
    }
    break;
  case 'degraded':
  case 'failed':
    badge = attentionBadge(instance, category === 'failed');
    if (instance.alertsMuted) {
      badge = { ...badge, muted: true };
    }
    break;
  default:
}
```

Preferred:

```ts
if (category === 'healthy') {
  return healthyOrPausedBadge(instance);
}
return attentionBadge(instance, category === 'failed');
```

### Return types

Google [leaves return-type annotations to the author](https://google.github.io/styleguide/tsguide.html#return-types). We let TypeScript infer the type of simple methods, whether they're exported or used in the same file.

Discouraged:

```ts
function isOpen(session: Session | null): boolean {
  return session !== null;
}
```

Preferred:

```ts
function isOpen(session: Session | null) {
  return session !== null;
}
```

## Error handling

### Scope of a try block

Use `try` and `catch` for a single call, and only if the failure pattern is unknown. Unpredictable methods can stem from a third-party library, a browser API, a worker, or methods like `JSON.parse` on input that wasn't produced by our own code. Keep the `try` to that one call, so each failure reaches its own handler. When a feature must not break the rest of the page, narrow the `try` to the call that can fail for outside reasons.

### Results over exceptions

Where applicable, we encourage typed results instead of throwing errors.

`{ ok: true, value }` or `{ ok: false, error }`

Typed results allow the caller (and the caller's caller, etc) to decide on their own what to do when a function fails to do its promised job. In addition, use `tryAwait` for promises.

### Error telemetry

If a function is not meant to act on a failure, it should return the failure to its caller. The call site that can act on it, usually the page or the event handler that started the work, reports it once through `telemetry.reportError` with a generated identifier and shows the user one message.

## Naming

### Abbreviations

Coming from Go, you might be familiar with `ctx` and `err` - that's discouraged in TypeScript codebases. Google [allows familiar abbreviations](https://google.github.io/styleguide/tsguide.html#descriptive-names), but we're a bit stricter, because JS's huge variety of libraries allows for misunderstandings when multiple libs use the same abbreviations. `i` is fine as a trivial loop counter.

Discouraged: `err`, `ctx`, `res`, `req`, `doc`, `el`, `tmp`, `arr`

Preferred: `error`, `context`, `response`, `request`, `parsedFeedDocument`, `element`

### Units and origins in names

When declaring unit- or origin-based variables, add the unit or origin to it.

Discouraged: `timeout`, `xml`, `Result`

Preferred: `timeoutSeconds`, `rssXmlText`, `TryAwaitResult`

### Vendor-neutral identifiers

Unless directly using a vendor's library, avoid mentioning vendors in wrappers and code. This way we ensure that we don't need big mechanical updates when changing vendors.

Discouraged: `sentryTag`, `Sentry.captureException(error)` in feature code

Preferred: `errorTag`, `telemetry.reportError(...)`

### Exported names

Exported names should be descriptive, since they show up in a developer's IDE's autocomplete without much context, when calling them from other files.

Discouraged: `isWarningSample`, `StatusIndicator`

Preferred: `isLatencyWarningSample`, `InstanceStatusIndicator`

### Stay true to the definition of an `event`

In frontend code, `event` is a reserved word and **only** used for [a DOM Event](https://developer.mozilla.org/en-US/docs/Web/API/Event).

Discouraged: `LatencySnapshotEvent` for an analytics payload

Preferred: `LatencySnapshotPayload`

## Type system

### Absence is null (almost always)

Google [allows either `null` or `undefined`](https://google.github.io/styleguide/tsguide.html#undefined-and-null).

We use `null`: a function returns `T | null`, and a required prop is typed `T | null`.

JavaScript produces `undefined` on its own, from an uninitialized field, a missing `return` or an out-of-range index, so a `null` is always deliberate. `undefined` is not.

An optional field or parameter (`name?:`) is an example of where we accept `undefined`.

Discouraged:

```ts
function findSession(id: string): Session | undefined
let tooltip: HealthBadgeTooltip | undefined;
```

Preferred:

```ts
function findSession(id: string): Session | null
let tooltip: HealthBadgeTooltip | null = null;
```

Acceptable:

```ts
interface ProfileCardProps {
  userId: string;
  subtitle?: string;   // string | undefined, because the caller may leave it out
}
```

### One declaration per shape

Declare a shape once and import it wherever it is used. One declaration is easier to understand than two that extend each other, and the type annotations stay flat, which keeps them readable.

This becomes acceptable when extending existing classes from Browser APIs and other libraries.

Discouraged:

```ts
interface HealthBadgeInput { desiredState?: string }
interface HealthProp extends HealthBadgeInput { desiredState?: ProtocolConverterState }

if (health.desiredState === 'stopped') { ... }
```

Preferred:

```ts
interface HealthBadgeInput { desiredState: ProtocolConverterState | null }

if (health.desiredState === ProtocolConverterState.Stopped) { ... }
```

### Domain types

Type a value as the set of values it can take. The compiler then rejects everything else, and call sites stop passing dummies.

Discouraged:

```ts
interface TooltipRow {
  header?: boolean;   // when true, value is ignored
  indent?: number;    // 0, 1 or 2
  value: string;
}

const rows = [{ header: true, value: '', indent: 0 }, ...];
```

Preferred:

```ts
interface TooltipHeaderRow { kind: 'header'; label: string }
interface TooltipValueRow { kind: 'value'; label: string; value: string; indent: 0 | 1 | 2 }
```

### One function per variant

Type variants separately and write one named function per variant. Keep a union parameter and the `switch` over it when the `switch` is what proves at compile time that every case is handled.

Discouraged:

```ts
function reduceBadgeDisclosure(state: BadgeState, action: BadgeDisclosureEvent) {
  switch (action.type) {
    case 'open': ...
    case 'freeze': ...
    case 'close': ...
  }
}
```

Preferred:

```ts
function openBadge(state: BadgeState, trigger: OpenTrigger) { ... }
function freezeBadge(state: BadgeState) { ... }
function closeBadge(state: BadgeState) { ... }
```

### Discriminated unions

Complex union types can be written in a way that impossible combinations are stated. At that point, a type definition does not represent reality anymore.

Discouraged:

```ts
type LatencySnapshot =
  | { kind: 'empty' }
  | { kind: 'open'; samples: LatencySample[]; peaks: LatencyPeak[] };

function foldSnapshot(snapshot: Extract<LatencySnapshot, { kind: 'open' }>) { ... }

function renderSnapshot(snapshot: LatencySnapshot) {
  if (snapshot.kind === 'empty') {
    return emptyView();
  }
  return foldSnapshot(snapshot);
}
```

Preferred:

```ts
interface OpenLatencySnapshot { samples: LatencySample[]; peaks: LatencyPeak[] }

function foldSnapshot(snapshot: OpenLatencySnapshot) { ... }

function renderSnapshot(snapshot: OpenLatencySnapshot | null) {
  if (snapshot === null) {
    return emptyView();
  }
  return foldSnapshot(snapshot);
}
```

Acceptable:

```ts
type UploadState =
  | { kind: 'idle' }
  | { kind: 'uploading'; progress: number }
  | { kind: 'failed'; reason: UploadFailure };

function uploadLabel(state: UploadState): string {
  switch (state.kind) {
    case 'idle':
      return 'Ready';
    case 'uploading':
      return `${state.progress}%`;
    case 'failed':
      return describeFailure(state.reason);
  }
}
```

### Long type declarations go in `types.ts`

When a reader scrolling for the implementation scrolls past type declarations to reach it, move the exported types to a sibling `types.ts`. If a type is a short declaration of props or a single-line type, it's OK to stay in the util or component.

## Using this page in a review

Link the rule's heading in the review comment and quote the declaration it applies to.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://engineering.umh.app/engineering/development-process/how-to-build/coding-standards/languages/typescript.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
