ข้ามไปยังเนื้อหา

Typing & Protocols

ใน TypeScript interface กำหนด contract ออบเจ็กต์ใดก็ตามที่มี shape ที่ถูกต้องก็ตรงตาม interface นั้น — นี่เรียกว่า structural typing Protocol ของ Python (จากโมดูล typing) เป็นแนวคิดเดียวกันทุกประการ: คลาสหนึ่งตรงตาม Protocol ถ้ามีเมธอดและ attributes ที่ถูกต้อง โดยไม่สนว่า inherit มาจากอะไร

TypeScript
// TypeScript structural interface
interface Drawable {
draw(): string;
}
function render(shape: Drawable): string {
return shape.draw();
}
class Circle {
draw() { return "Drawing Circle"; }
}
class Square {
draw() { return "Drawing Square"; }
}
console.log(render(new Circle())); // "Drawing Circle"
console.log(render(new Square())); // "Drawing Square"
Python
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> str: ...
def render(shape: Drawable) -> str:
return shape.draw()
class Circle:
def draw(self) -> str:
return "Drawing Circle"
class Square:
def draw(self) -> str:
return "Drawing Square"
print(render(Circle())) # Drawing Circle
print(render(Square())) # Drawing Square

สังเกตว่า Circle กับ Square ไม่ได้ inherit จาก Drawable เลย ทั้งคู่แค่มี method draw() ที่ signature ตรงเท่านั้น ซึ่งพอแล้ว ส่วนงานจับข้อผิดพลาดเป็นหน้าที่ของ static analysis tool อย่าง mypy และ pyright

TypeScript generics แมปโดยตรงกับ TypeVar + Generic ของ Python

TypeScript
// TypeScript generic class
class Stack<T> {
private items: T[] = [];
push(item: T): void { this.items.push(item); }
pop(): T { return this.items.pop()!; }
peek(): T { return this.items[this.items.length - 1]; }
}
const stack = new Stack<number>();
stack.push(1);
stack.push(2);
console.log(stack.pop()); // 2
Python
from typing import TypeVar, Generic
T = TypeVar('T')
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
def peek(self) -> T:
return self._items[-1]
stack: Stack[int] = Stack()
stack.push(1)
stack.push(2)
print(stack.pop()) # 2

ตั้งแต่ Python 3.9+ คุณสามารถเขียน list[T] ได้โดยตรงในคลาสโดยไม่ต้อง import List จาก typing

Python 3.12 เพิ่มการประกาศ type parameter ไว้ที่ตัว declaration เลย generic class หรือ function จึงไม่ต้องมี TypeVar ระดับ module อีกต่อไป scope เป็นแบบ implicit เหมือน class Stack<T> ใน TypeScript เป๊ะ

TypeScript
// TypeScript — parameter อยู่ที่ declaration
class Stack<T> {
private items: T[] = [];
push(item: T): void { this.items.push(item); }
}
function first<T>(items: T[]): T { return items[0]; }
type Pair<T> = [T, T];
Python
# Python 3.12+ — PEP 695 ไม่ต้อง import TypeVar
class Stack[T]:
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def first[T](items: list[T]) -> T:
return items[0]
type Pair[T] = tuple[T, T] # type alias statement
# bound และ constraint เขียนแบบ inline ได้เหมือนกัน:
def largest[T: (int, float)](values: list[T]) -> T:
return max(values)

TypeVar ยังใช้ได้และเป็นสิ่งที่คุณจะเจอใน library ที่ยังรองรับ Python เก่า อ่านให้ออกทั้งสองแบบ แต่ code ใหม่บน 3.12+ ควรใช้ syntax วงเล็บเหลี่ยม Python 3.13 ยังให้ type parameter มี default ได้ด้วย (PEP 696) — TypeVar('T', default=int) หรือ class Box[T = int] ซึ่งเทียบได้กับ class Box<T = number>

TypedDict เป็น interface ของ TypeScript ที่อธิบาย plain object (ไม่มีเมธอด) เหมาะมากสำหรับข้อมูลแบบ JSON

TypeScript
// TypeScript interface for a plain object
interface UserRecord {
name: string;
age: number;
email?: string; // optional key
}
const user: UserRecord = { name: "Alice", age: 30 };
console.log(user.name);
Python
from typing import TypedDict, NotRequired
class UserRecord(TypedDict):
name: str
age: int
email: NotRequired[str] # optional key
user: UserRecord = {"name": "Alice", "age": 30}
print(user["name"]) # Alice
# mypy/pyright จะจับ key ที่ขาดหาย:
# bad: UserRecord = {"name": "Alice"} # age missing

Python 3.13 เพิ่ม ReadOnly สำหรับ TypedDict ซึ่งเทียบได้กับ modifier readonly ของ TypeScript type checker จะไม่ยอมให้เขียนทับ key นั้น แต่ key อื่นยังแก้ไขได้ตามปกติ

TypeScript
interface Config {
readonly name: string;
retries: number;
}
Python
from typing import TypedDict, ReadOnly
class Config(TypedDict):
name: ReadOnly[str]
retries: int
cfg: Config = {"name": "api", "retries": 3}
cfg["retries"] = 5 # ได้
# cfg["name"] = "other" # type checker ฟ้อง

อีกสองชิ้นที่เข้าใกล้สิ่งที่ TypeScript มีอยู่แล้ว TypeIs (Python 3.13) เป็น type predicate ที่ narrow ทั้งสอง branch ซึ่งเป็นพฤติกรรมที่ arg is Type ของ TypeScript ทำมาตลอด ส่วน TypeGuard ตัวเดิม narrow ให้เฉพาะ branch ที่เป็นจริง และ @override (Python 3.12) สั่งให้ checker ตรวจว่า method นั้น override method ของ base class จริงไหม เหมือน keyword override ของ TypeScript ภายใต้ noImplicitOverride

TypeScript
// TypeScript — predicate narrow ทั้งสอง branch
function isString(x: unknown): x is string {
return typeof x === "string";
}
class Base { greet(): void {} }
class Child extends Base {
override greet(): void {} // ถูกตรวจ
}
Python
from typing import TypeIs, override
def is_string(x: object) -> TypeIs[str]:
return isinstance(x, str)
def handle(value: int | str) -> None:
if is_string(value):
print(value.upper()) # narrow เป็น str
else:
print(value + 1) # narrow เป็น int — TypeGuard ทำตรงนี้ไม่ได้
class Base:
def greet(self) -> None: ...
class Child(Base):
@override
def greet(self) -> None: ... # checker ตรวจว่ามี method นี้ใน base จริง

TypeScript ใช้ string | null | undefined สำหรับ nullable types Python ใช้ Optional[T] (ซึ่งคือ T | None) หรือ union syntax สมัยใหม่ T | None (Python 3.10+)

TypeScript
// TypeScript
function greet(name: string | null): string {
if (name === null) return "Hello, stranger!";
return `Hello, ${name}!`;
}
// Modern TS nullable shorthand
function greet2(name?: string): string {
return name ? `Hello, ${name}!` : "Hello, stranger!";
}
Python
from typing import Optional
# Optional[str] คือ str | None
def greet(name: Optional[str] = None) -> str:
if name is None:
return "Hello, stranger!"
return f"Hello, {name}!"
# Python 3.10+ union syntax
def greet_modern(name: str | None = None) -> str:
return f"Hello, {name}!" if name else "Hello, stranger!"
print(greet()) # Hello, stranger!
print(greet("Alice")) # Hello, Alice!
from typing import Protocol, TypeVar, Generic, TypedDict, Optional
# Protocol — structural typing
class Serializable(Protocol):
def to_json(self) -> str: ...
class User:
def __init__(self, name: str, age: int) -> None:
self.name = name
self.age = age
def to_json(self) -> str:
return f'{{"name": "{self.name}", "age": {self.age}}}'
class Product:
def __init__(self, title: str) -> None:
self.title = title
def to_json(self) -> str:
return f'{{"title": "{self.title}"}}'
def export_data(item: Serializable) -> str:
return item.to_json()
print(export_data(User("Alice", 30)))
print(export_data(Product("Widget")))
# Generic Stack
T = TypeVar('T')
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
s: Stack[str] = Stack()
s.push("hello")
s.push("world")
print(s.pop())
# TypedDict
class Config(TypedDict):
host: str
port: int
cfg: Config = {"host": "localhost", "port": 8080}
print(f"Connecting to {cfg['host']}:{cfg['port']}")
# Optional
def find_user(user_id: int) -> Optional[str]:
db = {1: "Alice", 2: "Bob"}
return db.get(user_id)
print(find_user(1)) # Alice
print(find_user(99)) # None
คลาส Python ใดที่ทำหน้าที่เทียบเท่ากับ TypeScript structural interface โดยตรง?
`Optional[str]` ในโมดูล typing ของ Python หมายความว่าอะไร?
เพื่อให้ Protocol ของ Python ทำงานกับ `isinstance()` ที่ runtime ต้องใช้ decorator ใด?
class ต้อง inherit จาก `Protocol` ตรง ๆ ไหม ถึงจะนับว่าเข้าเกณฑ์?
Python เวอร์ชันไหนที่เขียน `class Stack[T]:` ได้โดยไม่ต้อง import TypeVar?
TypeIs ให้อะไรที่ TypeGuard ให้ไม่ได้?