Links#
https://www.typescriptlang.org/docs/handbook/intro.html
https://www.typescriptlang.org/docs/handbook/2/everyday-types.html
https://www.typescriptlang.org/docs/handbook/2/narrowing.html
https://www.typescriptlang.org/docs/handbook/2/classes.html
https://www.prisma.io/docs/orm
https://www.prisma.io/docs/orm/prisma-client
https://www.prisma.io/docs/orm/prisma-client/queries/crud
1. Goal#
目标:
用一个尽可能小的 order project 覆盖 TypeScript 常用语法
使用 Prisma 作为 TypeScript 常见 ORM 示例
不是为了业务完整
不是为了框架完整
只是为了把语法放进真实代码位置
you will see:
type / interface
union / discriminated union
enum-like literal union
optional / readonly
generic
utility types
class
implements
async / await
unknown
type narrowing
never
Record / tuple / satisfies
Prisma schema
Prisma Client
repository with ORM
2. Project Structure#
ts-syntax-project
├── package.json
├── tsconfig.json
├── .env
├── prisma
│ └── schema.prisma
└── src
├── main.ts
├── domain.ts
├── prisma.ts
├── repository.ts
├── service.ts
└── result.ts
3. package.json#
{
"name": "ts-syntax-project",
"version": "1.0.0",
"type": "module",
"private": true,
"scripts": {
"dev": "tsx src/main.ts",
"typecheck": "tsc --noEmit",
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev --name init"
},
"dependencies": {
"@prisma/client": "latest"
},
"devDependencies": {
"@types/node": "latest",
"prisma": "latest",
"tsx": "latest",
"typescript": "latest"
}
}
4. tsconfig.json#
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"skipLibCheck": true
},
"include": ["src/**/*.ts"]
}
5. .env#
DATABASE_URL="file:./dev.db"
why:
SQLite keeps this syntax demo self-contained
Prisma reads DATABASE_URL from .env
6. prisma/schema.prisma#
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model Order {
orderId String @id @default(uuid())
userId String
status String
createdAt DateTime @default(now())
paidAt DateTime?
items OrderItem[]
}
model OrderItem {
id Int @id @default(autoincrement())
orderId String
sku String
name String
quantity Int
amount Float
currency String
order Order @relation(fields: [orderId], references: [orderId], onDelete: Cascade)
}
why:
Order / OrderItem 展示 relation
SQLite 让本地运行不依赖外部数据库
Prisma Client 会基于 schema 生成类型安全的 query API
7. src/result.ts#
// generic: Result<T, E> 可以复用给不同 value/error 类型,避免每个 service 重复定义返回结构
export type Result<T, E extends string = string> =
// discriminated union: ok=true/false 让 TypeScript 自动 narrow value 或 error
| { ok: true; value: T }
| { ok: false; error: E };
// never: 用于 switch exhaustive check,新增 union member 时可以让编译器提醒
export function assertNever(value: never): never {
throw new Error(`unexpected value: ${String(value)}`);
}
8. src/domain.ts#
// literal union: 比 enum 更轻,适合固定业务状态
export type OrderStatus = "PENDING" | "PAID" | "CANCELLED";
export type Currency = "USD" | "HKD" | "CNY";
// Readonly: 金额对象创建后不应该被随意修改
export type Money = Readonly<{
amount: number;
currency: Currency;
}>;
export type OrderItemInput = {
// readonly: sku 是 item identity,不希望业务逻辑中被改写
readonly sku: string;
name: string;
quantity: number;
price: Money;
// optional property: tags 不是必填字段
tags?: string[];
};
export type Order = {
readonly orderId: string;
userId: string;
status: OrderStatus;
items: OrderItemInput[];
createdAt: Date;
paidAt?: Date;
};
// Pick + intersection: 复用 Order 的 userId 类型,同时扩展创建订单需要的 items
export type CreateOrderInput = Pick<Order, "userId"> & {
items: OrderItemInput[];
};
// Pick: API list 返回摘要,不暴露完整对象
export type OrderSummary = Pick<Order, "orderId" | "userId" | "status"> & {
totalAmount: number;
};
// Partial: patch 更新只允许改一部分字段
export type OrderPatch = Partial<Pick<Order, "status" | "paidAt">>;
// satisfies + Record: 保证每个 OrderStatus 都有 label,又保留 object literal 的精确类型
export const statusLabel = {
PENDING: "Waiting Payment",
PAID: "Paid",
CANCELLED: "Cancelled"
} satisfies Record<OrderStatus, string>;
// discriminated union: audit event 通过 type 字段区分不同 payload
export type AuditEvent =
| { type: "ORDER_CREATED"; orderId: string; at: Date }
| { type: "ORDER_PAID"; orderId: string; transactionId: string; at: Date }
| { type: "ORDER_CANCELLED"; orderId: string; reason: string; at: Date };
// tuple: pagination 的第一个值固定是 limit,第二个值可选是 cursor
export type Pagination = readonly [limit: number, cursor?: string];
9. src/prisma.ts#
import { PrismaClient } from "@prisma/client";
// singleton: 一个进程复用一个 PrismaClient,避免频繁创建连接
export const prisma = new PrismaClient();
10. src/repository.ts#
import type { PrismaClient } from "@prisma/client";
import type { Order, OrderItemInput } from "./domain.js";
// interface: service 依赖抽象,方便测试时替换为 fake repository
export interface OrderRepository {
findById(orderId: string): Promise<Order | null>;
save(order: Order): Promise<void>;
list(): Promise<Order[]>;
}
export class PrismaOrderRepository implements OrderRepository {
// constructor parameter property: 简洁声明并初始化 private readonly 字段
constructor(private readonly db: PrismaClient) {}
async findById(orderId: string): Promise<Order | null> {
const record = await this.db.order.findUnique({
where: { orderId },
// ORM relation: include items,一次查出 order 和 order_items
include: { items: true }
});
// nullish check: Prisma 查不到时返回 null
if (record === null) {
return null;
}
return {
orderId: record.orderId,
userId: record.userId,
// type assertion: DB 存 string,这里收窄成业务 literal union
status: record.status as Order["status"],
createdAt: record.createdAt,
// conditional spread: exactOptionalPropertyTypes 下不要写 paidAt: undefined
...(record.paidAt === null ? {} : { paidAt: record.paidAt }),
items: record.items.map((item) => ({
sku: item.sku,
name: item.name,
quantity: item.quantity,
price: {
amount: item.amount,
currency: item.currency as OrderItemInput["price"]["currency"]
}
}))
};
}
async save(order: Order): Promise<void> {
await this.db.order.upsert({
where: { orderId: order.orderId },
update: {
status: order.status,
paidAt: order.paidAt,
// nested write: 简化 demo,先删旧 items 再重建
items: {
deleteMany: {},
create: order.items.map((item) => ({
sku: item.sku,
name: item.name,
quantity: item.quantity,
amount: item.price.amount,
currency: item.price.currency
}))
}
},
create: {
orderId: order.orderId,
userId: order.userId,
status: order.status,
createdAt: order.createdAt,
paidAt: order.paidAt,
items: {
create: order.items.map((item) => ({
sku: item.sku,
name: item.name,
quantity: item.quantity,
amount: item.price.amount,
currency: item.price.currency
}))
}
}
});
}
async list(): Promise<Order[]> {
const records = await this.db.order.findMany({
include: { items: true },
orderBy: { createdAt: "desc" }
});
// Promise.all: map async function 时等待所有转换完成
return Promise.all(records.map((record) => this.findById(record.orderId)))
.then((orders) => orders.filter((order): order is Order => order !== null));
}
}
11. src/service.ts#
import { randomUUID } from "node:crypto";
import type { AuditEvent, CreateOrderInput, Order, OrderSummary, Pagination } from "./domain.js";
import { assertNever, type Result } from "./result.js";
import type { OrderRepository } from "./repository.js";
type PayOrderError = "ORDER_NOT_FOUND" | "INVALID_STATUS";
export class OrderService {
constructor(private readonly repository: OrderRepository) {}
async create(input: CreateOrderInput): Promise<Order> {
const order: Order = {
orderId: randomUUID(),
userId: input.userId,
status: "PENDING",
items: input.items,
createdAt: new Date()
};
await this.repository.save(order);
this.handleAudit({ type: "ORDER_CREATED", orderId: order.orderId, at: new Date() });
return order;
}
async pay(orderId: string): Promise<Result<Order, PayOrderError>> {
const order = await this.repository.findById(orderId);
if (order === null) {
return { ok: false, error: "ORDER_NOT_FOUND" };
}
if (order.status !== "PENDING") {
return { ok: false, error: "INVALID_STATUS" };
}
// object spread: 基于旧对象复制并覆盖 status / paidAt
const paidOrder: Order = {
...order,
status: "PAID",
paidAt: new Date()
};
await this.repository.save(paidOrder);
this.handleAudit({
type: "ORDER_PAID",
orderId,
transactionId: randomUUID(),
at: new Date()
});
return { ok: true, value: paidOrder };
}
async summarize(pagination: Pagination = [20]): Promise<OrderSummary[]> {
// destructuring + default parameter: 调用方不传 pagination 时默认 limit=20
const [limit] = pagination;
const orders = await this.repository.list();
return orders.slice(0, limit).map((order) => ({
orderId: order.orderId,
userId: order.userId,
status: order.status,
// reduce: 把多个 item 金额聚合成订单总额
totalAmount: order.items.reduce(
(sum, item) => sum + item.quantity * item.price.amount,
0
)
}));
}
parseCreateInput(value: unknown): CreateOrderInput {
// unknown + type guard: 外部输入必须先校验再进入业务逻辑
if (!isCreateOrderInput(value)) {
throw new Error("invalid create order input");
}
return value;
}
private handleAudit(event: AuditEvent): void {
switch (event.type) {
case "ORDER_CREATED":
console.log(`created ${event.orderId}`);
return;
case "ORDER_PAID":
console.log(`paid ${event.orderId} by ${event.transactionId}`);
return;
case "ORDER_CANCELLED":
console.log(`cancelled ${event.orderId}: ${event.reason}`);
return;
default:
// never: 如果 AuditEvent 新增类型但 switch 没处理,编译期会暴露问题
assertNever(event);
}
}
}
function isCreateOrderInput(value: unknown): value is CreateOrderInput {
if (typeof value !== "object" || value === null) {
return false;
}
// type assertion: 经过 object/null 检查后,用临时结构读取字段
const candidate = value as { userId?: unknown; items?: unknown };
return typeof candidate.userId === "string" && Array.isArray(candidate.items);
}
12. src/main.ts#
import { prisma } from "./prisma.js";
import { PrismaOrderRepository } from "./repository.js";
import { OrderService } from "./service.js";
const repository = new PrismaOrderRepository(prisma);
const service = new OrderService(repository);
const input = service.parseCreateInput({
userId: "u-1001",
items: [
{
sku: "sku-1001",
name: "Keyboard",
quantity: 1,
price: {
amount: 99.9,
currency: "USD"
},
tags: ["hardware"]
}
]
});
const order = await service.create(input);
const paid = await service.pay(order.orderId);
// discriminated union narrowing: paid.ok 为 true 时才能访问 paid.value
if (paid.ok) {
console.log("paid order", paid.value);
} else {
console.log("pay failed", paid.error);
}
console.log(await service.summarize([10]));
await prisma.$disconnect();
13. Run#
npm install
npm run prisma:migrate
npm run typecheck
npm run dev
expected:
created <order_id>
paid <order_id> by <transaction_id>
paid order {...}
[{ orderId, userId, status, totalAmount }]
14. Reading Order#
read files in this order:
1. prisma/schema.prisma
2. domain.ts
3. result.ts
4. prisma.ts
5. repository.ts
6. service.ts
7. main.ts
理解重点:
Prisma schema defines database model
Prisma Client provides type-safe ORM API
domain.ts defines business shape
repository maps ORM records to domain objects
service owns business logic
main wires everything together