Enhancing UI Component Design with TypeScript Namespaces

TSKaigi Kansai @ Miyako Messe Kyoto

saku🌸 / @sakupi01

saku

Web Frontend Engineer
@Cybozu

🍏/ ☕/ 🌏 > ❤️
𝕏 @sakupi01

Enhancing UI Component Design with TypeScript Namespaces

Available in EN

UI Component Creation in TSX

// components/button.tsx
export type ButtonProps = { ... };

export function Button(props: ButtonProps) { ... }
// components/navigation-button.tsx
import { Button, type ButtonProps } from "./button";

export type NavigationButtonProps = ButtonProps & { ... };

export function NavigationButton(props: NavigationButtonProps) { ... }

When Components Get Complex

// components/enhanced-navigation-button.tsx
import { NavigationButton, type NavigationButtonProps } from "./navigation-button";

export type EnhancedNavigationButtonProps = NavigationButtonProps & { ... };

export function EnhancedNavigationButton(props: EnhancedNavigationButtonProps) { ... }

EnhancedNavigationButtonProps: EnhancedなNavigationのButtonのProps

Too Long Props Names

Limitations of Naming Conventions

  • Need to update component names when Props types change
    → Reduced maintainability
  • Components with many child components or complex data structures
    → Increasingly complex naming

We need a better way to:

  • Manage components "values" and their Props "types" as a unit
  • Express nested data structures clearly

1. Managing Values and Types as a Unit

結論:Solution: Use TypeScript's Companion Object Pattern

TypeScript's Advantage: Separate handling of values and types

  • Allows adding type definitions to existing JavaScript libraries

→ For components, we need to intentionally maintain the relationship between values and types

Companion Object Pattern

  • Values and types are separate entities
  • → Can use the same name for both
// components/button/index.tsx
export type Button = { ... };

export function Button(props: Button) { ... }

Companion Object Pattern

You can use values and types as a set by defining both the component and its Props as Button.

// components/button/index.tsx
export type Button = { ... };

export function Button(props: Button) { ... }
// components/navigation-button/index.tsx
import { Button } from "./button";

// as Button Props Type
export type Props = Button & { ... };

export function NavigationButton(props: Props) { 
  // as Buttonコンポーネント Value
  return <Button {...props} className={{ ... }} />;
}

Got a way to manage values and types as a unit!

2. Expressing Nested Data Structures

Solution: Use TypeScript Namespaces

Values and Types in TypeScript

  • Values: Elements that affect the runtime
  • Types: Elements that affect the compile time
  • → TypeScript treats values and types as separate entities

Module of "Values"

  • Use Object
  • Use IIFE scope

Module of "Types"

  1. ESM's import asexport as
  2. TS's namespace

Express NavigationButton.Props

1. ESM's import asexport as

/**
 * components/button/type.tsx
 */
export type Props = {...};

/**
 * components/navigation-button/type.tsx
 */
// Import as Module Namespace Object
import * as Button from './type';
export type Props = Button.Props & {...};

/**
 * components/navigation-button/index.tsx
 */
// Import as Module Namespace Object
import * as NavigationButton from './type';

export function NavigationButton(props: NavigationButton.Props) { ... }

Issues with Structuring Props Types with ESM

  • Need to split files to create namespaces
    • Need to re-export for nested type objects
    • Don't want to split files just to represent data structures
  • import as allows freely naming imports
    • Risk of inconsistency

Use TypeScript's namespace to Modularize Data Types

// components/button/index.tsx
export namespace Button {
  export type Props = { ... };
}

export function Button(props: Button.Props) { ... }

/**
 * components/navigation-button/index.tsx
 */

import { Button } from "./button";

export namespace NavigationButton {
  export type Props = Button.Props & { ... };
}

export function NavigationButton(props: NavigationButton.Props) { ... }

Use TypeScript's namespace to Modularize Data Types

Using TypeScript's namespace for UI component design, you can do like this to manage related type definitions in one place:

// components/button.tsx
export namespace Button {
  export type Variant = "solid" | "ghost" | "outline";
  export type Size = "xs" |  "sm" | "md" | "lg" | "xl";
  export type Props = {
    variant: Variant;
    size: Size;
  }
}

export function Button(props: Button.Props) { ... }

3. Summary

Using TypeScript's namespace (and the Companion Object Pattern),

  • Managed values and types as a unit, and expressed data structures in modules
  • → Flexible and structural naming possible!

A good tip to keep in mind for UI component design

Thank you for listening!