TypeScript Migration Skill
When to Use This Skill
- Converting .js/.jsx files to .ts/.tsx
- Adding types to existing JavaScript code
- Typing React props, state, hooks, and events
- Typing API responses and data models
- Fixing TypeScript errors in existing code
Migration Strategy
Always migrate incrementally — one file at a time. Never convert the whole project at once.
1. Rename file: Component.jsx → Component.tsx
2. Fix immediate errors only
3. Add types gradually — use 'any' temporarily if stuck
4. Remove 'any' types in a second pass
Step 1 — Props Interfaces
// ❌ JavaScript — no type safety
function UserCard({ name, age, email, onClick, isAdmin }) {
return <div
}
// ✅ TypeScript — fully typed
interface UserCardProps {
name: string;
age: number;
email: string;
onClick: () => void;
isAdmin?: boolean; // optional prop
children?: React.ReactNode; // children is always optional
}
function UserCard({ name, age, email, onClick, isAdmin = false }: UserCardProps) {
return <div
}
AI instruction: Every React component MUST have a Props interface. Optional props use ?. Children use React.ReactNode.
Step 2 — useState with Types
// ❌ Inferred as never[] — breaks immediately
const [items, setItems] = useState([]);
// ✅ Explicit generic
const [items, setItems] = useState<Item[]>([]);
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const [count, setCount] = useState<number>(0);
Step 3 — useRef Types
// ❌ Untyped ref
const inputRef = useRef(null);
// ✅ DOM element ref
const inputRef = useRef<HTMLInputElement>(null);
const divRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
// ✅ Mutable ref (for values, not DOM)
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const countRef = useRef<number>(0);
Step 4 — Event Handlers
// ❌ any event
const handleChange = (e: any) => setValue(e.target.value);
// ✅ Specific event types
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
};
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') submit();
};
Step 5 — API Response Types
// ❌ Untyped — data is any
const fetchUser = async (id: string) => {
const res = await fetch(`/api/users/${id}`);
const data = await res.json();
return data;
};
// ✅ Typed response
interface User {
id: string;
name: string;
email: string;
createdAt: string;
role: 'admin' | 'user' | 'viewer';
}
interface ApiResponse<T> {
data: T;
error: string | null;
status: number;
}
const fetchUser = async (id: string): Promise<User> => {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`Failed: ${res.status}`);
const json: ApiResponse<User> = await res.json();
return json.data;
};
Step 6 — Custom Hooks
// ❌ Untyped custom hook
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(initialValue);
// ...
return [value, setValue];
}
// ✅ Generic typed hook
function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = (value: T) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
Step 7 — Converting PropTypes → TypeScript
// PropTypes (before)
UserCard.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number.isRequired,
email: PropTypes.string,
onClick: PropTypes.func.isRequired,
role: PropTypes.oneOf(['admin', 'user']),
};
// TypeScript interface (after)
interface UserCardProps {
name: string;
age: number;
email?: string;
onClick: () => void;
role?: 'admin' | 'user';
}
Common Types Reference
// Children
children: React.ReactNode // anything renderable
children: React.ReactElement // only JSX elements
children: JSX.Element // single JSX element
// Style
style?: React.CSSProperties
// HTML props passthrough
props: React.HTMLAttributes<HTMLDivElement>
inputProps: React.InputHTMLAttributes<HTMLInputElement>
// Refs
ref: React.RefObject<HTMLInputElement>
ref: React.MutableRefObject<number>
// Context
const ThemeContext = createContext<Theme | null>(null);
// Discriminated unions
type Status =
| { state: 'loading' }
| { state: 'success'; data: User[] }
| { state: 'error'; message: string };
Companion Script
npx reactforge gen-typescript ./src
Reads existing PropTypes and generates TypeScript interfaces automatically.
Migration Order (recommended)
- Types/interfaces files first (
types.ts) - Utility functions (pure functions are easiest)
- Custom hooks
- Leaf components (no children)
- Container components
- Root/App component last