This guide walks you through the process of migrating PolyCode IDE from JavaScript/JSX to TypeScript/TSX. TypeScript provides:
- Type Safety: Catch errors at compile-time instead of runtime
- Better IDE Support: Enhanced IntelliSense, autocompletion, and refactoring
- Improved Maintainability: Self-documenting code through type annotations
- Early Bug Detection: Catch common mistakes before they reach production
Ensure you have installed all TypeScript dependencies:
npm install --save-dev typescript @types/node @types/react @types/react-dom
npm install --save-dev ts-loader @babel/preset-typescript fork-ts-checker-webpack-plugin- TypeScript configured (
tsconfig.json) - Webpack builds TypeScript
- Type checking in build process
- Core files migrated (~20% complete)
- All components migrated
- All utility files migrated
- All tests migrated
The tsconfig.json is configured with:
- Strict mode enabled for maximum type safety
- ES2020 target for modern JavaScript features
- React JSX transform for seamless React integration
- Source maps for debugging
- Declaration files for better editor support
# Rename .js to .ts for regular files
mv src/main/file.js src/main/file.ts
# Rename .jsx to .tsx for React components
mv src/renderer/components/File.jsx src/renderer/components/File.tsxBefore (JavaScript):
function add(a, b) {
return a + b;
}After (TypeScript):
function add(a: number, b: number): number {
return a + b;
}Before (JSX):
function Editor({ filePath, content, onSave }) {
const [value, setValue] = useState('');
return <div>{value}</div>;
}After (TSX):
interface EditorProps {
filePath: string;
content: string;
onSave: (path: string, content: string) => void;
}
const Editor: React.FC<EditorProps> = ({ filePath, content, onSave }) => {
const [value, setValue] = useState<string>('');
return <div>{value}</div>;
};useState:
const [count, setCount] = useState<number>(0);
const [items, setItems] = useState<ItemType[]>([]);
const [user, setUser] = useState<User | null>(null);useEffect:
useEffect(() => {
const fetchData = async () => {
const result = await api.getData();
setData(result);
};
fetchData();
}, []); // Empty dependency array: runs once on mountuseRef:
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
const timerRef = useRef<NodeJS.Timeout | null>(null);useCallback:
const handleSave = useCallback(
(content: string) => {
onSave(filePath, content);
},
[filePath, onSave]
);const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
// Handle click
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// Handle form submission
};import { ipcMain, IpcMainInvokeEvent } from 'electron';
ipcMain.handle('generate-code', async (
event: IpcMainInvokeEvent,
{ prompt, context }: { prompt: string; context?: string }
): Promise<{ success: boolean; data?: CodeGenerationResult; error?: string }> => {
try {
const result = await orchestrator.generateCode(prompt, context);
return { success: true, data: result };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});interface GenerateResult {
code: string;
isMultiFile: boolean;
files?: Record<string, string>;
model: string;
score: number;
deliberation?: DeliberationData;
}
interface DeliberationData {
rounds: number;
totalGenerations: number;
totalEvaluations: number;
}interface EditorProps {
filePath: string;
content: string;
language: string;
previousContent?: string;
onSave: (path: string, content: string) => void;
onContentChange?: (content: string) => void;
onRun?: (code: string) => void;
isDirty: boolean;
onDirtyChange: (isDirty: boolean) => void;
}
interface AIPanelProps {
activeFile: string | null;
code: string;
language: string;
models: string[];
isConnected: boolean;
files?: Record<string, string>;
onCodeGenerated: (result: GenerateResult) => void;
onDeliberationUpdate?: (messages: DeliberationMessage[]) => void;
}Solution 1: Type assertion (use sparingly):
const data = someValue as ExpectedType;Solution 2: Proper interface:
interface ExpectedType {
x: string;
y: number;
}Solution 3: Index signature (when object structure is dynamic):
const data: Record<string, unknown> = someValue;Solution: Define the type:
// Bad
function process(data: any) { }
// Good
interface Data {
id: string;
value: number;
}
function process(data: Data) { }Solution 1: Non-null assertion (only when you're certain):
const value = potentiallyNull!.someProperty;Solution 2: Optional chaining:
const value = potentiallyNull?.someProperty;Solution 3: Null check:
if (potentiallyNull) {
const value = potentiallyNull.someProperty;
}Solution: Install @types package:
npm install --save-dev @types/package-nameSolution: Check your types are compatible:
// Ensure the interface matches the actual data structure
interface Expected {
name: string;
age: number;
}
// Use Partial if some fields are optional
function update(data: Partial<Expected>) { }- Prefer Interfaces over Type Aliases for object shapes
- Use strict mode to catch more errors
- Avoid 'any' type - use
unknownor proper types instead - Document types with comments for complex structures
- Use utility types like
Partial<T>,Pick<T>,Omit<T> - Keep components small - easier to type and maintain
- Type props explicitly even with React.FC for clarity
- Use readonly for immutable arrays/objects
For each file you migrate:
- Rename file extension (.js → .ts, .jsx → .tsx)
- Add type annotations to all function parameters
- Add return type annotations to functions
- Create interfaces for component props
- Type all useState hooks
- Type all useRef hooks
- Type all useCallback and useMemo dependencies
- Type event handlers
- Fix import/require statements
- Run
npm run buildto check for type errors - Test the application still works
# Type check all files
npx tsc --noEmit
# Type check with watch mode
npx tsc --noEmit --watch
# Build with type checking (webpack will do this automatically)
npm run build:rendererinterface ApiResponse<T> {
data: T;
success: boolean;
error?: string;
}
async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
// Implementation
}type Status = 'loading' | 'success' | 'error';
type FileOrFolder = File | Folder;interface SuccessResponse {
status: 'success';
data: string;
}
interface ErrorResponse {
status: 'error';
error: string;
}
type Response = SuccessResponse | ErrorResponse;
function handleResponse(response: Response) {
if (response.status === 'success') {
// TypeScript knows response.data exists here
console.log(response.data);
} else {
// TypeScript knows response.error exists here
console.error(response.error);
}
}- ✅
src/main/main.ts- Main Electron process - ✅
src/renderer/components/Editor.tsx- Code editor component - ✅
src/renderer/components/AIPanel.tsx- AI panel component
src/renderer/app.tsxsrc/renderer/components/ProjectPanel.tsxsrc/renderer/components/Settings.tsxsrc/renderer/components/SaveDialog.tsxsrc/renderer/components/Welcome.tsxsrc/renderer/utils/monaco-config.tssrc/renderer/utils/shortcuts.tssrc/main/core/*.ts(all core modules)src/main/preload.ts
- Use
anyas a temporary fix if stuck on complex types, but document it - Run tests after migrating each file to ensure functionality is preserved
- Check the console for type errors during development
- Incremental migration is fine - you can migrate file by file
- The build process will fail if there are type errors (strict mode)