-
-
Notifications
You must be signed in to change notification settings - Fork 8
Description
Problem
TypeScript enums create significant migration friction when updating packages across the MetaMask ecosystem. Even when enum values are identical, TypeScript treats different enum types as incompatible due to nominal typing.
Real-World Example: MenuItem Migration
PR #39810 demonstrates the complexity of migrating the MenuItem component from @metamask/component-library to @metamask/design-system-react. Because both packages use enums for the same conceptual values, MenuItem is forced to support dual prop systems:
type MenuItemProps = {
// Legacy props from component-library
iconNameLegacy?: IconNameLegacy; // Old enum
iconColorLegacy?: IconColorLegacy;
textVariantLegacy?: TextVariantLegacy;
// New props from @metamask/design-system-react
iconName?: IconName; // New enum (same values!)
iconSize?: IconSize;
textVariant?: TextVariant;
};
// Runtime logic to choose which system to use
const useNewSystem = iconName || textVariant;The problem: Even though IconNameLegacy.Trash and IconName.Trash both resolve to 'Trash', TypeScript considers them incompatible types. This forces complex workarounds:
- Duplicate props for legacy and new systems
- Runtime branching to determine which system to use
- Conditional rendering based on which props are provided
- Every consumer component must be updated to use the new prop names
This pattern is repeated across hundreds of call sites:
- <MenuItem iconName={IconName.Trash} />
+ <MenuItem iconNameLegacy={IconName.Trash} />Impact on Consumer Codebases
Extension and mobile teams face similar challenges:
- They may define their own enums that match design system values
- Wrapper components must support multiple enum types
- Gradual migration becomes difficult without dual type support
Proposed Solution: String Union Types
Replace TypeScript enums with string union types + const objects for value references:
// Instead of:
export enum ButtonIconSize {
Sm = 'sm',
Md = 'md',
Lg = 'lg',
}
// Use:
export type ButtonIconSize = 'sm' | 'md' | 'lg';
export const ButtonIconSize = {
Sm: 'sm',
Md: 'md',
Lg: 'lg',
} as const;Benefits
1. Backwards Compatibility
Both type and value exports work identically to enums:
// Type usage (works)
const props: { size: ButtonIconSize } = { size: 'md' };
// Value reference (works)
<ButtonIcon size={ButtonIconSize.Md} />2. Cross-Package Compatibility
String unions use structural typing, so they accept values from any source:
// Extension's own enum
enum ButtonIconSizeEnum {
Sm = 'sm',
Md = 'md',
Lg = 'lg',
}
// All of these work with design-system-react's string union type:
<ButtonIcon size="sm" /> // ✅ String literal
<ButtonIcon size={ButtonIconSize.Sm} /> // ✅ Design system const
<ButtonIcon size={ButtonIconSizeEnum.Sm} /> // ✅ Extension enumNo dual props needed! One prop type works with old enums, new const objects, and string literals.
3. Simplified Migrations
The MenuItem migration would have been much simpler:
type MenuItemProps = {
iconName: IconName; // Single prop accepts all sources!
};
// Works with old enums automatically:
<MenuItem iconName={IconNameLegacy.Trash} /> // ✅
<MenuItem iconName="Trash" /> // ✅4. Better Developer Experience
- String literals work without imports:
<Button size="md" /> - Autocomplete still works with const object references
- Type errors are clearer (shows string values, not enum member names)
- Easier to document (values are self-explanatory strings)
Migration Strategy
Phase 1: Update Design System Exports
For each enum in @metamask/design-system-react:
- Export both string union type and const object with same name
- Remove the enum
- Update internal component prop types
Example:
// Before
export enum ButtonIconSize {
Sm = 'sm',
Md = 'md',
Lg = 'lg',
}
// After
export type ButtonIconSize = 'sm' | 'md' | 'lg';
export const ButtonIconSize = {
Sm: 'sm',
Md: 'md',
Lg: 'lg',
} as const;Phase 2: Consumer Adoption
Extension and mobile teams can migrate gradually:
// Mark old enums as deprecated but keep them
/** @deprecated Use string literals or ButtonIconSize const object */
enum ButtonIconSizeEnum {
Sm = 'sm',
Md = 'md',
Lg = 'lg',
}
// Both work during transition
<ButtonIcon size={ButtonIconSizeEnum.Sm} /> // Still works
<ButtonIcon size="sm" /> // Gradually adoptNo breaking changes - old enum values work with new string union types!
Phase 3: Cleanup
After migration is complete, consumer codebases can:
- Remove deprecated enums
- Adopt string literals for cleaner code
- Or keep using design system const objects
Enums to Migrate
Common enum types that should be converted:
ButtonIconSize,ButtonSizeIconName,IconSize,IconColorTextVariant,TextColor,TextAlignBadgeStatusAvatarSize,AvatarVariantBorderRadius,BackgroundColor- Any other enums in component prop types
References
- MenuItem Migration (PR #39810): refactor: Migrate MenuItem to @metamask/design-system-react metamask-extension#39810
- Demonstrates dual-prop complexity required for enum-to-enum migration
- Shows how wrapper components must support multiple type systems
- MenuItem Storybook Stories (PR #39811): docs: Add Storybook stories for MenuItem refactor components metamask-extension#39811
- Documentation showing the final dual-system implementation
Success Criteria
- All enums in
@metamask/design-system-reactmigrated to string unions - Extension and mobile teams can use old enums during transition
- No breaking changes to existing component APIs
- Documentation updated with migration guide
- TypeScript strict mode compatibility maintained