Imported from eng-manager-xyz/auteur-rs (
.agents/skills/ts-object-design/SKILL.md). Install upstream withnpx skills add eng-manager-xyz/auteur-rs --skill ts-object-design. Copyright stays with the author.
TypeScript Object Design
Production patterns for designing predictable, optimizable objects with full type safety.
Base Web Constants: as const Pattern
Base Web components export constants as as const objects for type-safe variant selection.
// From baseui/button/constants.ts
export const KIND = {
primary: 'primary',
secondary: 'secondary',
tertiary: 'tertiary',
} as const;
export const SIZE = {
mini: 'mini',
compact: 'compact',
default: 'default',
large: 'large',
} as const;
export const SHAPE = {
default: 'default',
pill: 'pill',
round: 'round',
circle: 'circle',
square: 'square',
} as const;
// Derive union types from constants
type ButtonKind = (typeof KIND)[keyof typeof KIND];
// 'primary' | 'secondary' | 'tertiary'
type ButtonSize = (typeof SIZE)[keyof typeof SIZE];
// 'mini' | 'compact' | 'default' | 'large'
type ButtonShape = (typeof SHAPE)[keyof typeof SHAPE];
// 'default' | 'pill' | 'round' | 'circle' | 'square'
Creating Your Own Constants
Follow the same pattern for application-level constants:
// trip-status.ts
export const TRIP_STATUS = {
requested: 'requested',
accepted: 'accepted',
arriving: 'arriving',
inProgress: 'in_progress',
completed: 'completed',
cancelled: 'cancelled',
} as const;
type TripStatus = (typeof TRIP_STATUS)[keyof typeof TRIP_STATUS];
// Use in function signatures
function getTripLabel(status: TripStatus): string {
// Exhaustive handling guaranteed by type
switch (status) {
case TRIP_STATUS.requested: return 'Requested';
case TRIP_STATUS.accepted: return 'Driver Accepted';
case TRIP_STATUS.arriving: return 'Driver Arriving';
case TRIP_STATUS.inProgress: return 'In Progress';
case TRIP_STATUS.completed: return 'Completed';
case TRIP_STATUS.cancelled: return 'Cancelled';
}
}
Base Web Override Object Design
Every Base Web component accepts an overrides prop for deep customization of sub-components.
Override Object Structure
import type { ButtonOverrides } from 'baseui/button';
// Three forms of override:
// 1. Style override (object or function)
// 2. Props override
// 3. Component replacement
const buttonOverrides: ButtonOverrides = {
// Style function -- receives styled props including $theme
Root: {
style: ({ $theme, $size, $isSelected }) => ({
backgroundColor: $isSelected
? $theme.colors.backgroundPrimary
: $theme.colors.backgroundSecondary,
borderRadius: $theme.borders.radius300,
paddingLeft: $theme.sizing.scale600,
paddingRight: $theme.sizing.scale600,
}),
},
// Static style object (no access to props/theme)
StartEnhancer: {
style: {
marginRight: '8px',
},
},
// Props override
EndEnhancer: {
props: {
'data-testid': 'button-end-enhancer',
'aria-hidden': true,
},
},
};
Component Replacement Override
import type { ModalOverrides } from 'baseui/modal';
const modalOverrides: ModalOverrides = {
// Replace the backdrop sub-component entirely
Backdrop: {
component: ({ children, ...props }) => (
<div
{...props}
style={{ backgroundColor: 'rgba(0, 0, 0, 0.7)' }}
>
{children}
</div>
),
},
// Override dialog styles
Dialog: {
style: ({ $theme }) => ({
borderTopLeftRadius: $theme.borders.radius400,
borderTopRightRadius: $theme.borders.radius400,
borderBottomLeftRadius: $theme.borders.radius400,
borderBottomRightRadius: $theme.borders.radius400,
}),
},
};
Merging Overrides
import { mergeOverrides } from 'baseui/helpers/overrides';
const baseOverrides: ButtonOverrides = {
Root: {
style: { marginBottom: '8px' },
},
};
const activeOverrides: ButtonOverrides = {
Root: {
style: ({ $theme }) => ({
backgroundColor: $theme.colors.backgroundAccent,
}),
},
};
// Deep merge -- style functions are composed, not replaced
const merged = mergeOverrides(baseOverrides, activeOverrides);
Theme Object Structure
Base Web themes are deeply nested objects with semantic tokens.
import { LightTheme, createLightTheme } from 'baseui';
import type { Theme } from 'baseui/theme';
// Theme object shape (key sections)
const theme: Theme = {
colors: {
// Semantic colors (use these)
backgroundPrimary: '#FFFFFF',
backgroundSecondary: '#F6F6F6',
contentPrimary: '#000000',
contentSecondary: '#545454',
borderOpaque: '#CBCBCB',
// Brand colors
brandBackgroundPrimary: '#276EF1',
brandContentPrimary: '#276EF1',
// Feedback colors
backgroundNegative: '#E11900',
backgroundPositive: '#048848',
backgroundWarning: '#FFC043',
},
sizing: {
scale0: '2px',
scale100: '4px',
scale300: '8px',
scale600: '16px',
scale800: '24px',
// ... up to scale4800
},
typography: {
font100: { fontFamily: '...', fontSize: '12px', fontWeight: 400, lineHeight: '20px' },
font200: { fontFamily: '...', fontSize: '14px', fontWeight: 400, lineHeight: '20px' },
// ... up to font1450
ParagraphSmall: { /* ... */ },
LabelMedium: { /* ... */ },
HeadingLarge: { /* ... */ },
},
borders: {
radius100: '2px',
radius200: '4px',
radius300: '8px',
radius400: '12px',
radius500: '16px',
buttonBorderRadius: '8px',
inputBorderRadius: '8px',
},
mediaQuery: {
small: '@media screen and (min-width: 320px)',
medium: '@media screen and (min-width: 600px)',
large: '@media screen and (min-width: 1136px)',
},
animation: {
easeOutCurve: 'cubic-bezier(.2, .8, .4, 1)',
easeInCurve: 'cubic-bezier(.8, .2, 1, .4)',
timing400: '400ms',
},
};
// Creating a custom theme
const customTheme = createLightTheme({
primaryFontFamily: 'UberMove, system-ui, sans-serif',
});
Accessing Theme in Styled Components
import { styled, useStyletron } from 'baseui';
// styled() -- theme via $theme prop
export const StyledCard = styled<'div', { $elevated?: boolean }>('div', (props) => {
const { $theme, $elevated } = props;
return {
backgroundColor: $theme.colors.backgroundPrimary,
borderRadius: $theme.borders.radius300,
padding: $theme.sizing.scale600,
boxShadow: $elevated ? $theme.lighting.shadow400 : 'none',
...($theme.mediaQuery.small && {
[$theme.mediaQuery.small]: {
padding: $theme.sizing.scale300,
},
}),
};
});
// useStyletron() -- hook for inline styles
function StatusBadge({ active }: { active: boolean }) {
const [css, theme] = useStyletron();
return (
<span
className={css({
color: active ? theme.colors.contentPositive : theme.colors.contentSecondary,
...theme.typography.LabelSmall,
padding: `${theme.sizing.scale100} ${theme.sizing.scale300}`,
})}
>
{active ? 'Active' : 'Inactive'}
</span>
);
}
Styled Component Props ($-prefixed)
Styled components receive $-prefixed custom props to avoid collision with HTML attributes.
import { styled } from 'baseui';
// Convention: $-prefixed props are styled props
interface StyledTabProps {
$active: boolean;
$disabled: boolean;
$orientation: 'horizontal' | 'vertical';
}
export const StyledTab = styled<'div', StyledTabProps>('div', (props) => {
const { $active, $disabled, $orientation, $theme } = props;
const { colors, sizing, typography } = $theme;
return {
...typography.font200,
color: $active ? colors.contentPrimary : colors.tabColor,
cursor: $disabled ? 'not-allowed' : 'pointer',
opacity: $disabled ? 0.4 : 1,
paddingTop: sizing.scale600,
paddingBottom: sizing.scale600,
borderBottomWidth: $active ? '2px' : '0px',
borderBottomStyle: 'solid',
borderBottomColor: $active ? colors.borderSelected : 'transparent',
display: $orientation === 'vertical' ? 'block' : 'inline-flex',
};
});
// Usage
<StyledTab $active={true} $disabled={false} $orientation="horizontal" />
Fusion.js Plugin deps/provides Pattern
Fusion.js plugins use dependency injection with typed deps and provides objects.
import { createPlugin, createToken, type Context } from 'fusion-core';
// Define tokens (typed dependency keys)
export const UserServiceToken = createToken<UserService>('UserService');
export const TripServiceToken = createToken<TripService>('TripService');
// Service interfaces
interface UserService {
getUser(id: string, ctx: Context): Promise<User>;
updateUser(id: string, data: Partial<User>, ctx: Context): Promise<User>;
}
interface TripService {
getTrip(id: string, ctx: Context): Promise<Trip>;
listTrips(userId: string, ctx: Context): Promise<Trip[]>;
}
// Plugin with typed deps and provides
export const DashboardPlugin = createPlugin({
// deps: object mapping local names to tokens
deps: {
userService: UserServiceToken,
tripService: TripServiceToken,
authHeaders: AuthHeadersToken,
},
// provides: factory receiving resolved deps, returns service object
provides: ({ userService, tripService, authHeaders }) => ({
getDashboard: async (userId: string, ctx: Context) => {
const [user, trips] = await Promise.all([
userService.getUser(userId, ctx),
tripService.listTrips(userId, ctx),
]);
return { user, trips, lastActivity: trips.at(-1)?.createdAt ?? null };
},
}),
});
// Extract the provided type for consumers
export type DashboardService = ExtractServiceType<typeof DashboardPlugin>;
// Registration in app
app.register(DashboardToken, DashboardPlugin);
RPC Handlers as provides Object
import { createPlugin, type Context } from 'fusion-core';
import { RPCHandlersToken } from 'fusion-plugin-rpc';
// RPC handlers follow the same deps/provides pattern
export const RpcHandlersPlugin = createPlugin({
deps: {
authHeaders: AuthHeadersToken,
services: ServiceConnectorToken,
},
provides: ({ authHeaders, services }) => ({
// Each key becomes an RPC endpoint
getUser: async (args: { id: string }, ctx: Context) => {
const auth = authHeaders.get(ctx);
return services.call('user-service', { ...args, uuid: auth.uuid }, ctx);
},
updateSettings: async (args: { key: string; value: unknown }, ctx: Context) => {
const auth = authHeaders.get(ctx);
return services.call('settings-service', { ...args, uuid: auth.uuid }, ctx);
},
}),
});
// Type extraction for client-side hooks
export type RpcHandlers = ExtractServiceType<typeof RpcHandlersPlugin>;
// Client-side usage
import { createReactQueryRpcHooks } from '@uber/fusion-plugin-react-query';
export const { useRpcQuery, useRpcMutation } = createReactQueryRpcHooks<RpcHandlers>();
Initialize All Properties
V8 creates hidden classes for objects. Adding properties later causes shape transitions, degrading performance.
interface ServiceConfig {
readonly name: string;
readonly baseUrl: string;
timeout: number;
retries: number;
headers: Record<string, string>;
lastError: Error | null;
}
// Initialize all properties upfront -- consistent V8 hidden class
function createServiceConfig(name: string, baseUrl: string): ServiceConfig {
return {
name,
baseUrl,
timeout: 5000,
retries: 3,
headers: {},
lastError: null, // Initialize to null, not undefined
};
}
// All configs have identical hidden class shape
const configs: ServiceConfig[] = [
createServiceConfig('user-service', 'https://user.uber.internal'),
createServiceConfig('trip-service', 'https://trip.uber.internal'),
createServiceConfig('payment-service', 'https://payment.uber.internal'),
];
Immutable Updates
Immutability prevents bugs from shared mutable state. Critical for React state and React Query cache.
interface TripState {
readonly tripId: string;
readonly status: string;
readonly driverId: string | null;
readonly updatedAt: Date;
}
// Immutable update
function updateTrip<T extends TripState>(trip: T, changes: Partial<T>): T {
return {
...trip,
...changes,
updatedAt: new Date(),
};
}
const trip: TripState = {
tripId: 'trip-123',
status: 'requested',
driverId: null,
updatedAt: new Date(),
};
const accepted = updateTrip(trip, { status: 'accepted', driverId: 'driver-456' });
// trip is unchanged
// Immutable nested update
interface DriverProfile {
readonly driverId: string;
readonly vehicle: {
readonly make: string;
readonly model: string;
readonly year: number;
};
readonly rating: number;
}
function updateVehicle(
profile: DriverProfile,
vehicleUpdates: Partial<DriverProfile['vehicle']>,
): DriverProfile {
return {
...profile,
vehicle: {
...profile.vehicle,
...vehicleUpdates,
},
};
}
ES2023 Immutable Array Methods
const numbers: readonly number[] = [5, 2, 8, 1, 9];
// toSorted - immutable sort
const sorted = numbers.toSorted((a, b) => a - b);
// numbers: [5, 2, 8, 1, 9] (unchanged)
// toReversed - immutable reverse
const reversed = numbers.toReversed();
// toSpliced - immutable splice
const spliced = numbers.toSpliced(1, 2, 99, 100);
// with - immutable index update
const updated = numbers.with(2, 999);
// Chaining immutable operations
interface TripItem {
readonly priority: number;
readonly tripId: string;
}
const trips: readonly TripItem[] = [
{ priority: 3, tripId: 'trip-a' },
{ priority: 1, tripId: 'trip-b' },
{ priority: 2, tripId: 'trip-c' },
];
const result = trips
.toSorted((a, b) => a.priority - b.priority)
.toReversed()
.with(0, { priority: 0, tripId: 'trip-featured' });
Deep Cloning
interface ComplexState {
user: { name: string; settings: Record<string, unknown> };
trips: Array<{ id: string; stops: Array<{ lat: number; lng: number }> }>;
metadata: Map<string, string>;
}
// structuredClone - handles complex types
const original: ComplexState = {
user: { name: 'Alice', settings: { darkMode: true } },
trips: [{ id: 'trip-1', stops: [{ lat: 37.7749, lng: -122.4194 }] }],
metadata: new Map([['version', '2']]),
};
const copy = structuredClone(original);
// Deep copy -- modifying copy does not affect original
copy.user.settings.darkMode = false;
console.log(original.user.settings.darkMode); // true
// Shallow copy comparison
const shallow = { ...original };
shallow.user.settings.darkMode = false;
console.log(original.user.settings.darkMode); // false (affected!)
// structuredClone limitations:
// - Cannot clone functions
// - Cannot clone DOM nodes
// - Cannot clone Error objects
// - Cannot clone symbols
Object Freezing
// Freeze config objects to prevent accidental mutation
interface AppConfig {
apiBaseUrl: string;
timeout: number;
features: {
darkMode: boolean;
newCheckout: boolean;
};
}
// Deep freeze utility
function deepFreeze<T extends object>(obj: T): Readonly<T> {
Object.freeze(obj);
for (const key of Object.keys(obj) as Array<keyof T>) {
const value = obj[key];
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
deepFreeze(value as object);
}
}
return obj;
}
const config = deepFreeze({
apiBaseUrl: 'https://api.uber.internal',
timeout: 5000,
features: { darkMode: true, newCheckout: false },
});
// config.features.darkMode = false; // Runtime error (frozen)
Null Object Pattern
interface UserEntity {
readonly uuid: string | null;
readonly name: string;
readonly isAuthenticated: boolean;
readonly roles: readonly string[];
hasRole(role: string): boolean;
}
// Avoid null checks with null objects
const anonymousUser: UserEntity = {
uuid: null,
name: 'Anonymous',
isAuthenticated: false,
roles: [],
hasRole: (): boolean => false,
};
const userCache = new Map<string, UserEntity>();
function getUser(uuid: string): UserEntity {
return userCache.get(uuid) ?? anonymousUser;
}
// Usage -- no null checks needed
const user = getUser('unknown-uuid');
console.log(user.name); // 'Anonymous'
console.log(user.hasRole('admin')); // false
Quick Reference
// as const for constants (Base Web pattern)
export const SIZE = { small: 'small', medium: 'medium', large: 'large' } as const;
type Size = (typeof SIZE)[keyof typeof SIZE];
// Override object (Base Web pattern)
const overrides = { Root: { style: ({ $theme }) => ({ color: $theme.colors.contentPrimary }) } };
// Styled props ($-prefixed)
styled<'div', { $active: boolean }>('div', ({ $active, $theme }) => ({ ... }));
// Plugin deps/provides (Fusion.js pattern)
createPlugin({ deps: { svc: Token }, provides: ({ svc }) => ({ method: async () => {} }) });
// Immutable update
const next: State = { ...state, updated: true };
// Immutable nested update
const next = { ...obj, nested: { ...obj.nested, value: 1 } };
// Deep clone
const copy = structuredClone(original);
// Immutable array methods
arr.toSorted() // sorted copy
arr.toReversed() // reversed copy
arr.with(i, val) // copy with updated index
// Freeze
Object.freeze(obj); // shallow
deepFreeze(obj); // deep
Best Practices
- Use as const for constants - SIZE, KIND, SHAPE pattern from Base Web
- Derive types from constants -
(typeof SIZE)[keyof typeof SIZE]not manual unions - Type override objects - Use
ButtonOverrides,ModalOverridesetc. from baseui - $-prefix styled props - Avoid collision with HTML attributes
- Initialize all properties - In constructor or factory for V8 hidden class stability
- Prefer immutability - Create new objects instead of mutating
- Use structuredClone - For deep copying complex objects
- Use ES2023 methods - toSorted, toReversed, with
- Freeze config objects - Prevent accidental mutation
- Use null objects - Avoid null checks in application code
- Type deps/provides - Extract service types from plugins with
ExtractServiceType
Evaluation
# Check for as const usage (constants pattern)
grep -rn "as const" --include="*.ts" | wc -l
# Check for override objects
grep -rn "overrides" --include="*.tsx" | wc -l
# Check for $-prefixed styled props
grep -rn "\$theme\|\$size\|\$active\|\$disabled" --include="*.ts" --include="*.tsx" | wc -l
# Check for createPlugin usage
grep -rn "createPlugin" --include="*.ts" --include="*.tsx" | wc -l
# Check for readonly usage
grep -rn "readonly " --include="*.ts" | wc -l
# Check for structuredClone usage
grep -rn "structuredClone" --include="*.ts" | wc -l
# Check for Object.freeze usage
grep -rn "Object.freeze" --include="*.ts" | wc -l
# Check for immutable array methods
grep -rn "toSorted\|toReversed\|\.with(" --include="*.ts" | wc -l
# Check for spread operator usage (immutable updates)
grep -rn "\.\.\." --include="*.ts" | wc -l
# Check for factory functions
grep -rn "function create[A-Z]" --include="*.ts" | wc -l
# Check for mergeOverrides usage
grep -rn "mergeOverrides" --include="*.ts" --include="*.tsx" | wc -l
# Check for styled() with typed props
grep -rn "styled<" --include="*.ts" --include="*.tsx" | wc -l