python-to-typescript-porting-mcp-server
Version:
Comprehensive MCP server providing systematic tools and references for Python-to-TypeScript porting with real-world examples
469 lines (363 loc) • 13.7 kB
Markdown
<!-- srcbook:{"language":"typescript"} -->
```json
{
"type": "module",
"dependencies": {
"date-fns": "^2.30.0"
},
"devDependencies": {
"typescript": "^5.0.0",
"tsx": "^4.0.0",
"@types/node": "^20.0.0"
}
}
```
This notebook demonstrates migrating common utility functions using Python 3.9+ features. This is an excellent starting point for understanding the tool workflow with simpler, low-risk conversions.
**Complexity Level:** Low (perfect for incremental migration start)
**Risk Level:** Minimal (pure functions, no external dependencies)
**Tools Needed:**
- `type-analysis` - For basic type mappings
- `pattern-mapping` - For list comprehensions and string operations
- Quick validation testing
Simple but modern Python 3.9+ utility functions showcasing clean syntax:
```python
from datetime import datetime
from pathlib import Path
StringList = list[str]
NumberDict = dict[str, int | float]
OptionalString = str | None
def clean_and_validate_strings(
inputs: list[str | None],
min_length: int = 1
) -> list[str]:
"""Clean and validate string inputs using Python 3.9+ features"""
# List comprehension with union type handling
cleaned = [
s.strip()
for s in inputs
if s is not None and isinstance(s, str) and len(s.strip()) >= min_length
]
return cleaned
def merge_user_preferences(
defaults: dict[str, str | int | bool],
user_prefs: dict[str, str | int | bool] | None = None
) -> dict[str, str | int | bool]:
"""Merge user preferences with defaults using Python 3.9+ dict merge"""
if user_prefs is None:
return defaults.copy()
# Python 3.9+ dict merge operator
return defaults | user_prefs
def calculate_stats(numbers: list[int | float]) -> dict[str, float]:
"""Calculate basic statistics with modern type hints"""
if not numbers:
return {'count': 0.0, 'mean': 0.0, 'min': 0.0, 'max': 0.0}
return {
'count': float(len(numbers)),
'mean': sum(numbers) / len(numbers),
'min': float(min(numbers)),
'max': float(max(numbers))
}
def format_file_sizes(
file_paths: list[str | Path]
) -> dict[str, str | None]:
"""Format file sizes with path handling"""
result: dict[str, str | None] = {}
for path in file_paths:
try:
path_obj = Path(path) if isinstance(path, str) else path
if path_obj.exists() and path_obj.is_file():
size_bytes = path_obj.stat().st_size
if size_bytes < 1024:
formatted = f"{size_bytes} B"
elif size_bytes < 1024 * 1024:
formatted = f"{size_bytes / 1024:.1f} KB"
else:
formatted = f"{size_bytes / (1024 * 1024):.1f} MB"
result[str(path_obj)] = formatted
else:
result[str(path)] = None
except Exception:
result[str(path)] = None
return result
def group_by_prefix(
items: list[str],
separator: str = "_"
) -> dict[str, list[str]]:
"""Group strings by prefix using modern Python 3.9+ syntax"""
grouped: dict[str, list[str]] = {}
for item in items:
if separator in item:
prefix = item.split(separator, 1)[0]
else:
prefix = "no_prefix"
if prefix not in grouped:
grouped[prefix] = []
grouped[prefix].append(item)
return grouped
def demo():
# Test data with union types
mixed_strings: list[str | None] = [" hello ", None, "world", "", " test "]
clean = clean_and_validate_strings(mixed_strings)
defaults = {"theme": "light", "notifications": True, "timeout": 30}
user_settings = {"theme": "dark", "language": "en"}
merged = merge_user_preferences(defaults, user_settings)
numbers = [1, 2, 3, 4, 5]
stats = calculate_stats(numbers)
return clean, merged, stats
```
Using `type-analysis` tool for the modern Python types:
**Simple Type Mappings:**
- `str | None` → `string | null` ✨ Perfect!
- `list[str]` → `string[]` ✨ Clean!
- `dict[str, int | float]` → `Record<string, number>` ✨ Direct!
- `list[str | Path]` → `(string | Path)[]` ✨ Union alignment!
**Migration Complexity:** Trivial to Simple
Using `pattern-mapping` tool for conversion patterns:
**Python:** `[s.strip() for s in inputs if s is not None and len(s) >= min_length]`
**TypeScript:** `.filter().map()` chain
**Complexity:** Simple
**Python:** `defaults | user_prefs` ✨
**TypeScript:** `{...defaults, ...user_prefs}`
**Complexity:** Simple (perfect pattern alignment!)
```typescript
// Type definitions derived from Python 3.9+ types
export type StringList = string[];
export type NumberDict = Record<string, number>;
export type OptionalString = string | null;
export interface FileStats {
count: number;
mean: number;
min: number;
max: number;
}
export interface UserPreferences {
[]: string | number | boolean;
}
```
```typescript
import { stat } from 'node:fs/promises';
import { resolve } from 'node:path';
// Clean and validate strings (Python list comprehension → TypeScript filter/map)
export function cleanAndValidateStrings(
inputs: (string | null)[], // Python list[str | None] → (string | null)[]
minLength: number = 1
): string[] {
// Python list comprehension → TypeScript filter + map chain
return inputs
.filter((s): s is string => s !== null && typeof s === 'string') // Type guard
.map(s => s.trim())
.filter(s => s.length >= minLength);
}
// Merge user preferences (Python 3.9+ dict merge → TypeScript spread)
export function mergeUserPreferences(
defaults: UserPreferences,
userPrefs?: UserPreferences | null // Python | None → | null
): UserPreferences {
if (!userPrefs) {
return { ...defaults }; // Copy defaults
}
// Python: defaults | user_prefs → TypeScript: {...defaults, ...userPrefs}
return { ...defaults, ...userPrefs };
}
// Calculate statistics (direct translation with proper typing)
export function calculateStats(numbers: number[]): FileStats {
if (numbers.length === 0) {
return { count: 0, mean: 0, min: 0, max: 0 };
}
return {
count: numbers.length,
mean: numbers.reduce((sum, n) => sum + n, 0) / numbers.length,
min: Math.min(...numbers),
max: Math.max(...numbers)
};
}
// Format file sizes (async for Node.js file operations)
export async function formatFileSizes(
filePaths: string[]
): Promise<Record<string, string | null>> {
const result: Record<string, string | null> = {};
for (const filePath of filePaths) {
try {
const fullPath = resolve(filePath);
const stats = await stat(fullPath);
if (stats.isFile()) {
const sizeBytes = stats.size;
// Simple size formatting (same logic as Python)
let formatted: string;
if (sizeBytes < 1024) {
formatted = `${sizeBytes} B`;
} else if (sizeBytes < 1024 * 1024) {
formatted = `${(sizeBytes / 1024).toFixed(1)} KB`;
} else {
formatted = `${(sizeBytes / (1024 * 1024)).toFixed(1)} MB`;
}
result[fullPath] = formatted;
} else {
result[filePath] = null;
}
} catch {
result[filePath] = null;
}
}
return result;
}
// Group by prefix (Python dict operations → TypeScript Record operations)
export function groupByPrefix(
items: string[],
separator: string = "_"
): Record<string, string[]> {
const grouped: Record<string, string[]> = {};
for (const item of items) {
const prefix = item.includes(separator)
? item.split(separator, 1)[0]
: "no_prefix";
if (!grouped[prefix]) {
grouped[prefix] = [];
}
grouped[prefix].push(item);
}
return grouped;
}
```
```typescript
import {
cleanAndValidateStrings,
mergeUserPreferences,
calculateStats,
formatFileSizes,
groupByPrefix
} from './utils.js';
import type { UserPreferences } from './types.js';
// Demo function showing converted functionality
export async function runDemo() {
console.log('🚀 Running TypeScript Utility Demo...');
// Test 1: String cleaning (Python list comprehension → TypeScript filter/map)
console.log('\n1. String Cleaning:');
const mixedStrings: (string | null)[] = [" hello ", null, "world", "", " test "];
const cleaned = cleanAndValidateStrings(mixedStrings);
console.log('Input:', mixedStrings);
console.log('Cleaned:', cleaned);
// Test 2: Preference merging (Python 3.9+ dict merge → TypeScript spread)
console.log('\n2. Preference Merging:');
const defaults: UserPreferences = { theme: "light", notifications: true, timeout: 30 };
const userSettings: UserPreferences = { theme: "dark", language: "en" };
const merged = mergeUserPreferences(defaults, userSettings);
console.log('Defaults:', defaults);
console.log('User settings:', userSettings);
console.log('Merged:', merged);
// Test 3: Statistics calculation
console.log('\n3. Statistics:');
const numbers = [1, 2, 3, 4, 5];
const stats = calculateStats(numbers);
console.log('Numbers:', numbers);
console.log('Stats:', stats);
// Test 4: File size formatting (async in TypeScript)
console.log('\n4. File Size Formatting:');
try {
const filePaths = ['./package.json', './nonexistent.txt'];
const sizes = await formatFileSizes(filePaths);
console.log('File sizes:', sizes);
} catch (error) {
console.log('File size error:', error);
}
// Test 5: Grouping by prefix
console.log('\n5. Grouping by Prefix:');
const items = ['user_alice', 'user_bob', 'admin_charlie', 'guest_diana', 'standalone'];
const grouped = groupByPrefix(items);
console.log('Items:', items);
console.log('Grouped:', grouped);
console.log('\n✅ Demo completed successfully!');
}
// Run the demo
runDemo().catch(console.error);
```
```typescript
import {
cleanAndValidateStrings,
mergeUserPreferences,
calculateStats,
groupByPrefix
} from './utils.js';
// Quick tests to validate conversion correctness
function runQuickTests() {
console.log('🧪 Running Quick Validation Tests...');
// Test 1: String cleaning edge cases
console.log('\nTest 1: String cleaning');
const result1 = cleanAndValidateStrings([null, "", " ", "valid"], 1);
console.log(`✅ Cleaned result: ${JSON.stringify(result1)} (expected: ["valid"])`);
// Test 2: Preference merging overrides
console.log('\nTest 2: Preference merging');
const base = { a: 1, b: 2 };
const override = { b: 3, c: 4 };
const merged = mergeUserPreferences(base, override);
console.log(`✅ Merged b=${merged.b} (expected: 3), c=${merged.c} (expected: 4)`);
// Test 3: Empty array stats
console.log('\nTest 3: Empty array statistics');
const emptyStats = calculateStats([]);
console.log(`✅ Empty stats count: ${emptyStats.count} (expected: 0)`);
// Test 4: Basic grouping
console.log('\nTest 4: Basic grouping');
const grouped = groupByPrefix(['a_1', 'a_2', 'b_1']);
const aGroup = grouped['a'];
console.log(`✅ Group 'a' length: ${aGroup?.length} (expected: 2)`);
console.log('\n✅ All quick tests passed!');
}
// Run tests
runQuickTests();
```
**Type Syntax Alignment:**
- `str | None` → `string | null` (identical!)
- `list[str]` → `string[]` (clean and direct)
- `dict[str, int]` → `Record<string, number>` (clear mapping)
**Pattern Alignment:**
- Dict merge `|` → Object spread `{...}` (very similar)
- List comprehensions → `.filter().map()` (readable conversion)
- Type guards translate naturally
**Runtime vs Compile-time:**
- Python type hints → TypeScript static types
- Runtime `isinstance()` → Compile-time type guards
**Async Operations:**
- File operations became async (Node.js pattern)
- Promise handling for file stats
**Library Differences:**
- Python `pathlib.Path` → Node.js `path` module
- Different but equivalent functionality
### Migration Strategy Success
This demonstrates the **incremental approach** working perfectly:
1. **Low risk** - Pure utility functions
2. **High confidence** - Simple type mappings
3. **Easy validation** - Pure function testing
4. **Quick wins** - Immediate productivity
### Python 3.9+ Advantages for Migration
✨ **Syntax Harmony:** Modern Python aligns beautifully with TypeScript
✨ **Type Clarity:** Built-in generics eliminate import confusion
✨ **Pattern Consistency:** Union types and dict operations translate cleanly
This example shows how Python 3.9+ creates an ideal migration starting point with simple, reliable conversions that build confidence for tackling more complex modules.