Enhancing UI Component Design with TypeScript Namespaces
TSKaigi Kansai @ Miyako Messe Kyoto
saku🌸 / @sakupi01
Enhancing UI Component Design with TypeScript Namespaces
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
- DefinitelyTyped is an example
→ 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"
- ESM's
import as・export as - TS's
namespace
Express NavigationButton.Props
1. ESM's import as・export 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 asallows 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