394 lines
8.6 KiB
Markdown
394 lines
8.6 KiB
Markdown
|
|
# TypeScript Type Migration Guide
|
||
|
|
|
||
|
|
## Overview
|
||
|
|
|
||
|
|
This guide helps you migrate from the old type patterns to the new unified patterns in `@customer-portal/domain`.
|
||
|
|
|
||
|
|
## Quick Migration Reference
|
||
|
|
|
||
|
|
### Async State Migration
|
||
|
|
|
||
|
|
#### Before (Old Pattern)
|
||
|
|
```typescript
|
||
|
|
// Old way - multiple different patterns
|
||
|
|
interface AsyncState<T> {
|
||
|
|
data: T | null;
|
||
|
|
loading: boolean;
|
||
|
|
error: string | null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage
|
||
|
|
const [state, setState] = useState<AsyncState<User>>({
|
||
|
|
data: null,
|
||
|
|
loading: false,
|
||
|
|
error: null,
|
||
|
|
});
|
||
|
|
|
||
|
|
// Checking state
|
||
|
|
if (state.loading) {
|
||
|
|
return <LoadingSpinner />;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (state.error) {
|
||
|
|
return <ErrorMessage error={state.error} />;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (state.data) {
|
||
|
|
return <UserProfile user={state.data} />;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### After (New Pattern)
|
||
|
|
```typescript
|
||
|
|
// New way - discriminated union
|
||
|
|
import { AsyncState, isLoading, isError, isSuccess } from '@customer-portal/domain';
|
||
|
|
|
||
|
|
// Usage
|
||
|
|
const [state, setState] = useState<AsyncState<User>>({ status: 'idle' });
|
||
|
|
|
||
|
|
// Checking state with type guards
|
||
|
|
if (isLoading(state)) {
|
||
|
|
return <LoadingSpinner />;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isError(state)) {
|
||
|
|
return <ErrorMessage error={state.error} />;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isSuccess(state)) {
|
||
|
|
return <UserProfile user={state.data} />;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Form State Migration
|
||
|
|
|
||
|
|
#### Before (Old Pattern)
|
||
|
|
```typescript
|
||
|
|
interface FormState<T> {
|
||
|
|
data: T;
|
||
|
|
errors: Record<keyof T, string | undefined>;
|
||
|
|
touched: Record<keyof T, boolean>;
|
||
|
|
dirty: boolean;
|
||
|
|
valid: boolean;
|
||
|
|
submitting: boolean;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### After (New Pattern)
|
||
|
|
```typescript
|
||
|
|
import { FormState, createFormState, getFormValues } from '@customer-portal/domain';
|
||
|
|
|
||
|
|
// Create initial form state
|
||
|
|
const initialState = createFormState({
|
||
|
|
email: '',
|
||
|
|
password: '',
|
||
|
|
name: '',
|
||
|
|
});
|
||
|
|
|
||
|
|
// Get form values
|
||
|
|
const formData = getFormValues(formState);
|
||
|
|
```
|
||
|
|
|
||
|
|
### API Response Handling
|
||
|
|
|
||
|
|
#### Before (Multiple Patterns)
|
||
|
|
```typescript
|
||
|
|
// Different response formats everywhere
|
||
|
|
interface WhmcsResponse<T> {
|
||
|
|
result: 'success' | 'error';
|
||
|
|
data?: T;
|
||
|
|
message?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface SalesforceResponse<T> {
|
||
|
|
success: boolean;
|
||
|
|
data?: T;
|
||
|
|
errors?: any[];
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### After (Unified with Adapters)
|
||
|
|
```typescript
|
||
|
|
import {
|
||
|
|
adaptWhmcsResponse,
|
||
|
|
adaptSalesforceResponse,
|
||
|
|
isSuccessResponse,
|
||
|
|
unwrapResponse
|
||
|
|
} from '@customer-portal/domain';
|
||
|
|
|
||
|
|
// Convert different API responses to unified format
|
||
|
|
const whmcsData = adaptWhmcsResponse(whmcsResponse);
|
||
|
|
const sfData = adaptSalesforceResponse(salesforceResponse);
|
||
|
|
|
||
|
|
// Use unified response handling
|
||
|
|
if (isSuccessResponse(whmcsData)) {
|
||
|
|
console.log('WHMCS data:', whmcsData.data);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Or unwrap directly (throws on error)
|
||
|
|
const userData = unwrapResponse(whmcsData);
|
||
|
|
```
|
||
|
|
|
||
|
|
## Step-by-Step Migration Process
|
||
|
|
|
||
|
|
### 1. Update Imports
|
||
|
|
|
||
|
|
Replace old imports:
|
||
|
|
```typescript
|
||
|
|
// ❌ Old
|
||
|
|
import type { AsyncState } from '../types';
|
||
|
|
import type { FormState } from '../utils/ui-state';
|
||
|
|
|
||
|
|
// ✅ New
|
||
|
|
import type { AsyncState, FormState } from '@customer-portal/domain';
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. Update State Initialization
|
||
|
|
|
||
|
|
Replace old state initialization:
|
||
|
|
```typescript
|
||
|
|
// ❌ Old
|
||
|
|
const [userState, setUserState] = useState({
|
||
|
|
data: null,
|
||
|
|
loading: false,
|
||
|
|
error: null,
|
||
|
|
});
|
||
|
|
|
||
|
|
// ✅ New
|
||
|
|
const [userState, setUserState] = useState<AsyncState<User>>({
|
||
|
|
status: 'idle'
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. Update State Transitions
|
||
|
|
|
||
|
|
Replace old state updates:
|
||
|
|
```typescript
|
||
|
|
// ❌ Old
|
||
|
|
setUserState({ data: null, loading: true, error: null });
|
||
|
|
setUserState({ data: user, loading: false, error: null });
|
||
|
|
setUserState({ data: null, loading: false, error: 'Failed to load' });
|
||
|
|
|
||
|
|
// ✅ New
|
||
|
|
import { createLoadingState, createSuccessState, createErrorState } from '@customer-portal/domain';
|
||
|
|
|
||
|
|
setUserState(createLoadingState());
|
||
|
|
setUserState(createSuccessState(user));
|
||
|
|
setUserState(createErrorState('Failed to load'));
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4. Update State Checks
|
||
|
|
|
||
|
|
Replace old state checks:
|
||
|
|
```typescript
|
||
|
|
// ❌ Old
|
||
|
|
if (state.loading) { /* ... */ }
|
||
|
|
if (state.error) { /* ... */ }
|
||
|
|
if (state.data) { /* ... */ }
|
||
|
|
|
||
|
|
// ✅ New
|
||
|
|
import { isLoading, isError, isSuccess } from '@customer-portal/domain';
|
||
|
|
|
||
|
|
if (isLoading(state)) { /* ... */ }
|
||
|
|
if (isError(state)) { /* ... */ }
|
||
|
|
if (isSuccess(state)) { /* ... */ }
|
||
|
|
```
|
||
|
|
|
||
|
|
### 5. Update Form Handling
|
||
|
|
|
||
|
|
Replace old form state:
|
||
|
|
```typescript
|
||
|
|
// ❌ Old
|
||
|
|
const [formState, setFormState] = useState({
|
||
|
|
data: { email: '', password: '' },
|
||
|
|
errors: {},
|
||
|
|
touched: {},
|
||
|
|
dirty: false,
|
||
|
|
valid: true,
|
||
|
|
submitting: false,
|
||
|
|
});
|
||
|
|
|
||
|
|
// ✅ New
|
||
|
|
import { createFormState, updateFormField } from '@customer-portal/domain';
|
||
|
|
|
||
|
|
const [formState, setFormState] = useState(
|
||
|
|
createFormState({ email: '', password: '' })
|
||
|
|
);
|
||
|
|
|
||
|
|
// Update field
|
||
|
|
const updatedState = {
|
||
|
|
...formState,
|
||
|
|
email: updateFormField(formState.email, newValue),
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
## Migration Helpers
|
||
|
|
|
||
|
|
### Temporary Compatibility
|
||
|
|
|
||
|
|
If you need to migrate gradually, use the migration helpers:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { migrateAsyncState, legacyAsyncState } from '@customer-portal/domain';
|
||
|
|
|
||
|
|
// Convert old state to new state
|
||
|
|
const newState = migrateAsyncState(oldState);
|
||
|
|
|
||
|
|
// Convert new state back to old format (for backward compatibility)
|
||
|
|
const oldState = legacyAsyncState(newState);
|
||
|
|
```
|
||
|
|
|
||
|
|
### Custom Hooks Migration
|
||
|
|
|
||
|
|
Update your custom hooks:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// ❌ Old
|
||
|
|
function useUser(id: string) {
|
||
|
|
const [state, setState] = useState({
|
||
|
|
data: null,
|
||
|
|
loading: false,
|
||
|
|
error: null,
|
||
|
|
});
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
setState({ data: null, loading: true, error: null });
|
||
|
|
|
||
|
|
fetchUser(id)
|
||
|
|
.then(user => setState({ data: user, loading: false, error: null }))
|
||
|
|
.catch(error => setState({ data: null, loading: false, error: error.message }));
|
||
|
|
}, [id]);
|
||
|
|
|
||
|
|
return state;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ✅ New
|
||
|
|
import { AsyncState, createLoadingState, createSuccessState, createErrorState } from '@customer-portal/domain';
|
||
|
|
|
||
|
|
function useUser(id: string) {
|
||
|
|
const [state, setState] = useState<AsyncState<User>>({ status: 'idle' });
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
setState(createLoadingState());
|
||
|
|
|
||
|
|
fetchUser(id)
|
||
|
|
.then(user => setState(createSuccessState(user)))
|
||
|
|
.catch(error => setState(createErrorState(error.message)));
|
||
|
|
}, [id]);
|
||
|
|
|
||
|
|
return state;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Benefits of Migration
|
||
|
|
|
||
|
|
### 1. Type Safety
|
||
|
|
- Impossible states are eliminated (can't have `loading: true` and `data: User` at the same time)
|
||
|
|
- Better IntelliSense and autocomplete
|
||
|
|
- Compile-time error detection
|
||
|
|
|
||
|
|
### 2. Consistency
|
||
|
|
- Single pattern across the entire application
|
||
|
|
- Predictable state transitions
|
||
|
|
- Unified error handling
|
||
|
|
|
||
|
|
### 3. Developer Experience
|
||
|
|
- Less cognitive load
|
||
|
|
- Easier testing
|
||
|
|
- Better debugging
|
||
|
|
|
||
|
|
### 4. Performance
|
||
|
|
- Smaller bundle sizes
|
||
|
|
- Better tree-shaking
|
||
|
|
- Reduced memory usage
|
||
|
|
|
||
|
|
## Common Patterns
|
||
|
|
|
||
|
|
### Loading States
|
||
|
|
```typescript
|
||
|
|
// ✅ New pattern
|
||
|
|
function UserProfile({ userId }: { userId: string }) {
|
||
|
|
const userState = useUser(userId);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div>
|
||
|
|
{isLoading(userState) && <LoadingSpinner />}
|
||
|
|
{isError(userState) && <ErrorMessage error={userState.error} />}
|
||
|
|
{isSuccess(userState) && <UserDetails user={userState.data} />}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Form Handling
|
||
|
|
```typescript
|
||
|
|
// ✅ New pattern
|
||
|
|
function LoginForm() {
|
||
|
|
const [formState, setFormState] = useState(
|
||
|
|
createFormState({ email: '', password: '' })
|
||
|
|
);
|
||
|
|
|
||
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
||
|
|
e.preventDefault();
|
||
|
|
const formData = getFormValues(formState);
|
||
|
|
|
||
|
|
try {
|
||
|
|
await login(formData);
|
||
|
|
} catch (error) {
|
||
|
|
// Handle form errors
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<form onSubmit={handleSubmit}>
|
||
|
|
{/* Form fields */}
|
||
|
|
</form>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### API Integration
|
||
|
|
```typescript
|
||
|
|
// ✅ New pattern with adapters
|
||
|
|
async function fetchUserData(id: string) {
|
||
|
|
const whmcsResponse = await whmcsApi.getClient(id);
|
||
|
|
const unifiedResponse = adaptWhmcsResponse(whmcsResponse);
|
||
|
|
|
||
|
|
return unwrapResponse(unifiedResponse);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Troubleshooting
|
||
|
|
|
||
|
|
### Common Issues
|
||
|
|
|
||
|
|
1. **Type Errors After Migration**
|
||
|
|
- Make sure to import types from `@customer-portal/domain`
|
||
|
|
- Use type guards instead of direct property access
|
||
|
|
- Update state initialization to use discriminated unions
|
||
|
|
|
||
|
|
2. **Runtime Errors**
|
||
|
|
- Check that you're using the correct state transition functions
|
||
|
|
- Ensure proper error handling with the new patterns
|
||
|
|
|
||
|
|
3. **Performance Issues**
|
||
|
|
- The new patterns should improve performance
|
||
|
|
- If you see regressions, check for unnecessary re-renders
|
||
|
|
|
||
|
|
### Getting Help
|
||
|
|
|
||
|
|
- Check the implementation examples in `/packages/domain/src/patterns/`
|
||
|
|
- Look at the test files for usage patterns
|
||
|
|
- Refer to the main documentation in `TYPE_STRUCTURE_ANALYSIS_REPORT.md`
|
||
|
|
|
||
|
|
## Next Steps
|
||
|
|
|
||
|
|
After completing the migration:
|
||
|
|
|
||
|
|
1. Remove deprecated type definitions
|
||
|
|
2. Update linting rules to prevent old patterns
|
||
|
|
3. Add tests for the new patterns
|
||
|
|
4. Update team documentation and training materials
|
||
|
|
|
||
|
|
This migration will significantly improve the type safety, consistency, and maintainability of your codebase while providing a better developer experience.
|