Agents convert-python-typescript
Bidirectional conversion between Python and Typescript. Use when migrating projects between these languages in either direction. Extends meta-convert-dev with Python↔Typescript specific patterns. Use when migrating Python projects to TypeScript, translating Pythonic patterns to TypeScript idioms, or refactoring Python codebases into TypeScript. Extends meta-convert-dev with Python-to-TypeScript specific patterns.
git clone https://github.com/aRustyDev/agents
T=$(mktemp -d) && git clone --depth=1 https://github.com/aRustyDev/agents "$T" && mkdir -p ~/.claude/skills && cp -r "$T/content/skills/convert-python-typescript" ~/.claude/skills/arustydev-agents-convert-python-typescript && rm -rf "$T"
content/skills/convert-python-typescript/SKILL.mdPython ↔ Typescript Conversion
Bidirectional conversion between Python and Typescript. This skill extends
meta-convert-dev with Python↔Typescript specific type mappings, idiom translations, and tooling.
This Skill Extends
- Foundational conversion patterns (APTV workflow, testing strategies)meta-convert-dev
For general concepts like the Analyze → Plan → Transform → Validate workflow, testing strategies, and common pitfalls, see the meta-skill first.
This Skill Adds
- Type mappings: Python types → TypeScript types (with runtime to static typing)
- Idiom translations: Pythonic patterns → TypeScript idioms
- Error handling: Python exceptions → TypeScript error patterns
- Async patterns: asyncio/async-await → Promise/async-await
- Type system differences: Dynamic duck typing → static structural typing
This Skill Does NOT Cover
- General conversion methodology - see
meta-convert-dev - Python language fundamentals - see
lang-python-dev - TypeScript language fundamentals - see
lang-typescript-patterns-dev
Quick Reference
| Python | TypeScript | Notes |
|---|---|---|
| | Unicode strings |
| / | Regular numbers or large integers |
| | Floating point |
| | Same concept |
| / | Mutable array |
| / | Fixed-size tuple |
| / | Key-value mapping |
/ | / | Nullable types |
| | Async operations |
/ | / | Structure definition |
/ | / object | Enumerated values |
When Converting Code
- Analyze source thoroughly before writing TypeScript
- Map types first - Python type hints → TypeScript types
- Preserve semantics over syntax similarity
- Adopt TypeScript idioms - don't write "Python code in TypeScript syntax"
- Handle edge cases - None → null/undefined, falsy values
- Test equivalence - same inputs → same outputs
Type System Mapping
Primitive Types
| Python | TypeScript | Notes |
|---|---|---|
| | UTF-16 in TS, but similar usage |
| | For safe integers (-2^53 to 2^53) |
| | For arbitrary precision (use when needed) |
| | IEEE 754 double precision |
| | Lowercase (true/false) in TS |
| | Explicit null |
| | Implicit absence (default) |
| - | | Function return type (no value) |
| | Escape hatch (avoid when possible) |
| | Type-safe any |
| | Function never returns |
Collection Types
| Python | TypeScript | Notes |
|---|---|---|
| / | Mutable, ordered |
| | Immutable sequence |
| | Fixed-size tuple |
| | Unique values, unordered |
| | Key-value with object keys |
| | String-keyed object |
| | Index signature |
| | Immutable set |
| | Immutable map view |
Composite Types
| Python | TypeScript | Notes |
|---|---|---|
| | Data structure |
| | With methods/behavior |
| | Structural typing |
| | Data-only classes |
| | Typed dict structure |
| | Union types (3.10+) |
| | Union types (pre-3.10) |
| - | | Intersection (combine fields) |
| / | Nullable |
| | String literal union |
Generic Types
| Python | TypeScript | Notes |
|---|---|---|
(3.12+) / | | Generic type parameter |
| | Bounded type variable |
| | Constrained type |
| | Function signature |
| | Variadic function |
| | Type of class |
| - | | Union of property keys |
| - | | Index access type |
Module System Translation
Python uses a straightforward import system, while TypeScript has ES modules with explicit imports/exports.
Import and Export Patterns
Python:
# math_utils.py - Python module def add(a: float, b: float) -> float: return a + b def multiply(a: float, b: float) -> float: return a * b # Explicit public API (optional) __all__ = ['add', 'multiply'] # Private function (convention) def _internal_helper(): pass
TypeScript:
// mathUtils.ts - TypeScript module export function add(a: number, b: number): number { return a + b; } export function multiply(a: number, b: number): number { return a * b; } // Private function (not exported) function internalHelper() { // ... } // Default export (Python has no equivalent) export default class Calculator { // ... }
Import Patterns
Python:
# Named imports from math_utils import add, multiply # Module import import math_utils # Import everything (not recommended) from math_utils import * # Import with alias from math_utils import add as addition # Relative imports from . import sibling_module from .. import parent_module from ..utils import helper
TypeScript:
// Named imports import { add, multiply } from './mathUtils'; // Default import import Calculator from './mathUtils'; // Import with alias import { add as addition } from './mathUtils'; // Namespace import import * as MathUtils from './mathUtils'; // Side-effect only import import './polyfills'; // Type-only imports (erased at runtime) import type { User, Config } from './types';
Package Structure
Python:
# mypackage/ # ├── __init__.py # Package initialization # ├── core.py # └── utils.py # mypackage/__init__.py """Package docstring.""" __version__ = "1.0.0" __all__ = ["CoreClass", "utility_function"] from .core import CoreClass from .utils import utility_function
TypeScript:
// mypackage/ // ├── index.ts // Barrel export (like __init__.py) // ├── core.ts // └── utils.ts // mypackage/index.ts export { CoreClass } from './core'; export { utilityFunction } from './utils'; export type { Config } from './types'; // Or re-export all export * from './core'; export * from './utils';
Dynamic Imports
Python:
# Dynamic import import importlib module_name = "json" json_module = importlib.import_module(module_name) # Lazy imports def expensive_operation(): import heavy_module # Only loaded when function called return heavy_module.process() # Conditional imports (type checking) from typing import TYPE_CHECKING if TYPE_CHECKING: from expensive_module import ExpensiveClass
TypeScript:
// Dynamic import (returns Promise) async function loadModule() { const module = await import('./heavyModule'); return module.process(); } // Conditional type import import type { ExpensiveClass } from './expensiveModule'; // Dynamic import with type type MathUtils = typeof import('./mathUtils');
Error Handling Translation
Python Exception Model → TypeScript Error Patterns
Both languages use exception-based error handling, but TypeScript also supports Result-like patterns.
| Aspect | Python | TypeScript |
|---|---|---|
| Base class | | |
| Try-catch | | |
| Throwing | | |
| Re-throwing | (without args) | (without args) |
| Type checking | or specific except | |
Exception Translation
Python:
class AppError(Exception): """Base exception for application errors.""" def __init__(self, message: str, code: str): super().__init__(message) self.code = code class NotFoundError(AppError): """Raised when a resource is not found.""" def __init__(self, resource: str): super().__init__(f"{resource} not found", "NOT_FOUND") self.resource = resource class ValidationError(AppError): """Raised when validation fails.""" def __init__(self, message: str, errors: list[str]): super().__init__(message, "VALIDATION_ERROR") self.errors = errors
TypeScript:
class AppError extends Error { constructor(message: string, public code: string) { super(message); this.name = this.constructor.name; // Maintain proper prototype chain Object.setPrototypeOf(this, new.target.prototype); } } class NotFoundError extends AppError { constructor(public resource: string) { super(`${resource} not found`, "NOT_FOUND"); } } class ValidationError extends AppError { constructor(message: string, public errors: string[]) { super(message, "VALIDATION_ERROR"); } }
Error Handling Patterns
Python:
def load_config(path: str) -> Config: try: with open(path) as f: content = f.read() data = json.loads(content) return Config(**data) except FileNotFoundError: raise NotFoundError(path) from None except json.JSONDecodeError as e: raise ValidationError(f"Invalid JSON in {path}") from e except Exception: # Re-raise unexpected errors raise # Usage try: config = load_config("config.json") except NotFoundError as e: print(f"Config not found: {e.resource}") except ValidationError as e: print(f"Invalid config: {e.errors}")
TypeScript:
function loadConfig(path: string): Config { try { const content = fs.readFileSync(path, 'utf-8'); const data = JSON.parse(content); return data as Config; } catch (error) { if (error.code === 'ENOENT') { throw new NotFoundError(path); } else if (error instanceof SyntaxError) { throw new ValidationError(`Invalid JSON in ${path}`, [error.message]); } else { // Re-throw unexpected errors throw error; } } } // Usage try { const config = loadConfig('config.json'); } catch (error) { if (error instanceof NotFoundError) { console.error(`Config not found: ${error.resource}`); } else if (error instanceof ValidationError) { console.error(`Invalid config: ${error.errors}`); } else { throw error; } }
Result Pattern (Alternative to Exceptions)
Python:
from dataclasses import dataclass from typing import Generic, TypeVar T = TypeVar('T') E = TypeVar('E') @dataclass class Ok(Generic[T]): value: T @dataclass class Err(Generic[E]): error: E Result = Ok[T] | Err[E] def divide(a: float, b: float) -> Result[float, str]: if b == 0: return Err("Division by zero") return Ok(a / b) # Usage result = divide(10, 2) match result: case Ok(value): print(f"Result: {value}") case Err(error): print(f"Error: {error}")
TypeScript:
// Result type pattern type Result<T, E> = | { success: true; data: T } | { success: false; error: E }; function divide(a: number, b: number): Result<number, string> { if (b === 0) { return { success: false, error: "Division by zero" }; } return { success: true, data: a / b }; } // Usage const result = divide(10, 2); if (result.success) { console.log(`Result: ${result.data}`); } else { console.error(`Error: ${result.error}`); }
Concurrency Patterns
Python Async/Await → TypeScript Promise/Async-Await
Both languages support async/await, but with different runtimes and patterns.
| Aspect | Python | TypeScript |
|---|---|---|
| Runtime | asyncio event loop (explicit) | V8 event loop (built-in) |
| Promise/Future | | |
| Concurrent | | |
| Race | | |
| Timeout | | + timeout |
Basic Async Functions
Python:
import asyncio import aiohttp async def fetch_user(id: str) -> User: async with aiohttp.ClientSession() as session: async with session.get(f'/api/users/{id}') as response: if not response.ok: raise Exception(f"Failed to fetch user {id}") data = await response.json() return User(**data) # Entry point requires event loop async def main(): user = await fetch_user('123') print(user.name) if __name__ == '__main__': asyncio.run(main())
TypeScript:
async function fetchUser(id: string): Promise<User> { const response = await fetch(`/api/users/${id}`); if (!response.ok) { throw new Error(`Failed to fetch user ${id}`); } return await response.json(); } // Can await at top level (in modules) const user = await fetchUser('123'); console.log(user.name); // Or in async function async function main() { const user = await fetchUser('123'); console.log(user.name); } main();
Concurrent Execution
Python:
# Sequential (slow) user1 = await fetch_user('1') user2 = await fetch_user('2') # Concurrent with gather users = await asyncio.gather( fetch_user('1'), fetch_user('2'), fetch_user('3') ) # With TaskGroup (Python 3.11+) async with asyncio.TaskGroup() as tg: task1 = tg.create_task(fetch_user('1')) task2 = tg.create_task(fetch_user('2')) task3 = tg.create_task(fetch_user('3')) users = [task1.result(), task2.result(), task3.result()] # Handle errors separately results = await asyncio.gather( fetch_user('1'), fetch_user('2'), return_exceptions=True ) for result in results: if isinstance(result, Exception): print(f"Error: {result}")
TypeScript:
// Sequential (slow) const user1 = await fetchUser('1'); const user2 = await fetchUser('2'); // Concurrent with Promise.all const users = await Promise.all([ fetchUser('1'), fetchUser('2'), fetchUser('3') ]); // Handle errors separately with allSettled const results = await Promise.allSettled([ fetchUser('1'), fetchUser('2'), fetchUser('3') ]); for (const result of results) { if (result.status === 'fulfilled') { console.log('User:', result.value); } else { console.error('Error:', result.reason); } }
Async Iteration
Python:
from typing import AsyncIterator async def generate_pages(start: int, end: int) -> AsyncIterator[Page]: for i in range(start, end + 1): yield await fetch_page(i) # Async iteration async for page in generate_pages(1, 10): process(page) # Async comprehension pages = [page async for page in generate_pages(1, 10)]
TypeScript:
async function* generatePages(start: number, end: number): AsyncIterable<Page> { for (let i = start; i <= end; i++) { yield await fetchPage(i); } } // Async iteration for await (const page of generatePages(1, 10)) { process(page); } // Convert to array const pages: Page[] = []; for await (const page of generatePages(1, 10)) { pages.push(page); }
Timeout and Cancellation
Python:
# Timeout with wait_for try: user = await asyncio.wait_for(fetch_user('1'), timeout=5.0) except asyncio.TimeoutError: print("Request timed out") # Timeout with timeout context manager (3.11+) try: async with asyncio.timeout(5.0): user = await fetch_user('1') except asyncio.TimeoutError: print("Request timed out") # Cancellation task = asyncio.create_task(fetch_user('1')) # Later... task.cancel() try: await task except asyncio.CancelledError: print("Task was cancelled")
TypeScript:
// Timeout with Promise.race async function fetchWithTimeout<T>( promise: Promise<T>, ms: number ): Promise<T> { const timeout = new Promise<never>((_, reject) => setTimeout(() => reject(new Error('Timeout')), ms) ); return Promise.race([promise, timeout]); } try { const user = await fetchWithTimeout(fetchUser('1'), 5000); } catch (error) { if (error.message === 'Timeout') { console.error('Request timed out'); } } // Cancellation with AbortController const controller = new AbortController(); const signal = controller.signal; // Later... controller.abort(); // Use with fetch const response = await fetch(url, { signal });
Metaprogramming Translation
Decorators
Python:
from functools import wraps from typing import Callable, TypeVar, ParamSpec P = ParamSpec('P') T = TypeVar('T') # Function decorator def log_calls(func: Callable[P, T]) -> Callable[P, T]: @wraps(func) def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: print(f"Calling {func.__name__}") result = func(*args, **kwargs) print(f"Finished {func.__name__}") return result return wrapper @log_calls def process_data(data: list) -> int: return len(data) # Class decorator def singleton(cls): instances = {} @wraps(cls) def get_instance(*args, **kwargs): if cls not in instances: instances[cls] = cls(*args, **kwargs) return instances[cls] return get_instance @singleton class Database: pass
TypeScript:
// Function decorator (experimental) function logCalls( target: any, propertyKey: string, descriptor: PropertyDescriptor ) { const original = descriptor.value; descriptor.value = function(...args: any[]) { console.log(`Calling ${propertyKey}`); const result = original.apply(this, args); console.log(`Finished ${propertyKey}`); return result; }; } class Service { @logCalls processData(data: any[]): number { return data.length; } } // Class decorator function singleton<T extends { new(...args: any[]): {} }>(constructor: T) { let instance: T; return class extends constructor { constructor(...args: any[]) { if (instance) { return instance; } super(...args); instance = this as any; } }; } @singleton class Database { // ... } // Higher-order function (alternative to decorators) function logCalls<T extends (...args: any[]) => any>(fn: T): T { return ((...args: any[]) => { console.log(`Calling ${fn.name}`); const result = fn(...args); console.log(`Finished ${fn.name}`); return result; }) as T; } const processData = logCalls((data: any[]) => data.length);
Property Access
Python:
class Circle: def __init__(self, radius: float): self._radius = radius @property def area(self) -> float: return 3.14159 * self._radius ** 2 @property def radius(self) -> float: return self._radius @radius.setter def radius(self, value: float) -> None: if value < 0: raise ValueError("Radius cannot be negative") self._radius = value # Usage circle = Circle(5) print(circle.area) # Computed property circle.radius = 10 # Setter validation
TypeScript:
class Circle { private _radius: number; constructor(radius: number) { this._radius = radius; } get area(): number { return Math.PI * this._radius ** 2; } get radius(): number { return this._radius; } set radius(value: number) { if (value < 0) { throw new Error("Radius cannot be negative"); } this._radius = value; } } // Usage const circle = new Circle(5); console.log(circle.area); // Computed property circle.radius = 10; // Setter validation
Dynamic Attribute Access
Python:
class DynamicObject: def __getattr__(self, name: str): return f"Dynamic: {name}" def __setattr__(self, name: str, value): super().__setattr__(name, value) def __delattr__(self, name: str): super().__delattr__(name) # Usage obj = DynamicObject() print(obj.anything) # "Dynamic: anything"
TypeScript:
// Using Proxy for dynamic properties function createDynamicObject() { return new Proxy({}, { get(target, prop) { if (prop in target) { return target[prop]; } return `Dynamic: ${String(prop)}`; }, set(target, prop, value) { target[prop] = value; return true; }, deleteProperty(target, prop) { delete target[prop]; return true; } }); } // Usage const obj = createDynamicObject(); console.log(obj.anything); // "Dynamic: anything"
Zero and Default Values
Null/None Handling
| Python | TypeScript | Notes |
|---|---|---|
| | Explicit null |
| | Default absence |
| | Nullable value |
| | Optional value |
| | Fully optional |
Python:
from typing import Optional def find_user(id: str) -> Optional[User]: user = database.get(id) return user # Can be None # Usage - explicit None check user = find_user('123') if user is not None: print(user.name) else: print("User not found") # Or with walrus operator if (user := find_user('123')) is not None: print(user.name)
TypeScript:
function findUser(id: string): User | null { const user = database.get(id); return user ?? null; // Convert undefined to null } // Usage - explicit null check const user = findUser('123'); if (user !== null) { console.log(user.name); } else { console.log('User not found'); } // Optional chaining const name = findUser('123')?.name ?? 'Unknown'; // Nullish coalescing const user = findUser('123') ?? defaultUser;
Default Values
Python:
# Default parameter values def greet(name: str = "World") -> str: return f"Hello, {name}!" # Mutable defaults (AVOID) def bad_append(item: str, items: list[str] = []) -> list[str]: items.append(item) # Same list reused! return items # Correct mutable defaults def good_append(item: str, items: list[str] | None = None) -> list[str]: if items is None: items = [] items.append(item) return items # Dataclass defaults from dataclasses import dataclass, field @dataclass class Config: host: str = "localhost" port: int = 8080 tags: list[str] = field(default_factory=list) # Mutable default
TypeScript:
// Default parameter values function greet(name: string = "World"): string { return `Hello, ${name}!`; } // Default object/array parameters (creates new instance each time) function append(item: string, items: string[] = []): string[] { items.push(item); return items; } // Interface with optional properties interface Config { host?: string; port?: number; tags?: string[]; } // Default values when destructuring function createConfig({ host = "localhost", port = 8080, tags = [] }: Partial<Config> = {}): Config { return { host, port, tags }; } // Class with defaults class Config { host: string = "localhost"; port: number = 8080; tags: string[] = []; // Creates new array per instance }
Serialization
JSON Serialization
Python:
import json from dataclasses import dataclass, asdict from datetime import datetime from enum import Enum @dataclass class User: id: str name: str email: str created_at: datetime # Custom encoder for non-JSON types class CustomEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, datetime): return obj.isoformat() if isinstance(obj, Enum): return obj.value return super().default(obj) # Serialization user = User("1", "Alice", "alice@example.com", datetime.now()) json_str = json.dumps(asdict(user), cls=CustomEncoder) # Deserialization with validation data = json.loads(json_str) user = User( id=data['id'], name=data['name'], email=data['email'], created_at=datetime.fromisoformat(data['created_at']) )
TypeScript:
interface User { id: string; name: string; email: string; createdAt: Date; } // Serialization (custom replacer for Date) const user: User = { id: "1", name: "Alice", email: "alice@example.com", createdAt: new Date() }; const jsonStr = JSON.stringify(user, (key, value) => { if (value instanceof Date) { return value.toISOString(); } return value; }); // Deserialization (custom reviver for Date) const data = JSON.parse(jsonStr, (key, value) => { if (key === 'createdAt' && typeof value === 'string') { return new Date(value); } return value; }) as User; // Better: Use a library like zod for validation import { z } from 'zod'; const UserSchema = z.object({ id: z.string(), name: z.string(), email: z.string().email(), createdAt: z.coerce.date() }); const user = UserSchema.parse(data); // Validates and transforms
Pydantic → Zod
Python (Pydantic):
from pydantic import BaseModel, EmailStr, validator from datetime import datetime class User(BaseModel): id: str name: str email: EmailStr age: int created_at: datetime @validator('age') def validate_age(cls, v): if v < 0: raise ValueError('Age must be positive') return v # Validation and serialization user = User( id="1", name="Alice", email="alice@example.com", age=30, created_at=datetime.now() ) json_str = user.model_dump_json() # Serialize user2 = User.model_validate_json(json_str) # Deserialize with validation
TypeScript (Zod):
import { z } from 'zod'; const UserSchema = z.object({ id: z.string(), name: z.string(), email: z.string().email(), age: z.number().int().positive(), createdAt: z.coerce.date() }); type User = z.infer<typeof UserSchema>; // Validation and serialization const user: User = { id: "1", name: "Alice", email: "alice@example.com", age: 30, createdAt: new Date() }; // Validate const validated = UserSchema.parse(user); // Safe parse (doesn't throw) const result = UserSchema.safeParse(user); if (result.success) { console.log(result.data); } else { console.error(result.error); } // JSON round-trip const jsonStr = JSON.stringify(user); const parsed = UserSchema.parse(JSON.parse(jsonStr));
Build and Dependencies
Package Manager Comparison
| Aspect | Python | TypeScript |
|---|---|---|
| Package manager | pip, uv, poetry | npm, yarn, pnpm |
| Manifest file | , | |
| Lock file | , | , , |
| Virtual environment | , | (per-project) |
| Global packages | Discouraged | Allowed with flag |
Project Configuration
Python (pyproject.toml):
[project] name = "myproject" version = "1.0.0" description = "A sample Python project" readme = "README.md" requires-python = ">=3.11" license = { text = "MIT" } dependencies = [ "requests>=2.31.0", "pydantic>=2.0.0", ] [project.optional-dependencies] dev = [ "pytest>=7.4.0", "ruff>=0.1.0", "mypy>=1.6.0", ] [projectcli] mytool = "myproject.cli:main" [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.ruff] line-length = 88 target-version = "py311" [tool.mypy] python_version = "3.11" strict = true
TypeScript (package.json + tsconfig.json):
// package.json { "name": "myproject", "version": "1.0.0", "description": "A sample TypeScript project", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { "build": "tsc", "test": "vitest", "lint": "eslint .", "format": "prettier --write ." }, "dependencies": { "axios": "^1.6.0" }, "devDependencies": { "typescript": "^5.3.0", "vitest": "^1.0.0", "eslint": "^8.54.0", "@typescript-eslint/eslint-plugin": "^6.13.0", "@typescript-eslint/parser": "^6.13.0" }, "engines": { "node": ">=18.0.0" } }
// tsconfig.json { "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "lib": ["ES2022"], "outDir": "dist", "rootDir": "src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "declaration": true, "declarationMap": true, "sourceMap": true, "noUncheckedIndexedAccess": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] }
Dependency Installation
Python:
# Using pip pip install requests pytest # Using uv (faster) uv add requests pytest uv add --dev ruff mypy # Install from requirements.txt pip install -r requirements.txt # Install project in editable mode pip install -e .
TypeScript:
# Using npm npm install axios npm install --save-dev vitest # Using pnpm (faster) pnpm add axios pnpm add -D vitest # Install from package.json npm install # Global install (for CLI tools) npm install -g typescript
Testing
Testing Framework Comparison
| Aspect | Python (pytest) | TypeScript (vitest/jest) |
|---|---|---|
| Runner | pytest | vitest / jest |
| Assertion | assert statement | expect().toBe() |
| Fixtures | @pytest.fixture | beforeEach / fixtures |
| Mocking | unittest.mock | vi.mock() / jest.mock() |
| Coverage | pytest-cov | vitest --coverage |
| Parametrize | @pytest.mark.parametrize | test.each() |
Basic Tests
Python (pytest):
# test_calculator.py import pytest def test_addition(): assert 1 + 1 == 2 def test_division_by_zero(): with pytest.raises(ZeroDivisionError): 1 / 0 # Parametrized tests @pytest.mark.parametrize("a,b,expected", [ (1, 1, 2), (2, 3, 5), (10, 5, 15), ]) def test_addition_parametrized(a, b, expected): assert a + b == expected # Fixtures @pytest.fixture def sample_user(): return {"name": "Alice", "age": 30} def test_user_data(sample_user): assert sample_user["name"] == "Alice" assert sample_user["age"] == 30
TypeScript (vitest):
// calculator.test.ts import { describe, it, expect, beforeEach } from 'vitest'; describe('Calculator', () => { it('should add numbers', () => { expect(1 + 1).toBe(2); }); it('should throw on division by zero', () => { expect(() => 1 / 0).toThrow(); }); // Parametrized tests it.each([ [1, 1, 2], [2, 3, 5], [10, 5, 15], ])('should add %i + %i = %i', (a, b, expected) => { expect(a + b).toBe(expected); }); // Fixtures with beforeEach let sampleUser: { name: string; age: number }; beforeEach(() => { sampleUser = { name: "Alice", age: 30 }; }); it('should have user data', () => { expect(sampleUser.name).toBe("Alice"); expect(sampleUser.age).toBe(30); }); });
Async Tests
Python:
import pytest import asyncio @pytest.mark.asyncio async def test_async_fetch(): result = await fetch_user('123') assert result.name == "Alice" # With timeout @pytest.mark.asyncio @pytest.mark.timeout(5) async def test_slow_operation(): result = await slow_operation() assert result is not None
TypeScript:
import { describe, it, expect } from 'vitest'; describe('Async operations', () => { it('should fetch user', async () => { const result = await fetchUser('123'); expect(result.name).toBe("Alice"); }); // With timeout it('should handle slow operation', async () => { const result = await slowOperation(); expect(result).not.toBeNull(); }, { timeout: 5000 }); });
Idiom Translation
Pattern 1: List Comprehensions → Array Methods
Python:
# List comprehension squares = [x**2 for x in range(10)] # With filter evens = [x for x in range(10) if x % 2 == 0] # Nested matrix = [[i+j for j in range(3)] for i in range(3)] # Dict comprehension word_lengths = {word: len(word) for word in ["hello", "world"]} # Set comprehension unique_lengths = {len(word) for word in ["hello", "world", "hi"]}
TypeScript:
// Array map const squares = Array.from({ length: 10 }, (_, i) => i ** 2); // Or const squares = [...Array(10).keys()].map(x => x ** 2); // With filter const evens = [...Array(10).keys()].filter(x => x % 2 === 0); // Nested const matrix = Array.from({ length: 3 }, (_, i) => Array.from({ length: 3 }, (_, j) => i + j) ); // Object from entries const wordLengths = Object.fromEntries( ["hello", "world"].map(word => [word, word.length]) ); // Set const uniqueLengths = new Set(["hello", "world", "hi"].map(w => w.length));
Pattern 2: With Statement → Try-Finally / Using
Python:
# Context manager with open("file.txt") as f: content = f.read() # File automatically closed # Custom context manager from contextlib import contextmanager @contextmanager def timer(): start = time.time() yield end = time.time() print(f"Elapsed: {end - start:.2f}s") with timer(): # Code to time time.sleep(1)
TypeScript:
// Try-finally pattern let file: fs.FileHandle | null = null; try { file = await fs.open("file.txt"); const content = await file.readFile('utf-8'); } finally { await file?.close(); } // Using Disposable pattern (TypeScript 5.2+) class Timer implements Disposable { private start = Date.now(); [Symbol.dispose]() { const end = Date.now(); console.log(`Elapsed: ${(end - this.start) / 1000}s`); } } { using timer = new Timer(); // Code to time await sleep(1000); } // Timer automatically disposed // Async disposable class FileHandle implements AsyncDisposable { async [Symbol.asyncDispose]() { await this.close(); } } { await using file = await fs.open("file.txt"); // Use file } // Automatically closed
Pattern 3: Multiple Return Values → Tuples/Objects
Python:
# Tuple unpacking def get_user_stats(user_id: str) -> tuple[str, int, float]: name = "Alice" count = 42 average = 3.14 return name, count, average name, count, average = get_user_stats("123") # Named tuple from typing import NamedTuple class UserStats(NamedTuple): name: str count: int average: float def get_user_stats(user_id: str) -> UserStats: return UserStats("Alice", 42, 3.14) stats = get_user_stats("123") print(stats.name, stats.count)
TypeScript:
// Tuple return function getUserStats(userId: string): [string, number, number] { const name = "Alice"; const count = 42; const average = 3.14; return [name, count, average]; } const [name, count, average] = getUserStats("123"); // Object return (preferred) interface UserStats { name: string; count: number; average: number; } function getUserStats(userId: string): UserStats { return { name: "Alice", count: 42, average: 3.14 }; } const stats = getUserStats("123"); console.log(stats.name, stats.count); // With destructuring const { name, count } = getUserStats("123");
Pattern 4: Dataclass → Interface/Type
Python:
from dataclasses import dataclass, field @dataclass class User: id: str name: str email: str age: int = 0 tags: list[str] = field(default_factory=list) def greet(self) -> str: return f"Hello, {self.name}!" # Usage user = User(id="1", name="Alice", email="alice@example.com") print(user.greet())
TypeScript:
// Interface (data only) interface User { id: string; name: string; email: string; age?: number; tags?: string[]; } // Class (with methods) class User { id: string; name: string; email: string; age: number = 0; tags: string[] = []; constructor(data: Omit<User, 'age' | 'tags'>) { this.id = data.id; this.name = data.name; this.email = data.email; } greet(): string { return `Hello, ${this.name}!`; } } // Usage const user = new User({ id: "1", name: "Alice", email: "alice@example.com" }); console.log(user.greet()); // Factory function (alternative) function createUser(data: Omit<User, 'age' | 'tags'>): User { return { age: 0, tags: [], ...data, greet() { return `Hello, ${this.name}!`; } }; }
Pattern 5: Protocol → Interface
Python:
from typing import Protocol class Drawable(Protocol): def draw(self) -> None: ... class Closeable(Protocol): def close(self) -> None: ... # Structural typing - no explicit inheritance needed class Window: def draw(self) -> None: print("Drawing window") def close(self) -> None: print("Closing window") def render(obj: Drawable) -> None: obj.draw() window = Window() render(window) # Works without explicit inheritance
TypeScript:
// Interface (structural typing built-in) interface Drawable { draw(): void; } interface Closeable { close(): void; } // Structural typing - no explicit implements needed (but recommended) class Window implements Drawable, Closeable { draw(): void { console.log("Drawing window"); } close(): void { console.log("Closing window"); } } function render(obj: Drawable): void { obj.draw(); } const window = new Window(); render(window); // Works due to structural typing
Pattern 6: Enum → Const Object or String Literal Union
Python:
from enum import Enum, StrEnum # Enum class class Status(Enum): PENDING = "pending" ACTIVE = "active" COMPLETED = "completed" # Or StrEnum (Python 3.11+) class Status(StrEnum): PENDING = "pending" ACTIVE = "active" COMPLETED = "completed" # Usage def process(status: Status) -> None: if status == Status.PENDING: print("Pending...") elif status is Status.ACTIVE: print("Active!") # With Literal from typing import Literal StatusType = Literal["pending", "active", "completed"] def handle(status: StatusType) -> None: if status == "pending": print("Pending...")
TypeScript:
// String enum enum Status { Pending = "pending", Active = "active", Completed = "completed" } function process(status: Status): void { if (status === Status.Pending) { console.log("Pending..."); } else if (status === Status.Active) { console.log("Active!"); } } // Const object (preferred - no runtime overhead) const Status = { Pending: "pending", Active: "active", Completed: "completed" } as const; type Status = typeof Status[keyof typeof Status]; function handle(status: Status): void { if (status === Status.Pending) { console.log("Pending..."); } } // String literal union (most TypeScript-like) type Status = "pending" | "active" | "completed"; function handleSimple(status: Status): void { switch (status) { case "pending": console.log("Pending..."); break; case "active": console.log("Active!"); break; case "completed": console.log("Completed!"); break; } }
Pattern 7: Generators → Generator Functions
Python:
def fibonacci(n: int): a, b = 0, 1 for _ in range(n): yield a a, b = b, a + b # Usage for num in fibonacci(10): print(num) # Generator expression squares = (x**2 for x in range(10))
TypeScript:
function* fibonacci(n: number): Generator<number> { let [a, b] = [0, 1]; for (let i = 0; i < n; i++) { yield a; [a, b] = [b, a + b]; } } // Usage for (const num of fibonacci(10)) { console.log(num); } // No generator expression equivalent - use Array methods const squares = Array.from({ length: 10 }, (_, i) => i ** 2);
Common Pitfalls
1. Python None
→ TypeScript null
vs undefined
NonenullundefinedProblem: Python has one "null" value (
None), TypeScript has two (null and undefined).
Python:
def find_user(id: str) -> User | None: # ... return None # Only one way to represent "no value"
TypeScript:
// Need to decide: null or undefined? function findUser(id: string): User | null { return null; // Explicit absence } // Or function findUser(id: string): User | undefined { return undefined; // Implicit absence (default) } // Often both are possible function findUser(id: string): User | null | undefined { // ... }
Solution: Choose a convention:
- Use
for explicit "not found" casesnull - Use
for optional/uninitialized valuesundefined - Or stick to one consistently (prefer
for simplicity)undefined
2. Mutable vs Immutable Collections
Problem: Python's mutable defaults vs TypeScript's const behavior.
Python:
# Mutable list items = [1, 2, 3] items.append(4) # Modifies in place # To make immutable, use tuple items = (1, 2, 3) # Cannot be modified
TypeScript:
// const reference, but mutable content const items = [1, 2, 3]; items.push(4); // Works! const doesn't freeze content // To make immutable const items: readonly number[] = [1, 2, 3]; items.push(4); // Error: push doesn't exist on readonly array // Or use as const const items = [1, 2, 3] as const; items.push(4); // Error
Solution: Use
readonly for truly immutable arrays in TypeScript.
3. Truthy/Falsy Values
Problem: Different falsy values between languages.
Python:
# Python falsy values: None, False, 0, "", [], {}, () if user: # False for None or empty dict print(user.name) # Be explicit if user is not None: # Only checks None print(user.name)
TypeScript:
// TypeScript falsy: null, undefined, false, 0, "", NaN if (user) { // False for null, undefined, or empty object console.log(user.name); } // Be explicit if (user !== null && user !== undefined) { console.log(user.name); } // Or use nullish coalescing const name = user?.name ?? "Unknown";
Solution: Use explicit comparisons when the distinction matters.
4. Integer Division
Problem: Python has floor division (
//), TypeScript only has float division.
Python:
result = 5 / 2 # 2.5 (float division) result = 5 // 2 # 2 (floor division)
TypeScript:
const result = 5 / 2; // 2.5 (always float) const floored = Math.floor(5 / 2); // 2 (explicit floor) const truncated = Math.trunc(5 / 2); // 2 (toward zero)
Solution: Use
Math.floor() or Math.trunc() in TypeScript for integer division.
5. Class Properties Initialization
Problem: Python requires
__init__, TypeScript allows class-level initialization.
Python:
class Counter: # Class variable (shared across instances!) count = 0 def __init__(self, initial: int = 0): # Instance variable (per-instance) self.count = initial
TypeScript:
class Counter { // Instance property (per-instance by default) count: number = 0; // Static property (shared across instances) static globalCount: number = 0; constructor(initial: number = 0) { this.count = initial; } }
Solution: Understand that TypeScript class properties are instance variables by default, unlike Python.
6. String Formatting
Problem: Python f-strings vs TypeScript template literals.
Python:
name = "Alice" age = 30 message = f"Hello, {name}! You are {age} years old." # Multi-line message = f""" Hello, {name}! You are {age} years old. """
TypeScript:
const name = "Alice"; const age = 30; const message = `Hello, ${name}! You are ${age} years old.`; // Multi-line (indentation matters!) const message = ` Hello, ${name}! You are ${age} years old. `;
Solution: Both support similar template syntax, but TypeScript preserves all whitespace.
7. Dictionary/Object Property Access
Problem: Python raises KeyError, TypeScript returns undefined.
Python:
user = {"name": "Alice"} email = user["email"] # KeyError! # Safe access email = user.get("email") # None email = user.get("email", "default@example.com") # Default value
TypeScript:
const user = { name: "Alice" }; const email = user["email"]; // undefined (no error) const email2 = user.email; // undefined (no error) // With strict types interface User { name: string; email?: string; // Explicitly optional } const user: User = { name: "Alice" }; const email = user.email; // string | undefined // Nullish coalescing for default const email = user.email ?? "default@example.com";
Solution: TypeScript's optional properties provide type safety, but always returns
undefined for missing keys.
8. Import Side Effects
Problem: Python executes module on first import, TypeScript has clearer side-effect semantics.
Python:
# config.py print("Loading config...") # Executes on first import DATABASE_URL = "..." # main.py import config # Prints "Loading config..." import config # Does NOT print again (cached)
TypeScript:
// config.ts console.log("Loading config..."); // Executes on first import export const DATABASE_URL = "..."; // main.ts import { DATABASE_URL } from './config'; // Prints "Loading config..." import './config'; // Side-effect import (explicit)
Solution: Both cache modules, but TypeScript makes side-effect imports more explicit.
Tooling
| Tool | Purpose | Notes |
|---|---|---|
| TypeScript Compiler (tsc) | Type checking and transpilation | Core TypeScript tool |
| ts-node | Run TypeScript directly | Development convenience |
| vitest | Testing framework | Modern, fast test runner |
| zod | Runtime validation | Like pydantic for TypeScript |
| prettier | Code formatting | Opinionated formatter |
| eslint | Linting | Code quality checks |
| type-fest | Utility types | Extended type utilities |
| axios | HTTP client | Like requests for Python |
| date-fns | Date manipulation | Alternative to Python datetime |
| lodash | Utility functions | Functional programming helpers |
Type Checking Setup
tsconfig.json:
{ "compilerOptions": { "target": "ES2022", "module": "ESNext", "strict": true, "noUncheckedIndexedAccess": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "exactOptionalPropertyTypes": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true } }
Linting Setup
.eslintrc.json:
{ "extends": [ "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended-requiring-type-checking" ], "parser": "@typescript-eslint/parser", "parserOptions": { "project": "./tsconfig.json" }, "rules": { "@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }] } }
Examples
Example 1: Simple - HTTP Request Function
Before (Python):
import httpx from dataclasses import dataclass @dataclass class User: id: str name: str email: str async def fetch_user(id: str) -> User: async with httpx.AsyncClient() as client: response = await client.get(f'https://api.example.com/users/{id}') if not response.is_success: raise Exception(f"HTTP {response.status_code}") data = response.json() return User(**data) # Usage async def main(): try: user = await fetch_user('123') print(user.name) except Exception as error: print(f'Failed: {error}') if __name__ == '__main__': import asyncio asyncio.run(main())
After (TypeScript):
interface User { id: string; name: string; email: string; } async function fetchUser(id: string): Promise<User> { const response = await fetch(`https://api.example.com/users/${id}`); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return await response.json(); } // Usage (top-level await in modules) try { const user = await fetchUser('123'); console.log(user.name); } catch (error) { console.error('Failed:', error); }
Example 2: Medium - Data Processing Pipeline
Before (Python):
from dataclasses import dataclass from typing import NamedTuple @dataclass class Product: id: str name: str price: float category: str in_stock: bool class CategoryStats(NamedTuple): category: str count: int average_price: float total_value: float def analyze_products(products: list[Product]) -> list[CategoryStats]: from collections import defaultdict from operator import attrgetter # Group by category grouped: dict[str, list[Product]] = defaultdict(list) for product in products: if product.in_stock: grouped[product.category].append(product) # Calculate statistics stats = [] for category, items in grouped.items(): total_price = sum(item.price for item in items) count = len(items) stats.append(CategoryStats( category=category, count=count, average_price=total_price / count, total_value=total_price )) return sorted(stats, key=attrgetter('total_value'), reverse=True) # Usage stats = analyze_products(products) for s in stats: print(f"{s.category}: {s.count} items, avg ${s.average_price:.2f}")
After (TypeScript):
interface Product { id: string; name: string; price: number; category: string; inStock: boolean; } interface CategoryStats { category: string; count: number; averagePrice: number; totalValue: number; } function analyzeProducts(products: Product[]): CategoryStats[] { // Group by category const grouped = new Map<string, Product[]>(); for (const product of products) { if (product.inStock) { const items = grouped.get(product.category) ?? []; items.push(product); grouped.set(product.category, items); } } // Calculate statistics const stats: CategoryStats[] = []; for (const [category, items] of grouped.entries()) { const totalPrice = items.reduce((sum, item) => sum + item.price, 0); const count = items.length; stats.push({ category, count, averagePrice: totalPrice / count, totalValue: totalPrice }); } // Sort by total value descending return stats.sort((a, b) => b.totalValue - a.totalValue); } // Usage const stats = analyzeProducts(products); for (const s of stats) { console.log(`${s.category}: ${s.count} items, avg $${s.averagePrice.toFixed(2)}`); }
Example 3: Complex - Generic Repository with Caching
Before (Python):
from dataclasses import dataclass, field from datetime import datetime, timedelta from typing import Generic, TypeVar, Protocol, Callable, Awaitable from collections import OrderedDict import asyncio class Entity(Protocol): id: str created_at: datetime updated_at: datetime @dataclass class User: id: str name: str email: str created_at: datetime = field(default_factory=datetime.now) updated_at: datetime = field(default_factory=datetime.now) T = TypeVar('T', bound=Entity) @dataclass class CacheOptions: ttl_seconds: int = 300 max_size: int = 100 @dataclass class CacheEntry(Generic[T]): value: T expires_at: datetime class CachedRepository(Generic[T]): def __init__( self, fetch_fn: Callable[[str], Awaitable[T]], options: CacheOptions | None = None ): self._fetch_fn = fetch_fn self._options = options or CacheOptions() self._cache: OrderedDict[str, CacheEntry[T]] = OrderedDict() self._ttl = timedelta(seconds=self._options.ttl_seconds) self._max_size = self._options.max_size async def get(self, id: str) -> T | None: # Check cache cached = self._cache.get(id) if cached and cached.expires_at > datetime.now(): self._cache.move_to_end(id) return cached.value # Fetch from source try: value = await self._fetch_fn(id) self._set(id, value) return value except Exception as error: if hasattr(error, 'status') and error.status == 404: return None raise async def get_many(self, ids: list[str]) -> dict[str, T]: results = await asyncio.gather( *[self._fetch_with_id(id) for id in ids], return_exceptions=False ) return { id: value for id, value in results if value is not None } async def _fetch_with_id(self, id: str) -> tuple[str, T | None]: value = await self.get(id) return (id, value) def _set(self, id: str, value: T) -> None: if len(self._cache) >= self._max_size: self._cache.popitem(last=False) self._cache[id] = CacheEntry( value=value, expires_at=datetime.now() + self._ttl ) def invalidate(self, id: str) -> None: self._cache.pop(id, None) def clear(self) -> None: self._cache.clear()
After (TypeScript):
interface Entity { id: string; createdAt: Date; updatedAt: Date; } interface User extends Entity { name: string; email: string; } interface CacheOptions { ttlSeconds: number; maxSize: number; } interface CacheEntry<T> { value: T; expiresAt: number; } class CachedRepository<T extends Entity> { private cache: Map<string, CacheEntry<T>> = new Map(); private readonly ttl: number; private readonly maxSize: number; constructor( private readonly fetchFn: (id: string) => Promise<T>, options: CacheOptions = { ttlSeconds: 300, maxSize: 100 } ) { this.ttl = options.ttlSeconds * 1000; this.maxSize = options.maxSize; } async get(id: string): Promise<T | null> { // Check cache const cached = this.cache.get(id); if (cached && cached.expiresAt > Date.now()) { // Move to end (LRU) this.cache.delete(id); this.cache.set(id, cached); return cached.value; } // Fetch from source try { const value = await this.fetchFn(id); this.set(id, value); return value; } catch (error) { if (error.status === 404) { return null; } throw error; } } async getMany(ids: string[]): Promise<Map<string, T>> { const results = await Promise.all( ids.map(async id => ({ id, value: await this.get(id) })) ); return new Map( results .filter(r => r.value !== null) .map(r => [r.id, r.value!]) ); } private set(id: string, value: T): void { // Evict oldest if at capacity if (this.cache.size >= this.maxSize) { const oldestKey = this.cache.keys().next().value; this.cache.delete(oldestKey); } this.cache.set(id, { value, expiresAt: Date.now() + this.ttl }); } invalidate(id: string): void { this.cache.delete(id); } clear(): void { this.cache.clear(); } } // Usage async function fetchUserFromApi(id: string): Promise<User> { const response = await fetch(`/api/users/${id}`); if (!response.ok) { throw Object.assign(new Error(`HTTP ${response.status}`), { status: response.status }); } return await response.json(); } const userRepo = new CachedRepository(fetchUserFromApi, { ttlSeconds: 600, maxSize: 50 }); // Fetch single user const user = await userRepo.get('123'); if (user) { console.log(`Found user: ${user.name}`); } // Fetch multiple users const users = await userRepo.getMany(['1', '2', '3']); for (const [id, user] of users) { console.log(`${id}: ${user.name}`); }
See Also
For more examples and patterns, see:
- Foundational patterns with cross-language examplesmeta-convert-dev
- Python development patternslang-python-dev
- TypeScript development patternslang-typescript-patterns-dev
Cross-cutting pattern skills (for areas not fully covered by lang-*-dev):
- Async, channels, threads across languagespatterns-concurrency-dev
- JSON, validation, struct tags across languagespatterns-serialization-dev
- Decorators, macros, annotations across languagespatterns-metaprogramming-dev