MetaMask / MetaMask/metamask-design-system
Migrate from enums to string union types for better cross-package compatibility
- Dominant language
- TypeScript
- Stars
- 37
- Forks
- 14
- Avg merge
- 1d 9h
- Merged PRs (30d)
- 60
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**:
```typescript
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:
1. Duplicate props for legacy and new systems
2. Runtime branching to determine which system to use
3. Conditional rendering based on which props are provided
4. Every consumer component must be updated to use the new prop names
This pattern is repeated across hundreds of call sites:
```diff
-
+
```
### 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:
```typescript
// 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:
```typescript
// Type usage (works)
const props: { size: ButtonIconSize } = { size: 'md' };
// Value reference (works)
```
**2. Cross-Package Compatibility**
String unions use structural typing, so they accept values from **any source**:
```typescript
// Extension's own enum
enum ButtonIconSizeEnum {
Sm = 'sm',
Md = 'md',
Lg = 'lg',
}
// All of these work with design-system-react's string union type:
// ✅ String literal
// ✅ Design system const
// ✅ Extension enum
```
**No 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:
```typescript
type MenuItemProps = {
iconName: IconName; // Single prop accepts all sources!
};
// Works with old enums automatically:
// ✅
// ✅
```
**4. Better Developer Experience**
- String literals work without imports: ``
- 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`:
1. Export both string union type and const object with same name
2. Remove the enum
3. Update internal component prop types
**Example:**
```typescript
// 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:
```typescript
// 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
// Still works
// Gradually adopt
```
**No 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`, `ButtonSize`
- `IconName`, `IconSize`, `IconColor`
- `TextVariant`, `TextColor`, `TextAlign`
- `BadgeStatus`
- `AvatarSize`, `AvatarVariant`
- `BorderRadius`, `BackgroundColor`
- Any other enums in component prop types
## References
- **MenuItem Migration (PR #39810):** https://github.com/MetaMask/metamask-extension/pull/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):** https://github.com/MetaMask/metamask-extension/pull/39811
- Documentation showing the final dual-system implementation
## Success Criteria
- [ ] All enums in `@metamask/design-system-react` migrated 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
Contributor guide
Assessment
This issue has not been assessed yet.