# Opentrons Typescript

> TypeScript conventions, React patterns, testing, styling, and import rules for the Opentrons monorepo JS/TS packages. Use when working with TypeScript or React files in app/, components/, shared-data/, step-generation/, protocol-designer/, protocol-visualization/, opentrons-ai-client/, or other JS/TS packages. Use when this capability is needed.

- Skill: `tomevault-io/opentrons-typescript` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/opentrons-typescript`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/opentrons-typescript/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/opentrons-typescript

---


# Opentrons Monorepo — TypeScript Conventions

Node.js, Yarn, Python setup, teardown, and troubleshooting are in the always-apply `monorepo-setup` rule.

## Monorepo Structure

Yarn workspaces monorepo with 15 TypeScript packages. No Lerna/Nx/Turbo — uses Yarn Classic workspaces + TypeScript project references.

### Packages

| Package                             | Directory                 | Type                              |
| ----------------------------------- | ------------------------- | --------------------------------- |
| `@opentrons/app`                    | `app/`                    | React app                         |
| `@opentrons/app-shell`              | `app-shell/`              | Electron shell                    |
| `@opentrons/app-shell-odd`          | `app-shell-odd/`          | Electron shell (ODD)              |
| `@opentrons/components`             | `components/`             | React UI components library       |
| `@opentrons/api-client`             | `api-client/`             | Pure TS library                   |
| `@opentrons/react-api-client`       | `react-api-client/`       | React hooks library               |
| `@opentrons/discovery-client`       | `discovery-client/`       | Pure TS (Node)                    |
| `@opentrons/shared-data`            | `shared-data/`            | Pure TS/JS data library           |
| `@opentrons/step-generation`        | `step-generation/`        | Pure TS library                   |
| `@opentrons/labware-library`        | `labware-library/`        | React app                         |
| `@opentrons/labware-designer`       | `labware-designer/`       | React app                         |
| `opentrons-ai-client`               | `opentrons-ai-client/`    | React app                         |
| `protocol-designer`                 | `protocol-designer/`      | React app                         |
| `@opentrons/protocol-visualization` | `protocol-visualization/` | React library (protocol viz, WIP) |
| `@opentrons/usb-bridge-client`      | `usb-bridge/node-client/` | Pure TS (Node)                    |

### Dependency Graph

`shared-data` is the foundation. Nothing should import "up" the tree:

```markdown
shared-data
├── step-generation
├── components
├── api-client → react-api-client
└── discovery-client
↓
protocol-visualization (scaffold; depends on components + shared-data + step-generation)
↓
app, protocol-designer, labware-library, opentrons-ai-client (leaf apps)
```

## TypeScript Configuration

All packages extend `tsconfig-base.json`:

- **Target/Module:** ESNext
- **Strict:** true (no `any`, strict null checks)
- **JSX:** preserve (Vite handles transform)
- **Declarations:** emitDeclarationOnly, composite for project references
- **Module resolution:** node

Each package defines `rootDir: "src"`, `outDir: "lib"`, and references its dependencies.

## Code Style (Prettier)

Enforced by Prettier with `@ianvs/prettier-plugin-sort-imports`:

- **No semicolons**
- **Single quotes** (double quotes in JSX)
- **Trailing commas:** ES5
- **Print width:** 80, **Tab width:** 2
- **Line endings:** LF

## Import Conventions

### Order (auto-sorted by Prettier plugin)

1. React imports (`import { useState } from 'react'`)
2. Third-party packages
3. `@opentrons/*` packages
4. Package-local absolute imports (`/app/*`, `/protocol-designer/*`, `/ai-client/*`)
5. Relative imports
6. `import type` (type-only imports, same sub-ordering)
7. Asset imports (images, CSS)

### Cross-Package Imports

Use the `@opentrons/` scope. These resolve to source via Vite aliases in dev/test:

```typescript
import { Flex, SPACING } from '@opentrons/components'
import { getPipetteSpecsV2 } from '@opentrons/shared-data'

import type { PipetteName } from '@opentrons/shared-data'
```

### Intra-Package Absolute Imports

Each app has a path alias (configured in tsconfig + Vite):

- `app/` → `/app/*`
- `protocol-designer/` → `/protocol-designer/*`
- `opentrons-ai-client/` → `/ai-client/*`

```typescript
// Good — absolute import within app
import { useRobot } from '/app/resources/robots'

// Bad — deep relative paths across features
import { useRobot } from '../../../resources/robots'
// Acceptable — relative for nearby files in the same feature
import { utils } from './utils'
```

### No Default Exports

ESLint enforces `import/no-default-export`. Always use named exports. Exceptions: config files (`vite.config.mts`, `*.stories.tsx`).

### Lodash

Import individual functions only:

```typescript
// Good

// Bad — imports entire library
import { mapValues } from 'lodash'
import mapValues from 'lodash/mapValues'
```

### Type Imports

Always use `import type` for type-only imports:

```typescript
import type { LabwareDefinition2 } from '@opentrons/shared-data'
```

## React Component Patterns

### Function Declarations (not arrows)

```typescript
interface MyComponentProps {
  title: string
  onClose: () => void
}

export function MyComponent({ title, onClose }: MyComponentProps): JSX.Element {
  return <div>{title}</div>
}
```

- Named function declarations, not arrow functions, for components
- Props interface named `<ComponentName>Props`
- Always destructure props in the function signature

### Atomic Design Hierarchy

Components are organized as `atoms/` → `molecules/` → `organisms/` → `pages/`. Custom ESLint rule `opentrons/no-imports-up-the-tree-of-life` prevents importing up the hierarchy:

- atoms must NOT import from molecules, organisms, or pages
- molecules must NOT import from organisms or pages
- organisms must NOT import from pages

### Application Boundaries (`app/` specific)

The `app` package separates Desktop and ODD (On-Device Display) UIs. ESLint rule `opentrons/no-imports-across-applications` prevents cross-contamination between `/Desktop/`, `/ODD/`, and shared code.

### Component Library (`@opentrons/components`)

Use primitives from the shared component library for layout and common UI:

```typescript
import {
  COLORS,
  DIRECTION_COLUMN,
  Flex,
  Icon,
  SPACING,
  StyledText,
} from '@opentrons/components'
```

### Hooks

- `useSelector` / `useDispatch` from `react-redux`
- `useTranslation` from `react-i18next` for i18n
- Custom hooks prefixed with `use*`
- Never call hooks conditionally
- ESLint enforces `react-hooks/rules-of-hooks` (error) and `react-hooks/exhaustive-deps` (warn)

## Styling

### CSS Modules (preferred for new code)

- File: `<componentname>.module.css` (lowercase, no separators)
- Classes: `snake_case` (enforced by Stylelint: `/^[a-z0-9_]+$/`)
- Use CSS custom properties from the design system (spacing, colors, typography, border-radius)
- Use `clsx` for conditional classes
- **Never use inline styles** in components (only in `*.stories.tsx`)

### styled-components (legacy)

Some packages still use `styled-components@5.3.6`. Do not introduce new styled-components — use CSS Modules for new code.

### Design System Tokens

```css
/* Spacing */
padding: var(--spacing-8);
gap: var(--spacing-16);

/* Colors */
color: var(--grey-60);
background: var(--white);

/* Typography */
font-size: var(--font-size-p);
font-weight: var(--font-weight-semi-bold);

/* Border radius */
border-radius: var(--border-radius-8);

/* Width/height — use explicit rem values, NOT variables */
width: 15rem;
```

## Testing

### Framework

- **Vitest 2.1.9** (not Jest) — `vi.fn()`, `vi.mock()`, `vi.mocked()`
- **@testing-library/react 16.3.0** — `screen`, `fireEvent`, `renderHook`
- **@testing-library/user-event 14.6.1**
- **vitest-when 0.5.0** for conditional mocking
- **jsdom** test environment (global `vitest.config.mts`)

### Test File Structure

```markdown
FeatureOrComponent/
├── index.tsx (or module.ts)
└── **tests**/
└── FeatureName.test.tsx
```

### renderWithProviders

React component tests MUST use `renderWithProviders` (wraps Redux Provider + QueryClientProvider + optional i18n), not plain `render`:

```typescript
import { renderWithProviders } from '/app/__testing-utils__'  // or /protocol-designer/__testing-utils__

describe('MyComponent', () => {
  let props: ComponentProps<typeof MyComponent>

  beforeEach(() => {
    props = { /* defaults */ }
  })

  afterEach(() => {
    vi.clearAllMocks()
  })

  it('renders', () => {
    renderWithProviders(<MyComponent {...props} />)
    expect(screen.getByRole('button')).toBeInTheDocument()
  })
})
```

### Mocking

- `vi.mock()` at file top for module mocks
- `vi.mocked(fn).mockReturnValue(...)` for typed mocks
- `vi.clearAllMocks()` in `afterEach` (always)

### Queries

- Prefer `screen.getByRole`, `screen.getByText`, `screen.getByTestId`
- Never use `container.querySelector`
- `data-testid` format: `ComponentName_ElementType`

## Makefile Targets

### Per-Package (run from the package directory)

Each package has a `Makefile` with some or all of:

| Target          | Description                   |
| --------------- | ----------------------------- |
| `make dev`      | Start Vite dev server         |
| `make build`    | Production build              |
| `make clean`    | Remove build output           |
| `make test`     | Run tests (delegates to root) |
| `make test-cov` | Run tests with coverage       |

### Root Makefile (run from monorepo root)

| Target                            | Description                                                        |
| --------------------------------- | ------------------------------------------------------------------ |
| `make setup-js`                   | Install all JS deps (`yarn`)                                       |
| `make test-js`                    | Run ALL JS tests                                                   |
| `make test-js-<project>`          | Run tests for one project (e.g., `make test-js-protocol-designer`) |
| `make lint-js`                    | ESLint + Prettier check                                            |
| `make lint-js-eslint`             | ESLint only                                                        |
| `make lint-js-prettier`           | Prettier only                                                      |
| `make lint-css`                   | Stylelint all CSS                                                  |
| `make format-js`                  | Auto-format with Prettier                                          |
| `make format-css`                 | Auto-fix CSS with Stylelint                                        |
| `make check-js` / `make build-ts` | TypeScript type-check (`tsc --build`)                              |
| `make clean-ts`                   | Clean TS build output                                              |
| `make circular-dependencies-js`   | Check circular imports (madge)                                     |

### Running Tests Directly

```bash
# Single file
yarn vitest app/src/organisms/__tests__/MyComponent.test.tsx

# Entire package
yarn vitest protocol-designer/

# Watch mode
yarn vitest --watch app/src/

# Specific project via Make
make test-js-app tests="src/organisms/__tests__/MyComponent.test.tsx"
```

### Linting Specific Files

```bash
yarn eslint path/to/file.tsx
yarn stylelint path/to/file.module.css
yarn prettier --check path/to/file.tsx
yarn prettier --write path/to/file.tsx   # auto-fix
```

## Event Handlers

```typescript
// Named handlers for complex logic
const handleClick = (e: React.MouseEvent<HTMLButtonElement>): void => {
  e.preventDefault()
  onClick()
}
return <button onClick={handleClick}>Click me</button>

// Direct reference for simple cases
return <button onClick={onClick}>Click me</button>

// Bad — unnecessary wrapper
return <button onClick={() => onClick()}>Click me</button>
```

Always specify `type` on buttons in forms:

```typescript
<button type="button" onClick={handleAttach}>Attach</button>
<button type="submit">Submit Form</button>
```

## Constants & Magic Numbers

Extract all constants — avoid inline magic numbers:

```typescript
// Good
const UNIT_MB = 1024 * 1024
const MAX_FILES = 5

export const FILE_SIZE_LIMITS = {
  pdf: 10 * UNIT_MB,
  csv: 2 * UNIT_MB,
} as const

// Bad
const sizeMB = Math.round(sizeLimit / (1024 * 1024))
```

Use object lookups for simple mappings instead of switch statements.

## Component Architecture

Separate router logic from presentation for testability:

```typescript
// Router-aware wrapper
function AppWithRouter() {
  const location = useLocation()
  return <AppContent isOnChatPage={location.pathname === '/chat'} />
}

// Pure presentation (easily testable)
function AppContent({ isOnChatPage }: { isOnChatPage: boolean }) {
  return <div>{!isOnChatPage ? <Footer /> : null}</div>
}
```

## Common Pitfalls

- Do NOT use `any` — strict TypeScript is enforced
- Do NOT use default exports (except config files and stories)
- Do NOT use class components — functional only
- Do NOT use arrow functions for component definitions
- Do NOT use implicit truthiness for null checks — use explicit `!= null`
- Do NOT import the full lodash package — use granular imports
- Do NOT use inline styles in components
- Do NOT use `querySelector` in tests
- Do NOT introduce new styled-components — use CSS Modules
- Do NOT import up the atomic design hierarchy (atoms ← molecules ← organisms ← pages)
- Do NOT skip `afterEach(() => vi.clearAllMocks())` in test suites
- Do NOT use semicolons (Prettier removes them)
- Do NOT use `console.log` or `debugger` in committed code

---
> Converted and distributed by [TomeVault](https://tomevault.io/claim/Opentrons) — claim your Tome and manage your conversions.
<!-- tomevault:4.0:skill_md:2026-04-16 -->

