Skip to content

TypeScript Patterns

This guide documents the TypeScript patterns used in Lakea. These conventions ensure consistency and help newcomers write code that fits the existing style.

We use "moduleResolution": "nodenext" which requires .js extensions in imports, even for .ts files.

// Correct
export * from './types/index.js';
import { Star } from './types/index.js';
// Wrong - will fail at runtime
export * from './types';
import { Star } from './types/index';

This is configured in tsconfig.base.json and applies to all packages.

Every directory has an index.ts that re-exports its contents. This enables clean imports.

packages/core/src/index.ts
export * from './types/index.js';
export * from './data-fetchers/index.js';
export * from './apps/index.js';
export * from './routing/index.js';

Consumers can then import from the package root:

import { Star, fetchExoplanets, getAppLink } from '@lakea/core';

Use export type for type-only exports. This helps bundlers with tree-shaking.

// Exporting both value and type
export { fetchExoplanets } from './exoplanetArchive.js';
export type { FetchExoplanetsOptions } from './exoplanetArchive.js';
  • Component props: XxxProps (e.g., ButtonProps, CardProps)
  • No I prefix (use ButtonProps, not IButtonProps)
  • Descriptive names (use DataSource, not ISource)
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
size?: ButtonSize;
children: ReactNode;
}

Use string literal unions for component variants. Export the type separately for consumers.

export type ButtonVariant = 'primary' | 'secondary' | 'ghost';
export type ButtonSize = 'sm' | 'md' | 'lg';

Then create a style map:

const variantStyles: Record<ButtonVariant, string> = {
primary: 'bg-cosmic-500 text-white hover:bg-cosmic-600',
secondary: 'bg-slate-700 text-slate-100',
ghost: 'bg-transparent text-slate-300',
};

Use as const for immutable objects like design tokens:

export const colors = {
cosmic: {
500: '#5a6cf4',
600: '#4a5cd4',
},
slate: {
800: '#1e293b',
900: '#0f172a',
},
} as const;

This ensures TypeScript infers literal types ('#5a6cf4') instead of string.

Use descriptive single-letter generics (T for type, K for key):

export interface QueryResult<T> {
data: T[];
totalCount: number;
source: DataSource;
pagination?: {
offset: number;
limit: number;
hasMore: boolean;
};
}

Use nullish coalescing for default values and optional chaining for callbacks:

// Nullish coalescing - preserves 0 and '' but replaces null/undefined
const mass = row.pl_masse ?? undefined;
// Optional chaining for callbacks
onPointClick?.(point);
// Combined pattern
const value = data?.results?.[0]?.value ?? 'default';

All packages use strict TypeScript settings from tsconfig.base.json:

{
"compilerOptions": {
"strict": true,
"noUnusedLocals": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true
}
}

Never disable these settings. If you get a type error, fix the code, don’t weaken the types.

Development imports use the @lakea/source custom condition for source file access:

{
"customConditions": ["@lakea/source"],
"paths": {
"@lakea/core": ["packages/core/src/index.ts"],
"@lakea/design-system": ["packages/design-system/src/index.ts"]
}
}

This allows importing source files directly during development while using dist files in production.

Each package has dual exports for dev and prod:

{
"exports": {
".": {
"@lakea/source": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
}
}
  • Config files: lowercase (vite.config.ts, tsconfig.json)
  • Source files: camelCase (exoplanetArchive.ts, fetchNasaData.ts)
  • React components: PascalCase (ScatterPlot.tsx, Button.tsx)
  • Directories: kebab-case (data-fetchers/, design-system/)

Library packages use named exports only:

// Correct - named export
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(...);
// Wrong in library packages - default export
export default Button;

App components may use default exports for page components.