Claude Code Plugins

Community-maintained marketplace

Feedback

convert-python-typescript

@aRustyDev/ai
0
0

Convert Python code to idiomatic TypeScript. 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.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name convert-python-typescript
description Convert Python code to idiomatic TypeScript. 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.

Convert Python to TypeScript

Convert Python code to idiomatic TypeScript. This skill extends meta-convert-dev with Python-to-TypeScript specific type mappings, idiom translations, and tooling.

This Skill Extends

  • meta-convert-dev - Foundational conversion patterns (APTV workflow, testing strategies)

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
  • Reverse conversion (TypeScript → Python) - see convert-typescript-python

Quick Reference

Python TypeScript Notes
str string Unicode strings
int number / bigint Regular numbers or large integers
float number Floating point
bool boolean Same concept
list[T] T[] / Array<T> Mutable array
tuple[T, U] [T, U] / readonly [T, U] Fixed-size tuple
dict[K, V] Record<K, V> / Map<K, V> Key-value mapping
T | None / Optional[T] T | null / T | undefined Nullable types
Coroutine[Any, Any, T] Promise<T> Async operations
class X / Protocol interface X / type X Structure definition
Enum / Literal enum X / const object Enumerated values

When Converting Code

  1. Analyze source thoroughly before writing TypeScript
  2. Map types first - Python type hints → TypeScript types
  3. Preserve semantics over syntax similarity
  4. Adopt TypeScript idioms - don't write "Python code in TypeScript syntax"
  5. Handle edge cases - None → null/undefined, falsy values
  6. Test equivalence - same inputs → same outputs

Type System Mapping

Primitive Types

Python TypeScript Notes
str string UTF-16 in TS, but similar usage
int number For safe integers (-2^53 to 2^53)
int bigint For arbitrary precision (use when needed)
float number IEEE 754 double precision
bool boolean Lowercase (true/false) in TS
None null Explicit null
None undefined Implicit absence (default)
- void Function return type (no value)
Any any Escape hatch (avoid when possible)
object unknown Type-safe any
NoReturn never Function never returns

Collection Types

Python TypeScript Notes
list[T] T[] / Array<T> Mutable, ordered
tuple[T, ...] readonly T[] Immutable sequence
tuple[T, U] [T, U] Fixed-size tuple
set[T] Set<T> Unique values, unordered
dict[K, V] Map<K, V> Key-value with object keys
dict[str, V] Record<string, V> String-keyed object
dict[str, V] { [key: string]: V } Index signature
frozenset[T] ReadonlySet<T> Immutable set
Mapping[K, V] ReadonlyMap<K, V> Immutable map view

Composite Types

Python TypeScript Notes
class X: interface X { ... } Data structure
class X: class X { ... } With methods/behavior
Protocol interface X { ... } Structural typing
dataclass interface X { ... } Data-only classes
TypedDict interface X { ... } Typed dict structure
T | U T | U Union types (3.10+)
Union[T, U] T | U Union types (pre-3.10)
- T & U Intersection (combine fields)
Optional[T] T | null / T | undefined Nullable
Literal["a", "b"] "a" | "b" String literal union

Generic Types

Python TypeScript Notes
[T] (3.12+) / TypeVar('T') <T> Generic type parameter
TypeVar('T', bound=U) <T extends U> Bounded type variable
TypeVar('T', A, B) T extends A | B Constrained type
Callable[[A, B], R] (a: A, b: B) => R Function signature
Callable[..., R] (...args: any[]) => R Variadic function
Type[T] typeof T Type of class
- keyof T Union of property keys
- T[K] 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 Exception Error
Try-catch try/except/else/finally try/catch/finally
Throwing raise Exception() throw new Error()
Re-throwing raise (without args) throw (without args)
Type checking isinstance() or specific except instanceof

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 Coroutine[Any, Any, T] Promise<T>
Concurrent asyncio.gather() Promise.all()
Race asyncio.wait(..., FIRST_COMPLETED) Promise.race()
Timeout asyncio.wait_for() Promise.race() + 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
None null Explicit null
None undefined Default absence
Optional[T] T | null Nullable value
T | None T | undefined Optional value
Union[T, None] T | null | undefined 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 pyproject.toml, requirements.txt package.json
Lock file uv.lock, poetry.lock package-lock.json, yarn.lock, pnpm-lock.yaml
Virtual environment venv, virtualenv node_modules (per-project)
Global packages Discouraged Allowed with -g 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",
]

[project.scripts]
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

Problem: 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 null for explicit "not found" cases
  • Use undefined for optional/uninitialized values
  • Or stick to one consistently (prefer undefined for simplicity)

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:

  • meta-convert-dev - Foundational patterns with cross-language examples
  • convert-typescript-python - Reverse conversion (TypeScript → Python)
  • lang-python-dev - Python development patterns
  • lang-typescript-patterns-dev - TypeScript development patterns

Cross-cutting pattern skills (for areas not fully covered by lang-*-dev):

  • patterns-concurrency-dev - Async, channels, threads across languages
  • patterns-serialization-dev - JSON, validation, struct tags across languages
  • patterns-metaprogramming-dev - Decorators, macros, annotations across languages