把 TS 从头到尾过了一遍

文章目录

    • 一、先交代点背景
    • 二、先建立认知:类型只是编译期的脚手架
    • 三、基础语法复习:每天真用的就这些
      • [3.1 注解能省则省,但这几处必须写](#3.1 注解能省则省,但这几处必须写)
      • [3.2 strict 模式下,null 和 undefined 不能随便塞](#3.2 strict 模式下,null 和 undefined 不能随便塞)
      • [3.3 数组与元组](#3.3 数组与元组)
      • [3.4 对象类型与多余属性检查](#3.4 对象类型与多余属性检查)
      • [3.5 函数类型](#3.5 函数类型)
      • [3.6 联合类型与字面量:TS 最高频的写法](#3.6 联合类型与字面量:TS 最高频的写法)
      • [3.7 enum:枚举](#3.7 enum:枚举)
      • [3.8 断言 as:绕开编译器,不是类型转换](#3.8 断言 as:绕开编译器,不是类型转换)
    • 四、any、unknown、never:三个特殊货
    • [五、type 还是 interface?(面试高频,一次讲透)](#五、type 还是 interface?(面试高频,一次讲透))
      • [5.1 声明合并:给改不了源码的类型打补丁](#5.1 声明合并:给改不了源码的类型打补丁)
    • 六、类型收窄与类型谓词
    • 七、泛型:类型层面的"函数参数"
    • [八、为 NestJS 打底的 TS 能力(重点)](#八、为 NestJS 打底的 TS 能力(重点))
      • [8.1 class 复习:从 ES6 到 TS](#8.1 class 复习:从 ES6 到 TS)
      • [8.2 继承、super 与 override](#8.2 继承、super 与 override)
      • [8.3 static:挂在类本身上](#8.3 static:挂在类本身上)
      • [8.4 TS 给 class 加的三样东西](#8.4 TS 给 class 加的三样东西)
      • [8.5 关键分水岭:class 是值,interface 只是类型](#8.5 关键分水岭:class 是值,interface 只是类型)
      • [8.6 依赖注入(DI):一个设计模式,不是框架专利](#8.6 依赖注入(DI):一个设计模式,不是框架专利)
    • 九、装饰器:这篇的主角
      • [9.1 装饰器到底是什么](#9.1 装饰器到底是什么)
      • [9.2 带括号的是装饰器工厂](#9.2 带括号的是装饰器工厂)
      • [9.3 执行顺序](#9.3 执行顺序)
      • [9.4 装饰器 + 元数据:框架自动化的全部原理](#9.4 装饰器 + 元数据:框架自动化的全部原理)
      • [9.5 类比校准](#9.5 类比校准)
      • [9.6 避坑:TS 有两代装饰器](#9.6 避坑:TS 有两代装饰器)
    • 十、tsconfig:把前面讲的全部串起来
    • 写在最后

P.S. 无意间发现了一个巨牛的人工智能教程,非常通俗易懂,对AI感兴趣的朋友强烈推荐去看看, 传送门https://blog.csdn.net/HHX_01

一、先交代点背景

上一期咱们聊完了 Node.js 的核心机制,按路线图下一步该进框架了。我兴冲冲打开 NestJS 的源码,当场表演了一个"地铁老人看手机"。

装饰器、依赖注入、constructor 里那个奇怪的 private,扑面而来的全是 Java 味。我第一反应是:这真的是 JS 吗?这是我认识的那个"随便写写就能跑"的家伙吗?

后来我想明白了,不是框架难,是我把 TS 里那一半"知道但不用"的能力全扔了。就像你考了驾照天天开车,但侧方位停车从来不敢碰------然后你突然上了高速。

为什么偏偏要在这个时间点死磕 TS?因为 AI Coding 时代,类型就是你和 AI 之间签的合同。你把入参出参写清楚,AI 生成的代码就歪不到哪去;你满屏 any,AI 也只能跟着瞎猜------你糊弄它,它就糊弄你,主打一个双向奔赴。

所以这篇我把 TS 从头到尾过了一遍,按「概念 → 代码 → 坑」整理成两大部分:前半篇复习基础语法(每天真用的那些),后半篇讲给框架打底的三块能力:class、依赖注入、装饰器。

开讲前先送一条主线,记住这一句,后面所有"为什么要这样设计"就都有答案了:

TS 的类型在编译后会全部被擦除,运行时一点不剩。

就像你精心写的备注,编译器编译完直接帮你删了,比渣男删聊天记录还干脆。后面所有内容,全是在这句话上长出来的。

二、先建立认知:类型只是编译期的脚手架

TS 本质上是给 JS 加了一层编译期的检查脚手架。tsc 编译完,类型注解、接口、as 断言全部消失,产物就是纯 JS。

这个认知直接推出三个结论:

  1. as 断言运行时什么都不做。它不是类型转换,是你拍着胸脯跟编译器说"我懂,你信我"。
  2. 类型再严也挡不住运行时错误 。JSON.parse 回来的东西标了 User,它就真是 User 了?编译器只是信了你的鬼话。
  3. 凡是要在运行时用类型信息的地方,都得有编译后还活着的载体。这条是后面理解依赖注入的钥匙,先记着。

三、基础语法复习:每天真用的就这些

3.1 注解能省则省,但这几处必须写

ts 复制代码
let name: string = 'kimi'; // 显式注解
let age = 18; // 推断为 number,注解可省
const PI = 3.14; // const 推断为字面量类型 3.14,不是 number

TS 的推断能力强得离谱,能推断就不写。但有三个地方必须显式写:函数返回值、复杂对象、空数组 。特别是空数组,[] 会被推断成 any[],之后你 push 什么它都不检查------典型的"开局一把刀,装备全靠刷"。

3.2 strict 模式下,null 和 undefined 不能随便塞

ts 复制代码
let s: string = null; // ❌ strict 下报错
let s2: string | null = null; // ✅ 显式声明"可能为空"

这是 strictNullChecks 的核心价值:逼你把"可能为空"写进类型里,让 Cannot read properties of undefined 这类线上事故在编译期就现形。新项目不开 strict?那跟出门不锁门差不多。

3.3 数组与元组

ts 复制代码
const list: string[] = ['a', 'b'];
const matrix: number[][] = [[1, 2]];
// 元组:定长、定类型、定顺序
const pair: [string, number] = ['age', 18];

坑:push 不受元组长度限制(pair.push('x') 编译不报错)------元组只保证"读取和解构时"的类型,这是 TS 已知的类型不健全点。什么意思呢?就好比你买了个两座跑车,结果后面还能塞人,塞进去了还不报警。

3.4 对象类型与多余属性检查

ts 复制代码
type User = {
  name: string;
  age?: number; // ? 可选属性
  readonly id: string; // 只读(编译期约束,运行时照常能改)
};

坑:对象字面量直接传参会触发多余属性检查(excess property check);先赋给变量再传就跳过了------拼错的字段会被悄悄忽略:

ts 复制代码
greet({ id: '1', name: 'a', nme: 'b' }); // 报错:nme 是多余属性
const u = { id: '1', name: 'a', nme: 'b' };
greet(u); // 不报错(nme 悄悄被忽略)

3.5 函数类型

ts 复制代码
// 完整写法
const add: (a: number, b: number) => number = (a, b) => a + b;
// 可选参数、默认值、rest 参数
function build(name: string, age?: number, ...tags: string[]) {}

坑:age?: number 和 age: number | undefined 不等价------前者可以不传 ,后者必须传一个 undefined。这俩的区别就像"你走不走"和"你必须明确表态":一个是你自由发挥,一个是逼你站队。

3.6 联合类型与字面量:TS 最高频的写法

ts 复制代码
type Status = 'pending' | 'success' | 'failed';
function setStatus(s: Status) {}
setStatus('success'); // ✅
setStatus('done'); // ❌ 编译报错

后面所有的类型收窄、判别字段(discriminated union)都建立在联合类型上,务必熟练。这是 TS 的立命之本,就像米饭之于干饭人。

3.7 enum:枚举

ts 复制代码
enum Direction { Up, Down, Left, Right }
// 数字枚举,默认从 0 起
Direction[0]; // 'Up' ← 双向映射,行为反直觉,打包体积还大

我们一般也不会这么用,都会给定义好枚举值。数字枚举那个双向映射------你说你是枚举,结果既能从名字查值,又能从值查名字,跟个双面间谍似的。

3.8 断言 as:绕开编译器,不是类型转换

ts 复制代码
const data = JSON.parse('{}') as User; // 编译产物里这个 as 完全消失

纪律:能用收窄解决就不用 as ;as unknown as X 双重断言基本等于作弊,Code Review 应当拦下。AI 生成的代码里 as 出现频率偏高,验收时重点扫这个关键字------它一多,就说明 AI 也拿不准类型,跟你一样在赌"编译器不会管"。

四、any、unknown、never:三个特殊货

any:编译器彻底下班。

标了 any 的变量,能装任何值、能赋给任何人、能调任何方法------类型检查对它完全关闭。所以 any 的真正危害不是"不检查",是会传染:

ts 复制代码
const data: any = JSON.parse('{}');
const name: string = data.user.name; // 编译通过,运行时可能直接炸

data 流向的每一处,类型检查全部失效,错误被推迟到运行时。一个 any 标记,能让一片区域的上下文全部失真------这是 AI Coding 时代 any 的新代价。团队里建议直接用 ESLint 的 @typescript-eslint/no-explicit-any 禁掉。一个 any 就像一颗老鼠屎,能坏一锅类型粥。

unknown:安全版的 any。

它和 any 一样能装任何值,但反过来:你不能把它直接赋给别人,也不能直接调它的方法------想用,先收窄:

ts 复制代码
let u: unknown = JSON.parse('{}');
if (typeof u === 'object' && u !== null) {
  // 收窄之后才能用
}

实战规则:API 响应、JSON.parse、catch 的 error(开了 useUnknownInCatchVariables 之后默认就是 unknown),一律标 unknown,逼自己走一遍收窄。多花两行代码,换类型检查重新上岗,值。unknown 就是戴了口罩的 any------能进这个门,但必须过安检。

never:什么都装不了的类型。

它是所有类型的子类型------能赋给任何类型,但没有任何值能赋给它。日常用到它的场景有两个:一是给"永不返回"的函数(抛错、死循环)当返回类型;二是穷举检查:

ts 复制代码
function handle(s: Status) {
  switch (s) {
    case 'pending': return;
    case 'success': return;
    case 'failed': return;
    default:
      const exhaustive: never = s;
      throw new Error(`unhandled: ${exhaustive}`);
  }
}

所有分支都处理完,能走到 default 的 s 理论上"不存在",标成 never 让编译器帮你守着;哪天 Status 加了新成员,这行立刻编译报错,提醒你回来补分支。never 就是类型界的幽灵------所有人都能变成它,但它谁都不要。

五、type 还是 interface?(面试高频,一次讲透)

先说相同点:都能描述对象结构、都能描述函数、都能被扩展。光看这些会觉得它俩可以互换------确实大量场景能互换,真正的区别藏在下面三点。

区别一:能描述的范围不同。

interface 只能描述"对象结构";type 是类型别名,什么类型都能起名字------联合、元组、函数、基本类型都行:

ts 复制代码
type ID = string | number; // 联合类型,interface 做不到
type Pair = [string, number]; // 元组,interface 做不到
type Callback = (x: number) => void; // 函数类型,interface 能写但长得很别扭

区别二:interface 会声明合并,type 不会。

同名的 interface 写两次,自动合并成一个;同名的 type 写两次,直接报错:

ts 复制代码
interface User { name: string }
interface User { age: number }
// 现在的 User = { name: string; age: number }

这个特性乍看有点怪,但它正是 interface 最重要的超能力,下面单独讲。就像俩兄弟长得一样,一个能互相补丁,一个见面就打架。

区别三:扩展的写法不同,报错的时机也不同。

interface 用 extends,type 用 & 交叉:

ts 复制代码
interface Admin extends User { role: string }
type Admin = User & { role: string };

效果接近,但处理冲突时不一样:extends 遇到同名属性类型不兼容会当场报错;& 会默默把冲突属性交叉成 never,等到用的时候才发现,排查起来更绕。一个是有话直说,一个是当面不说背后记账。

选择约定:描述对象结构优先 interface(能合并、错误提示更友好);联合、函数、复杂组合用 type。面试如果再被追问一句"为什么第三方库的类型定义几乎都用 interface",答案就落在声明合并上。

5.1 声明合并:给改不了源码的类型打补丁

想象一个场景:你在用一个库,想给它暴露出来的某个类型加一个字段。库的类型你改不了源码;extends 一个子类型?也不行------库里所有函数接收和返回的还是旧类型,你的子类型塞不回去。

声明合并就是干这个的:不动库的源码,直接往它的 interface 里追加成员:

ts 复制代码
// 经典场景:给 Express 的 Request 加 user 字段
declare module 'express' {
  interface Request {
    user?: { id: string };
  }
}
// 之后项目里所有 req.user 都有类型提示了,库本身一行没改

和 extends 的本质区别:extends 是造一个新类型 (调用方得换名字);声明合并是增强旧类型本身(所有用到它的地方自动升级)。这就好比一个是重新注册商标,一个是给老字号偷偷加经营范围------后者不改招牌,顾客还都认。

它比"往 window 上挂全局变量"那套野路子安全,靠的是三道护栏:

  1. 编译期生效,产物无痕迹
  2. 同名成员类型冲突 → 直接报错(只能增补,不能覆盖)
  3. 默认模块作用域 隔离,跨文件需显式 declare module / declare global

纪律:默认不用;只在打补丁时用;补丁集中在 types/*.d.ts;declare global 慎用。

六、类型收窄与类型谓词

内置守卫:typeof、instanceof、in、判别字段(code === 0)。但有一个经典场景内置守卫搞不定:判断逻辑抽成了函数 ------函数返回 boolean,TS 看不懂它和类型的关系。这时需要类型谓词:

ts 复制代码
function isCat(animal: Cat | Dog): animal is Cat { // ← 给编译器的承诺书
  return 'meow' in animal;
}
if (isCat(animal)) {
  animal.meow(); // ✅ 收窄成功
}

三要素:参数名对应、窄类型与宽类型相容、函数体返回 boolean。

最高频的实战场景------数组 filter 收窄:

ts 复制代码
const list: (string | null)[] = ['a', null, 'b'];
const r = list.filter((x): x is string => x !== null); // string[]

⚠️ 编译器不验证承诺的真假,判断逻辑必须自己写对。类型谓词本质是你给编译器写承诺书------签字画押就行,但你要是签假合同,编译器可不管你。这条在验收 AI 生成的谓词函数时尤其重要------签名写得再漂亮,函数体错了编译器也不拦。

七、泛型:类型层面的"函数参数"

ts 复制代码
function first(arr: T[]): T {
  return arr[0];
}
// 泛型约束:限制 T 必须有 length
function logLen(arg: T): T {
  console.log(arg.length);
  return arg;
}
// 泛型默认值
interface ApiResponse {
  code: number;
  data: T;
}

理解泛型只需要一个类比:普通函数抽象"值",泛型抽象"类型" 。T 就是类型层面的形参,extends 是参数校验,= unknown 是默认参数。T 就是类型界的实习生------你给它安排什么活,它就地变成什么样。

服务端代码里泛型无处不在:Promise、ApiResponse 这种响应包装、数据库层的 Repository 模式。后面读框架源码时满眼都是它,这关必须过。

八、为 NestJS 打底的 TS 能力(重点)

下一期才正式讲 NestJS,所以这部分我不用框架里的具体概念,只讲三样前置能力:class、依赖注入这个设计模式、装饰器。学完你会发现,框架里那些看起来像魔法的东西,全是这三样的组合。所谓魔法,不过是你看不懂的套路。

8.1 class 复习:从 ES6 到 TS

ts 复制代码
class Animal {
  name: string; // 实例属性(挂在 this 上,每个实例各一份)
  constructor(name: string) { // new 时执行的装配车间
    this.name = name;
  }
  speak(): string { // 方法(挂在原型上,所有实例共享一份)
    return `${this.name} makes a sound`;
  }
}
const a = new Animal('cat');

三个认知:

  1. constructor 是 new 时的装配车间,this.xxx = xxx 把参数装到实例上
  2. 方法定义在类体里 → 在原型 上,全实例共享;属性在 constructor 里赋值 → 在实例上,各自一份
  3. class 同时是值 (能 new、能当参数传来传去)和类型 (能标注变量,let b: Animal)------interface 做不到第一点,这是分水岭,8.5 细说

8.2 继承、super 与 override

ts 复制代码
class Dog extends Animal {
  breed: string;
  constructor(name: string, breed: string) {
    super(name); // 子类 constructor 里必须先调 super 才能碰 this
    this.breed = breed;
  }
  override speak(): string { // override:明确声明"我在覆盖父类方法"
    return `${this.name} barks`;
  }
}
  • super(...) 调父类 constructor;super.speak() 调父类方法
  • instanceof 沿原型链判断:dog instanceof Animal → true
  • 开 noImplicitOverride 后,覆盖父类方法必须写 override------防止父类改了方法名,子类这边静默失效。这就跟"领导换了个手机号,你没备注,对方还觉得你认识他"一个道理。

后面学框架时,自定义错误处理、自定义管道经常要继承框架给的基类,用的就是这套。

8.3 static:挂在类本身上

ts 复制代码
class Config {
  static env = process.env.NODE_ENV; // 类属性,不走实例
  static create() { return new Config(); } // 工厂方法惯例
}
Config.env; // 不需要 new

8.4 TS 给 class 加的三样东西

ES6 class 之上,TS 又加了几样,每一样后面都天天见。

参数属性:声明属性、接收参数、赋值 this,三合一:

ts 复制代码
class UserService {
  constructor(private readonly db: Database) {}
}

这一行顶过去三行代码,比双十一满减还划算。后面看框架代码,所有"注入"的写法都长这样,认识它就能读懂一大半。

修饰符 :private / protected / readonly------注意它们全是编译期约束 ,运行时产物里没有真正的私有(真正的私有是 JS 原生的 #field 写法)。就像小区门口的保安,只管进门时看你一眼,进门之后干嘛他不管。

implements 与 abstract:

ts 复制代码
// implements:约束类必须实现接口结构(编译期检查,运行时不存在)
interface Payable { pay(amount: number): void }
class Alipay implements Payable {
  pay(amount: number) { /* ... */ }
}
// abstract:不能实例化,留给子类实现
abstract class BaseRepository {
  abstract find(id: string): unknown;
}

8.5 关键分水岭:class 是值,interface 只是类型

回到全文主线。interface 编译后完全消失,运行时不存在;class 编译后还在------它既是一个值,又是一个类型。

由此得出一个重要推论:框架想在运行时根据"类型"做点什么事,唯一的选择就是 class。下一节讲依赖注入,马上就会用到这个结论。

8.6 依赖注入(DI):一个设计模式,不是框架专利

NestJS 代码里最扎眼的就是"依赖注入",但它不是框架发明的黑话,而是一个朴素的设计模式。我们从零推一遍。

写服务端代码,类之间一定有依赖:UserService 要查数据库,Database 要读配置。最朴素的写法是自己 new:

ts 复制代码
class Database {
  query(sql: string) { /* ... */ }
}
class UserService {
  private db = new Database(); // 依赖由自己创建
}

小项目这么写没问题。项目大了,三个麻烦:

  1. 换实现要改源码 。Database 想换个实现,每个 new 它的地方都得跟着改
  2. 测试没法造假 。想单测 UserService 又不想真连数据库?依赖写死在类内部,假的塞不进去
  3. 依赖链全靠手工维护。A 依赖 B、B 依赖 C......每层都自己 new,谁先谁后、是不是同一个实例,全靠自己操心

依赖注入的思路很直白:类不自己造依赖,只在构造函数里声明"我需要什么";剩下的交给一个容器------由它统一 new、按依赖顺序组装、从构造函数传进来:

ts 复制代码
class UserService {
  constructor(private db: Database) {} // 只声明需求,不关心怎么造
}
// 容器替你干的事(伪代码):
const db = new Database();
const userService = new UserService(db);

创建对象的控制权,从"类自己"手里反转给了"容器"------所以 DI 又叫控制反转(IoC) 。以前是类自己动手丰衣足食,现在是容器给你发盒饭。上面三个麻烦随之解决:换实现,改容器的登记就行;测试,往容器里塞个假的 Database;依赖再多,装配顺序容器自己算。

现在可以把上一节的分水岭接上了:**容器是运行时干活的,它怎么知道 UserService 需要一个 Database?**直觉上靠的就是 constructor 上那个类型标注------可类型不是会被擦除吗?所以这里需要两个东西配合:依赖必须用 class 来标(编译后还在),再加一个能把类型信息"抢救"下来的编译开关。这个开关第十节讲,先记住有这回事。

九、装饰器:这篇的主角

铺垫了这么久,终于到正主。NestJS 满天飞的 @ 语法就是它,这节把它彻底拆一遍。

9.1 装饰器到底是什么

剥掉语法糖,装饰器就是一个函数 ,@ 只是调用它的特殊写法。关键特征:它在类被定义的时候执行一次,跟有没有 new 实例毫无关系:

ts 复制代码
function Log(target: any, key: string) {
  console.log(`给方法 ${key} 贴了个标签`);
}
class Demo {
  @Log
  hello() {}
}
// 这段代码一加载就打印:给方法 hello 贴了个标签
// 注意:一次 new 都没发生

这段代码一加载就打印,一次 new 都没发生------活儿先干了,比 996 的打工人都积极。

它能贴的位置有五种:类、方法、属性、方法的参数、访问器。贴的位置不同,函数收到的参数也不同:贴在类上收到类本身;贴在方法上收到(类、方法名、属性描述符);贴在参数上再多一个参数下标。

9.2 带括号的是装饰器工厂

@Log 是直接用这个函数;@Log('xxx') 带括号的,是先调用 Log('xxx'),把它返回的函数当成真正的装饰器------参数靠闭包带进去:

ts 复制代码
function Log(msg: string) {
  return function (target: any, key: string) {
    console.log(`${msg}: ${key}`);
  };
}
class Demo {
  @Log('审计')
  hello() {}
}

带括号的装饰器就像你妈给你介绍对象------先让你妈出马,她选好的人才是你的约会对象,参数就是她提前打听好的条件。

9.3 执行顺序

三条规则,自己跑一遍就记住了:

  • 多个装饰器叠在一处:从下往上执行(洋葱模型,@A @B 作用于 x 等价于 A(B(x)))
  • 同一个类里:实例成员 → 静态成员 → 类装饰器最后
  • 方法和它的参数:参数装饰器先于方法装饰器
ts 复制代码
function A(target: any, key?: string) { console.log('A'); }
function B(target: any, key?: string) { console.log('B'); }
@A            // 类装饰器,后执行
class Demo {
  @B          // 方法装饰器,先执行
  hello() {}
}
// 输出:B → A

从下往上执行,跟剥洋葱一个道理------剥到哪层,哭到哪层。

9.4 装饰器 + 元数据:框架自动化的全部原理

装饰器本身只是"在定义类时执行了一个函数",真正让它有用的是配合 reflect-metadata 这个库,往类上存数据、读数据:

ts 复制代码
import 'reflect-metadata';
function Route(path: string) {
  return function (target: any, key: string) {
    Reflect.defineMetadata('path', path, target, key); // 往类上存一条数据
  };
}
class Demo {
  @Route('/hello')
  hello() {}
}
// 框架启动时:扫一遍所有的类,把标签读出来,按标签办事
const path = Reflect.getMetadata('path', Demo.prototype, 'hello'); // '/hello'

所谓框架的"自动装配",拆开就是三步:装饰器贴标签 → 启动时 Reflect 读标签 → 按标签注册和组装。跟超市扫码结账一个流程:贴条码、扫条码、算账。NestJS 的路由注册、依赖注入全是这个套路,下一期我们完整地拆它。

9.5 类比校准

装饰器不是 slot(留坑填内容),它更像 React HOC、Koa 洋葱、axios 拦截器------给已有的东西包一层壳。一句话:它是个套娃,不是个抽屉。

9.6 避坑:TS 有两代装饰器

  • 旧版 experimental:支持参数装饰器,NestJS 用的就是这套
  • TS 5.0+ 新标准:不支持参数装饰器

网上资料对不上时,先确认对方讲的是哪一代。tsconfig 里的 experimentalDecorators: true,就是显式选择旧版。两代装饰器就像两代身份证,长得像,但有的业务只认旧版。

十、tsconfig:把前面讲的全部串起来

json 复制代码
{
  "compilerOptions": {
    "strict": true, // 严格模式全家桶,必开
    "experimentalDecorators": true, // 启用旧版装饰器(NestJS 需要)
    "emitDecoratorMetadata": true, // 把参数类型贴成元数据------DI 的魔法来源
    "target": "ES2021",
    "module": "CommonJS" // Node 服务端惯例
  }
}

第八节留了个悬念:constructor 上的类型标注会被擦除,容器运行时怎么知道每个类需要什么依赖?答案就是 emitDecoratorMetadata:开了它,编译器会自动把装饰器目标的类型信息存成元数据 (design:paramtypes)。运行时 Reflect.getMetadata 一读,"UserService 需要一个 Database"这件事就被抢救下来了。类型信息本来要被删的,这个开关等于在删除前做了个备份------比云盘备份还靠谱。

所以这份配置里每一项都对应前面讲过的机制:strict 对应第二章,experimentalDecorators 对应第九节,emitDecoratorMetadata 对应第八章的悬念。没有玄学,全是因果。

写在最后

回顾这篇的主线:

  1. 类型在编译后被全部擦除------TS 只是编译期的脚手架
  2. 所以 as 不转换数据,interface 运行时不可查,运行时要用类型只能靠 class
  3. 依赖注入容器在运行时工作,靠 class 标注 + 元数据才知道每个类需要什么
  4. 装饰器贴标签,Reflect 读标签,emitDecoratorMetadata 负责把类型抢救下来------自动装配的全部真相

再呼应下开头:AI Coding 时代,TS 的价值反而被放大了。类型是你和 AI 之间最精确的契约,是生成质量的护栏,也是验收生成代码时最先该扫的东西。把 TS 功底打扎实,是这个时代投入产出比最高的学习之一------别让你和 AI 的合同,签在了一张空白的纸上。

下一期正式进框架:Express、Koa、NestJS 三代框架的中间件模型是怎么演进的?为什么公司里的框架长得像 NestJS?这篇埋的"装饰器 + 元数据"伏笔,到时候全部回收。

最后留个小问题:你在让 AI 生成代码时,有没有因为类型标注不清而翻过车?评论区聊聊,让我看看有多少人跟我一样,曾经在 any 的海洋里游过泳。

P.S. 无意间发现了一个巨牛的人工智能教程,非常通俗易懂,对AI感兴趣的朋友强烈推荐去看看,传送门https://blog.csdn.net/HHX_01

相关推荐
我是你的开心果7781 小时前
ai全栈软件开发day26
人工智能
hahaha60161 小时前
完美反射法--白平衡算法
人工智能·算法
jimmyleeee1 小时前
大模型安全之三十九:幻觉防线---- AI 幻觉检测、治理与预防指南
人工智能·安全
梦帮科技2 小时前
可证伪性工程测评与生产部署红蓝对抗:压力测试、长尾鲁棒性与全生命周期安全质检守卫
人工智能·深度学习·神经网络·安全·机器学习·自然语言处理·压力测试
蜗牛互联网2 小时前
Gemini 4 Argon的1M输出窗口与长程Agent工程边界
java·人工智能·后端
dtsola2 小时前
一个人怎么指挥一支 AI 队伍
人工智能·程序员·ai创业·独立开发者·openclaw·一人公司·小遥claw
坤盾科技2 小时前
用坤擎智能体搭一套企业级 AI 中台
人工智能·大模型·企业数字化·ai智能体·坤擎智能体
johnsong2 小时前
幽灵协议:当AI推荐死去的SaaS,编码Agent控制层正在形成
大数据·人工智能
这张生成的图像能检测吗2 小时前
(论文速读)DefectDiffu:基于一致性建模的少样本工业缺陷图像生成
人工智能·深度学习·计算机视觉·异常检测·少样本学习·扩散生成