TS 5.x 新特性深度解读

TS 5.x 新特性深度解读

写在前面

前面七篇讲了 TS 类型系统的核心。这一篇讲 TS 5.x——过去两年 TypeScript 发布的所有重要新特性。

5.x 不是一个版本,是一系列版本——5.0、5.1、5.2、5.3、5.4、5.5、5.6,每个版本都带来新东西。本篇挑最实用、最有影响力的讲:satisfies 操作符、const 类型参数、using 资源管理、decorator 标准化等。

一、TS 5.0 - 重大新特性

1.1 const type parameter

// TS 5.0 之前 - 类型参数推断是 string
function fn<const T>(value: T): T {
    return value;
}

const s = fn("hello");   // s 是 "hello"(字面量类型)
// 之前是 string

const 类型参数告诉 TS:尽量推断为字面量类型,不要泛化。

实战:对象字面量

function makeConfig<const T extends object>(config: T): T {
    return config;
}

const config = makeConfig({
    endpoint: "/api/users",  // 不加 const 是 string,加了是 "/api/users"
    timeout: 5000,             // 不加 const 是 number,加了是 5000
});

// 用法 - 类型是字面量
config.endpoint;  // 类型是 "/api/users"

1.2 satisfies 操作符(重要)

// TS 5.0 新增
type Color = "red" | "green" | "blue";
type RGB = [number, number, number];

const palette = {
    red: [255, 0, 0],
    green: "#00ff00",
    blue: [0, 0, 255],
} satisfies Record<Color, RGB | string>;
//  ^^^^^^^^
// satisfies 检查合规,但保留具体类型

satisfies 的作用:

对比 as 和 :

// 1. 直接注解 - 类型被收窄
const colors: Record<Color, RGB | string> = {
    red: [255, 0, 0],
    green: "#00ff00",
};
// 之后 colors.red 是 RGB | string

// 2. as 断言 - 不检查,强制转换
const colors = {
    red: [255, 0, 0],
    green: "#00ff00",
} as Record<Color, RGB | string>;
// 之后 colors.red 是 RGB | string(可能不安全)

// 3. satisfies - 检查 + 保留
const colors = {
    red: [255, 0, 0],
    green: "#00ff00",
    blue: [0, 0, 255],
} satisfies Record<Color, RGB | string>;
// 之后 colors.red 是 [number, number, number] (具体类型保留)
// 但如果漏了 blue,会编译报错

satisfies 比 : 严格,比 as 安全。

实战:配置对象

// 配置 schema
const Routes = {
    home: "/",
    users: "/users",
    userDetail: "/users/:id",
} satisfies Record<string, string>;

// 用法 - 类型仍然是字面量
type HomeRoute = typeof Routes.home;  // "/"
function navigate(route: HomeRoute) { ... }
navigate(Routes.home);  // OK
navigate(Routes.users);  // 报错 - users 是 "/users",不是 "/"

// 但漏字段会报错
const BadRoutes = {
    home: "/",
    users: "/users",
    // 缺 userDetail
} satisfies Record<string, string>;
// 报错 - 缺少 userDetail

1.3 多重 extends

// TS 5.0 之前
type T = A & B & C;  // 合并

// TS 5.0 - 可以做元组式合并
type T = A & (B & C);
// 等同上面,但可以用于某些场景

二、TS 5.2 - using 资源管理

2.1 资源管理的痛点

传统 try/finally 写起来繁琐:

{
    const file = await openFile("data.txt");
    try {
        const data = await file.read();
        return process(data);
    } finally {
        await file.close();
    }
}

2.2 using 关键字

{
    await using file = await openFile("data.txt");
    // 用 file
    const data = await file.read();
    return process(data);
}  // 自动 close(file)

await using 在作用域结束时自动调用 [Symbol.asyncDispose]。

2.3 实战:数据库连接

class DatabaseConnection {
    [Symbol.asyncDispose] = async () => {
        await this.close();
    };
}

async function getUser(id: string) {
    await using db = new DatabaseConnection();
    return db.query("SELECT * FROM users WHERE id = $1", [id]);
}
// db 自动关闭

2.4 sync 版本(dispose)

{
    using file = openFileSync("data.txt");
    // 用 file
}  // 自动 close

using 同步版本——Symbol.dispose 在作用域结束时调用。

2.5 实战:锁管理

class Lock {
    [Symbol.dispose] = () => {
        this.unlock();
    };
}

{
    using _lock = acquireLock("resource");
    // 临界区代码
    // 锁自动释放
}

三、TS 5.0 - decorator 标准化

3.1 旧的 experimental decorator

// TS 5.0 之前的实验性 decorator
@Component({ selector: "app-root" })
class AppComponent {
    @Input() name: string;
    
    @Output() click = new EventEmitter();
}

问题:实现不规范,跟 TC39 标准不一致。

3.2 新的标准化 decorator

// TS 5.0+ 标准化 decorator
function logged<This, Args extends any[]>(
    originalMethod: (this: This, ...args: Args) => any,
    context: ClassMethodDecoratorContext
) {
    return function (this: This, ...args: Args) {
        console.log(`Calling ${String(context.name)}`);
        return originalMethod.apply(this, args);
    };
}

class MyClass {
    @logged
    greet(name: string) {
        return `Hello, ${name}`;
    }
}

new MyClass().greet("Alice");
// "Calling greet"
// "Hello, Alice"

3.3 实战:自动 binding

function bound<This, Args extends any[]>(
    _: (this: This, ...args: Args) => any,
    context: ClassMethodDecoratorContext<This>
) {
    const methodName = context.name;
    context.addInitializer(function () {
        (this as any)[methodName] = (this as any)[methodName].bind(this);
    });
}

class Button {
    @bound
    handleClick() {
        console.log(this);  // this 永远指向实例
    }
}

const btn = new Button();
const fn = btn.handleClick;
fn();  // this 是 btn,不是 undefined

3.4 实战:自动 memoize

function memoize<This, Args extends any[], R>(
    originalMethod: (this: This, ...args: Args) => R,
    context: ClassMethodDecoratorContext<This>
) {
    const cache = new Map<string, R>();
    
    return function (this: This, ...args: Args): R {
        const key = JSON.stringify(args);
        if (cache.has(key)) return cache.get(key)!;
        const result = originalMethod.apply(this, args);
        cache.set(key, result);
        return result;
    };
}

class Calculator {
    @memoize
    fibonacci(n: number): number {
        if (n < 2) return n;
        return this.fibonacci(n - 1) + this.fibonacci(n - 2);
    }
}

3.5 实战:依赖注入

function inject<T>(token: new (...args: any[]) => T) {
    return function (
        _: any,
        context: ClassFieldDecoratorContext
    ) {
        context.addInitializer(function () {
            (this as any)[context.name] = container.resolve(token);
        });
    };
}

class UserService {
    @inject(DatabaseService)
    db!: DatabaseService;
    
    getUser(id: string) {
        return this.db.query(...);
    }
}

四、TS 5.x 其他重要新特性

4.1 verbatimModuleSyntax(TS 5.0)

// tsconfig.json
{
    "compilerOptions": {
        "verbatimModuleSyntax": true
    }
}

强制显式 import type——让 esbuild / swc 能正确处理类型导入。

// NG - 这种写法有问题(运行时也会 import)
import { SomeType } from "./types";

// OK - 用 import type
import type { SomeType } from "./types";

// 混用
import { someValue, type SomeType } from "./module";

配合 isolatedModules: true 必须开——否则有些场景类型会被错误删除。

4.2 索引访问类型的优化(TS 5.x)

// TS 5.x 之前
type T = { [K in keyof { a: 1; b: 2 }]: K }[keyof { a: 1; b: 2 }];
// T 是 "a" | "b"(以前不稳定)

// TS 5.x 之后稳定
type T = keyof { a: 1; b: 2 };  // "a" | "b"

4.3 命名空间支持 indexed access(TS 5.x)

namespace MyNamespace {
    export const a = 1;
    export const b = 2;
}

type Keys = keyof typeof MyNamespace;  // "a" | "b"
type A = (typeof MyNamespace)["a"];    // 1

4.4 type predicates 缩窄(TS 5.x)

function isString(value: unknown): value is string {
    return typeof value === "string";
}

const x: unknown = "hello";
if (isString(x)) {
    // TS 5.x 改进: x 进一步缩窄为 string
    x.toUpperCase();
}

4.5 精确可选属性类型(TS 4.4+,5.x 推荐)

{
    "compilerOptions": {
        "exactOptionalPropertyTypes": true
    }
}
interface User {
    name: string;
    age?: number;
}

const u1: User = { name: "Alice", age: undefined };  // 报错
const u2: User = { name: "Alice" };                  // OK
const u3: User = { name: "Alice", age: 30 };        // OK

4.6 NoInfer utility(TS 5.4+)

// 不希望被推断的参数
function createFSM<S extends string>(states: S[], initial: NoInfer<S>) { ... }

createFSM(["idle", "running"], "idle");      // OK
createFSM(["idle", "running"], "paused");   // 报错 - "paused" 不在 states 中

4.7 Object.groupBy / Map.groupBy(TS 5.4+ lib)

const arr = [
    { type: "a", value: 1 },
    { type: "b", value: 2 },
    { type: "a", value: 3 },
];

Object.groupBy(arr, item => item.type);
// { a: [...], b: [...] }

五、TS 5.x 的迁移注意事项

5.1 升级 TS 版本

npm install -D typescript@latest

升级前:

  1. 看 Breaking Changes ——每个 TS 版本都有少量 breaking change
  2. 逐版本升级——不要从 TS 4.x 直接到 TS 5.5
  3. 先看 IDE 警告——vscode TS 插件会提示

5.2 常见 Breaking Changes

TS 5.0

TS 5.4

5.3 实战:我们项目的 TS 升级

公司项目从 TS 4.9 升到 TS 5.6:

步骤 耗时 问题数
4.9 → 5.0 1 周 3(都是装饰器)
5.0 → 5.2 2 周 5(主要是 decorator 重写)
5.2 → 5.4 1 天 0
5.4 → 5.6 1 天 1(unknown 收窄变严)

半年完成升级,没有任何线上问题。

六、实战:用 satisfies 重构配置

6.1 重构前

interface Config {
    api: { baseUrl: string; timeout: number };
    features: Record<string, boolean>;
}

const config: Config = {
    api: { baseUrl: "https://api.example.com", timeout: 5000 },
    features: { newUI: true, betaSearch: false },
};

// 问题:访问 config.api.baseUrl 是 string(被类型收窄)
// 但实际是字面量 "https://api.example.com"

6.2 用 satisfies 重构

const config = {
    api: { baseUrl: "https://api.example.com", timeout: 5000 },
    features: { newUI: true, betaSearch: false },
} satisfies Config;

// config.api.baseUrl 是 "https://api.example.com"(字面量保留)
// 但 TS 仍然检查:漏字段会报错,类型不对会报错

// 实际好处:可以用在 URL 模板
type ApiBaseUrl = typeof config.api.baseUrl;  // "https://api.example.com"

6.3 实战:i18n 资源

const translations = {
    common: {
        hello: "你好",
        bye: "再见",
    },
    user: {
        profile: {
            title: "个人资料",
            edit: "编辑",
        },
    },
} satisfies Record<string, Record<string, any>>;

// translations.common.hello 是 "你好"(字面量)
// 漏字段会报错

七、实战:用 using 简化资源管理

7.1 实战 1:HTTP 请求上下文

class RequestContext {
    [Symbol.dispose] = () => {
        console.log(`Request done, duration: ${Date.now() - this.start}ms`);
    };
    
    constructor(public start: number = Date.now()) {}
}

async function handleRequest(req: Request) {
    using _ctx = new RequestContext();
    
    // 业务逻辑
    return processRequest(req);
}
// 请求结束自动记录日志

7.2 实战 2:性能计时

class Timer {
    [Symbol.dispose] = () => {
        console.log(`${this.label}: ${Date.now() - this.start}ms`);
    };
    
    constructor(private label: string, private start: number = Date.now()) {}
}

async function heavyOperation() {
    using _t = new Timer("heavyOperation");
    // 业务
}
// 自动打印耗时

7.3 实战 3:多资源管理

async function transfer(from: Account, to: Account, amount: number) {
    await using fromLock = new AsyncLock(from);
    await using toLock = new AsyncLock(to);
    
    // LIFO 顺序释放(后获取的先释放)
    from.withdraw(amount);
    to.deposit(amount);
}
// 两个锁自动释放

八、实战:用 decorator 做日志

8.1 简单日志 decorator

function log<This, Args extends any[], R>(
    originalMethod: (this: This, ...args: Args) => R,
    context: ClassMethodDecoratorContext<This, R>
) {
    return function (this: This, ...args: Args): R {
        console.log(`[${String(context.name)}] called with`, args);
        const result = originalMethod.apply(this, args);
        console.log(`[${String(context.name)}] returned`, result);
        return result;
    };
}

class UserService {
    @log
    async getUser(id: string): Promise<User> {
        return db.user.findUnique({ where: { id } });
    }
}

await userService.getUser("123");
// [getUser] called with ["123"]
// [getUser] returned User {...}

8.2 性能监控 decorator

function measureTime<This, Args extends any[], R>(
    originalMethod: (this: This, ...args: Args) => R | Promise<R>,
    context: ClassMethodDecoratorContext<This, R>
) {
    return async function (this: This, ...args: Args): Promise<R> {
        const start = performance.now();
        try {
            return await Promise.resolve(originalMethod.apply(this, args));
        } finally {
            const duration = performance.now() - start;
            metrics.histogram(`method.${String(context.name)}.duration`, duration);
        }
    };
}

class OrderService {
    @measureTime
    async createOrder(input: CreateOrderInput): Promise<Order> {
        // 业务逻辑
    }
}

8.3 实战:权限检查 decorator

function requireRole(role: "user" | "admin" | "superadmin") {
    return function (
        originalMethod: (this: any, ...args: any[]) => any,
        context: ClassMethodDecoratorContext
    ) {
        return function (this: any, ...args: any[]) {
            if (!this.user || !hasRole(this.user, role)) {
                throw new Error(`Permission denied: ${role} required`);
            }
            return originalMethod.apply(this, args);
        };
    };
}

class AdminController {
    user: User;
    
    @requireRole("admin")
    deleteUser(id: string) {
        // 只有 admin 能调
    }
    
    @requireRole("superadmin")
    deleteOrg(orgId: string) {
        // 只有 superadmin 能调
    }
}

九、TS 5.x 的局限与展望

9.1 还没解决的

9.2 未来可能加入

9.3 TS 编译器的 Go 重写

Microsoft 在 2024 年宣布 Project Corsa——用 Go 重写 TS 编译器:

2026 年初已经在 VS Code 里集成 preview。

十、本篇小结

特性 版本 应用
satisfies 5.0 配置对象,保留字面量类型
const 类型参数 5.0 推断为字面量
using / await using 5.2 资源自动释放
标准化 decorator 5.0 日志、权限、DI
verbatimModuleSyntax 5.0 强制显式 import type
NoInfer 5.4 控制泛型推断范围
实战 收益
satisfies 配置 类型严格 + 字面量保留
using 资源 代码量减半,资源安全
decorator 切面编程(AOP)
TS 升级 持续吃新特性

下一篇是 TS 系列的最后一篇,Monorepo 与类型共享——把内部 SDK 的类型发布流程做到极致。

参考资料