From 2148ad8427588b9aa1ef44c0333f9bd740c8cfbe Mon Sep 17 00:00:00 2001
From: WEIREN FENG
- 给一个值「定类型」,其实是在对编译器说话,
- 而这三种写法说的是三句不同的话。注解(
- name: string,等于告诉编译器:
- 这个类造出来的每个对象都有 name,而且是 string。
- 于是别处的代码可以直接写{" "}
+ 这个类造出来的每个对象都有 name,而且是 string。于是别处的代码可以直接写{" "}
shop.name.toUpperCase(),不必先判断{" "}
undefined。
>
@@ -650,8 +647,7 @@ export default function ClassesPage() {
<>
把赋值挪到别的方法里。构造器里调{" "}
this.init()、由 init 给字段赋值 ——
- 照样报 ts(2564)。编译器不跨方法追踪赋值,
- 因为它无法确定那个方法一定会被调用。要么就在构造器里赋值。
+ 照样报 ts(2564)。编译器不跨方法追踪赋值,因为它无法确定那个方法一定会被调用。要么就在构造器里赋值。
>
}
/>
@@ -676,11 +672,8 @@ export default function ClassesPage() {
太早搬出 !。
- boss!: string 是明确赋值断言:
- 它把这份保证从编译器手里转到你手里。检查不再报错,
- 但也没有任何验证。真是 undefined{" "}
- 的话,出问题的时刻挪到了运行时。
- 只在「类外有东西确定会赋值」时用它,比如由框架注入的字段。
+ boss!: string 是明确赋值断言:它把这份保证从编译器手里转到你手里。检查不再报错,但也没有任何验证。真是 undefined{" "}
+ 的话,出问题的时刻挪到了运行时。只在「类外有东西确定会赋值」时用它,比如由框架注入的字段。
>
}
/>
@@ -737,9 +730,7 @@ export default function ClassesPage() {
}
zh={
<>
- 方法没有新规则。方法就是写在类里的函数,
- 参数和返回值的标注方式和第 02 章完全一样。
- 真正的新内容从下一节开始:规定哪些代码可以读某个成员。
+ 方法没有新规则。方法就是写在类里的函数,参数和返回值的标注方式和第 02 章完全一样。真正的新内容从下一节开始:规定哪些代码可以读某个成员。
>
}
/>
@@ -772,8 +763,7 @@ export default function ClassesPage() {
}
zh={
<>
- public 表示任何代码都能读这个成员。它是默认值,
- 不写修饰符就等于写了 public。protected{" "}
+ public 表示任何代码都能读这个成员。它是默认值,不写修饰符就等于写了 public。protected{" "}
收紧到「本类,以及任何 extends 它的类」。private{" "}
再收紧一步,只剩类体自己。写成代码是这样:
>
@@ -807,9 +797,7 @@ export default function ClassesPage() {
}
zh={
<>
- 上面有两行是报错的。最快的确认办法,是让编译器自己说。
- 下面这个窗口里跑的是真的 tsc,就在你的浏览器里:
- 把 protected 删掉,或者把 private{" "}
+ 上面有两行是报错的。最快的确认办法,是让编译器自己说。下面这个窗口里跑的是真的 tsc,就在你的浏览器里:把 protected 删掉,或者把 private{" "}
改成 public,对应的那条报错会自己消失。
>
}
@@ -853,8 +841,7 @@ export default function ClassesPage() {
标题栏上还挂着另外两份稿子。只声明不赋值
那一份里有两个字段,一个都没给值 —— 这正是 §01
那道检查,ts(2564)。把代码上方的{" "}
- strictPropertyInitialization 关掉,
- 两条报错一起消失:代码一个字没变,变的是一个编译选项。
+ strictPropertyInitialization 关掉,两条报错一起消失:代码一个字没变,变的是一个编译选项。
>
}
/>
@@ -909,8 +896,7 @@ export default function ClassesPage() {
zh={
<>
有一个后果很容易忽略:子类自己的字段初始化,是在{" "}
- super() 返回之后才执行的。
- 所以父类构造器如果去读一个由子类初始化的字段,读到的是{" "}
+ super() 返回之后才执行的。所以父类构造器如果去读一个由子类初始化的字段,读到的是{" "}
undefined。父类构造器需要的东西,应该以参数形式传给它。
>
}
@@ -946,11 +932,9 @@ export default function ClassesPage() {
}
zh={
<>
- 第 01 章说过,TypeScript 编译时会把类型全部擦除。
- 访问修饰符是类型系统的一部分,所以同样会被擦掉:
+ 第 01 章说过,TypeScript 编译时会把类型全部擦除。访问修饰符是类型系统的一部分,所以同样会被擦掉:
private vaultCode{" "}
- 在产物里就是一个普通属性,谁都读得到。
- JavaScript 自己也有私有字段,写作 # 开头 ——
+ 在产物里就是一个普通属性,谁都读得到。JavaScript 自己也有私有字段,写作 # 开头 ——
那是另一套机制。两边对照着看:
>
}
@@ -998,9 +982,7 @@ export default function ClassesPage() {
<>
注意左边第 7 行。s["vaultCode"]{" "}
完全不报错,连断言都不用写:TypeScript
- 有意允许用方括号访问私有成员,给测试和老代码留一条路。
- 而一个普通 JavaScript 文件引入这个类时更是毫无察觉,
- 直接 s.vaultCode 就读到了。
+ 有意允许用方括号访问私有成员,给测试和老代码留一条路。而一个普通 JavaScript 文件引入这个类时更是毫无察觉,直接 s.vaultCode 就读到了。
>
}
/>
@@ -1022,10 +1004,8 @@ export default function ClassesPage() {
}
zh={
<>
- 上面那张对照是事先摆好的,下面这个不是:一个类,两种私有字段,
- 底下是真编译器。切到编译产物 JS 读一遍类体 ——{" "}
- private 不在里面,那个词只是给类型检查器看的;
- 而 #realCode 还在,和你写下的一模一样。
+ 上面那张对照是事先摆好的,下面这个不是:一个类,两种私有字段,底下是真编译器。切到编译产物 JS 读一遍类体 ——{" "}
+ private 不在里面,那个词只是给类型检查器看的;而 #realCode 还在,和你写下的一模一样。
>
}
/>
@@ -1068,10 +1048,8 @@ export default function ClassesPage() {
有两件事值得试。把 target 调到{" "}
es2015 —— 那个版本的 JavaScript 还没有{" "}
# 字段:编译器会把 #realCode{" "}
- 改写成一个以实例为键的 WeakMap,
- 好让它在那里也照样读不到。private{" "}
- 不需要这套机械,因为它在运行时本来就没许诺过什么。
- 然后换上从类外读一读,对比那两条报错:ts(2341)
+ 改写成一个以实例为键的 WeakMap,好让它在那里也照样读不到。private{" "}
+ 不需要这套机械,因为它在运行时本来就没许诺过什么。然后换上从类外读一读,对比那两条报错:ts(2341)
来自类型系统,ts(18013) 来自 JavaScript 这门语言。
>
}
@@ -1098,8 +1076,7 @@ export default function ClassesPage() {
zh={
<>
没有运行时开销,报错清楚,而且会参与类型兼容判定(§06
- 讲的就是它)。用它来标记哪些成员属于内部实现,
- 让改代码的人不会顺手伸进去。封装的大部分需求,到这里就够了。
+ 讲的就是它)。用它来标记哪些成员属于内部实现,让改代码的人不会顺手伸进去。封装的大部分需求,到这里就够了。
>
}
/>
@@ -1123,10 +1100,7 @@ export default function ClassesPage() {
}
zh={
<>
- 这是 JavaScript 的语言特性,不是 TypeScript 的。
- 它在编译后依然存在,程序运行时类外也确实读不到。
- TypeScript 对它照样做完整的类型检查。
- 当「你控制不了的代码绝对不能碰到这个字段」时用它 ——
+ 这是 JavaScript 的语言特性,不是 TypeScript 的。它在编译后依然存在,程序运行时类外也确实读不到。TypeScript 对它照样做完整的类型检查。当「你控制不了的代码绝对不能碰到这个字段」时用它 ——
主要是写库的场景。
>
}
@@ -1161,9 +1135,7 @@ export default function ClassesPage() {
字段并不构成保护。一句 JSON.stringify(shop){" "}
就打印出来了,浏览器 DevTools
一样看得见,任何序列化对象的日志也一样。
- #field 确实能挡住这几处,
- 但值仍然是内存里的明文,如果是硬编码的,它照样在你的打包产物里。
- 真正的机密应该放在服务端,而不是任何一种字段里。
+ #field 确实能挡住这几处,但值仍然是内存里的明文,如果是硬编码的,它照样在你的打包产物里。真正的机密应该放在服务端,而不是任何一种字段里。
>
}
/>
@@ -1195,9 +1167,7 @@ export default function ClassesPage() {
}
zh={
<>
- 参数属性(parameter properties)省下的样板最多:
- 在构造器参数前面加一个访问修饰符,这一行就同时完成了
- 声明字段、接收实参、赋值三件事:
+ 参数属性(parameter properties)省下的样板最多:在构造器参数前面加一个访问修饰符,这一行就同时完成了声明字段、接收实参、赋值三件事:
>
}
/>
@@ -1253,14 +1223,11 @@ export default function ClassesPage() {
}
zh={
<>
- 这是本章唯一一个在 JavaScript 里没有对应写法的特性。
- 这一章其他东西要么被擦除,要么本来就是 JavaScript;
- 而参数属性会让编译器生成一句你源码里没写过的{" "}
+ 这是本章唯一一个在 JavaScript 里没有对应写法的特性。这一章其他东西要么被擦除,要么本来就是 JavaScript;而参数属性会让编译器生成一句你源码里没写过的{" "}
this.db = db。Node 内置的 TypeScript 支持(22.18
起)只做类型剥离,所以会拒绝这种语法。打开{" "}
erasableSyntaxOnly 后,编译器会以 ts(1294)
- 提前提醒你。有正常构建步骤时尽管用;
- 但如果要让 Node 直接跑 .ts 文件,就别用它。
+ 提前提醒你。有正常构建步骤时尽管用;但如果要让 Node 直接跑 .ts 文件,就别用它。
>
}
/>
@@ -1309,12 +1276,9 @@ export default function ClassesPage() {
zh={
<>
readonly 和 private{" "}
- 一样是编译期检查,而且管的范围比看上去窄:
- 它禁止的是给这个属性赋值,不是禁止改属性指向的那个对象。
- 比如 readonly tags: string[],
+ 一样是编译期检查,而且管的范围比看上去窄:它禁止的是给这个属性赋值,不是禁止改属性指向的那个对象。比如 readonly tags: string[],
o.tags = [] 会报错,但{" "}
- o.tags.push("x") 完全合法。
- 想在运行时也拦住修改,那是 Object.freeze 的事。
+ o.tags.push("x") 完全合法。想在运行时也拦住修改,那是 Object.freeze 的事。
>
}
/>
@@ -1345,8 +1309,7 @@ export default function ClassesPage() {
zh={
<>
从 TypeScript 4.3 起,setter 接受的类型可以比 getter
- 返回的更宽,只要 getter 的类型能赋给 setter 的类型。
- 当你想接受多种输入形式、却总是返回同一种时很好用:
+ 返回的更宽,只要 getter 的类型能赋给 setter 的类型。当你想接受多种输入形式、却总是返回同一种时很好用:
set sugar(v: number | string) 配上{" "}
get sugar(): number,调用方可以写{" "}
o.sugar = "30",而读 o.sugar{" "}
@@ -1384,9 +1347,7 @@ export default function ClassesPage() {
}
zh={
<>
- abstract 类是一个没写完的类:一部分成员已经实现,
- 标了 abstract 的那些只有签名。
- 它不能被实例化;子类必须把每一个抽象成员都实现掉,才能拿来用:
+ abstract 类是一个没写完的类:一部分成员已经实现,标了 abstract 的那些只有签名。它不能被实例化;子类必须把每一个抽象成员都实现掉,才能拿来用:
>
}
/>
@@ -1418,8 +1379,7 @@ export default function ClassesPage() {
<>
implements 是一次检查,不是继承:interface
列出一个类必须有哪些成员,implements{" "}
- 让编译器去核对这个类有没有。它不会给类添加任何东西。
- 收银台不关心背后接的是哪家支付服务,只要那个对象能{" "}
+ 让编译器去核对这个类有没有。它不会给类添加任何东西。收银台不关心背后接的是哪家支付服务,只要那个对象能{" "}
pay、能 refund:
>
}
@@ -1451,8 +1411,7 @@ export default function ClassesPage() {
zh={
<>
interface 的价值体现在调用方:MilkTeaShop 接受任何
- 符合 PaymentProvider 的对象,
- 所以换一家支付服务,店铺内部一行都不用改:
+ 符合 PaymentProvider 的对象,所以换一家支付服务,店铺内部一行都不用改:
>
}
/>
@@ -1491,13 +1450,10 @@ export default function ClassesPage() {
}
zh={
<>
- ① implements 不会改变类的类型:
- 它不添加成员,也不替你标注任何东西。如果写成{" "}
+ ① implements 不会改变类的类型:它不添加成员,也不替你标注任何东西。如果写成{" "}
pay(amount) {"{ … }"} 而不标类型,
- amount 不会变成 number,
- 而是隐式 any,在 strict 下直接报错
- ts(7006)。参数类型得自己写,
- 编译器再拿你写的去和接口核对。
+ amount 不会变成 number,而是隐式 any,在 strict 下直接报错
+ ts(7006)。参数类型得自己写,编译器再拿你写的去和接口核对。
>
}
/>
@@ -1518,10 +1474,7 @@ export default function ClassesPage() {
zh={
<>
② abstract class 和 interface 的区别。interface
- 只描述形状,编译期就被擦除 —— 运行时这个名字根本不存在。
- abstract 类是一个真实的类:它在运行时存在,
- 可以带着已实现的方法和状态,子类会继承这些。
- 想让子类继承可用的代码,用 abstract 类;只想描述形状,用 interface。
+ 只描述形状,编译期就被擦除 —— 运行时这个名字根本不存在。abstract 类是一个真实的类:它在运行时存在,可以带着已实现的方法和状态,子类会继承这些。想让子类继承可用的代码,用 abstract 类;只想描述形状,用 interface。
>
}
/>
@@ -1581,9 +1534,7 @@ export default function ClassesPage() {
}
zh={
<>
- 第 04 章说过,TypeScript 比较类型看的是形状,不是名字。
- class 也不例外:两个毫无关系的类,只要成员一样就可以互换。
- 但有一个例外,只要类里出现 private 或{" "}
+ 第 04 章说过,TypeScript 比较类型看的是形状,不是名字。class 也不例外:两个毫无关系的类,只要成员一样就可以互换。但有一个例外,只要类里出现 private 或{" "}
protected 成员,它就出现了。逐帧看:
>
}
@@ -1612,8 +1563,7 @@ export default function ClassesPage() {
}
zh={
<>
- 把两行 private stock 都删掉,赋值就通过了;
- 或者保留它,把这个声明挪到两个杯子共同 extends
+ 把两行 private stock 都删掉,赋值就通过了;或者保留它,把这个声明挪到两个杯子共同 extends
的基类里 —— 这样声明只有一处,两个类型重新兼容。
>
}
@@ -1642,11 +1592,8 @@ export default function ClassesPage() {
}
zh={
<>
- 类型按形状比较,但只要涉及 private 或 protected 成员,
- 两个类型必须让这些成员来自同一处声明才兼容
- (从共同基类继承下来的算)。
- 这也正是一些库模拟名义类型的办法:给类塞一个私有成员,
- 形状相同的路人就再也顶替不了它。
+ 类型按形状比较,但只要涉及 private 或 protected 成员,两个类型必须让这些成员来自同一处声明才兼容
+ (从共同基类继承下来的算)。这也正是一些库模拟名义类型的办法:给类塞一个私有成员,形状相同的路人就再也顶替不了它。
>
}
/>
@@ -1694,10 +1641,8 @@ export default function ClassesPage() {
),
zh: (
<>
- 声明了的字段必须真的被赋值:就地初始化、在构造器里赋值,
- 或者用 !{" "}
- 自己承担。strictPropertyInitialization 负责这项检查,
- 而且它不追踪写在其他方法里的赋值。
+ 声明了的字段必须真的被赋值:就地初始化、在构造器里赋值,或者用 !{" "}
+ 自己承担。strictPropertyInitialization 负责这项检查,而且它不追踪写在其他方法里的赋值。
>
),
},
@@ -1729,10 +1674,8 @@ export default function ClassesPage() {
),
zh: (
<>
- private 会被擦除:编译产物里它就是个普通属性,
- 连在 TypeScript 里 s["field"]{" "}
- 都能直接读到,不用断言。要一个运行时也拿不到的字段,
- 用 JavaScript 的 #field。
+ private 会被擦除:编译产物里它就是个普通属性,连在 TypeScript 里 s["field"]{" "}
+ 都能直接读到,不用断言。要一个运行时也拿不到的字段,用 JavaScript 的 #field。
>
),
},
@@ -1747,8 +1690,7 @@ export default function ClassesPage() {
),
zh: (
<>
- 参数属性一行顶四行,但它是本章唯一会生成代码的特性。
- 只做类型剥离的运行时会拒绝它 —— 比如直接跑{" "}
+ 参数属性一行顶四行,但它是本章唯一会生成代码的特性。只做类型剥离的运行时会拒绝它 —— 比如直接跑{" "}
.ts 文件的 Node。
>
),
@@ -1767,8 +1709,7 @@ export default function ClassesPage() {
<>
abstract{" "}
标记的是不能实例化的类,以及子类必须实现的成员。
- implements 只是拿类去和接口核对:
- 既不添加成员,也不推断参数类型。一个类可以同时接受多个接口的检查。
+ implements 只是拿类去和接口核对:既不添加成员,也不推断参数类型。一个类可以同时接受多个接口的检查。
>
),
},
@@ -1784,8 +1725,7 @@ export default function ClassesPage() {
zh: (
<>
class 也按形状比较,只有一个例外:一旦类里有{" "}
- private 或 protected 成员,
- 这些成员必须来自同一处声明,两个类型才兼容。
+ private 或 protected 成员,这些成员必须来自同一处声明,两个类型才兼容。
>
),
},
diff --git a/app/classes/viz.tsx b/app/classes/viz.tsx
index 041ea14..3f1bd0b 100644
--- a/app/classes/viz.tsx
+++ b/app/classes/viz.tsx
@@ -302,8 +302,7 @@ export function AccessGate() {
<>
九种组合都点一遍。public 三处全通,protected 两通一拦,private
一通两拦。两条报错值得读细:protected 那条末尾多了{" "}
- and its subclasses,private 那条没有。
- 两种检查都只发生在编译期。
+ and its subclasses,private 那条没有。两种检查都只发生在编译期。
>
}
/>
@@ -353,10 +352,8 @@ const ERASE_TABS: EraseTab[] = [
}
zh={
<>
- private 这个词被整个擦掉了。编译产物里就是一个普通属性。
- 连源码里的 shop["vaultCode"] TypeScript
- 都放行 —— 用方括号访问私有成员是它有意保留的后门,连断言都不用写。
- 运行时,JSON.stringify(shop)、DevTools
+ private 这个词被整个擦掉了。编译产物里就是一个普通属性。连源码里的 shop["vaultCode"] TypeScript
+ 都放行 —— 用方括号访问私有成员是它有意保留的后门,连断言都不用写。运行时,JSON.stringify(shop)、DevTools
和任何一个普通 JavaScript 文件都看得见这个值。
>
}
@@ -395,8 +392,7 @@ const ERASE_TABS: EraseTab[] = [
#vaultCode 是 JavaScript 的语法,不是 TypeScript 的。
在较新的 target 下它原样输出,运行时类外确实拿不到。类外写{" "}
shop.#vaultCode 是语法错误,
- JSON.stringify(shop) 打印出来是 {"{}"}。
- target 较老时,编译器会改用 WeakMap 实现,行为一样。
+ JSON.stringify(shop) 打印出来是 {"{}"}。target 较老时,编译器会改用 WeakMap 实现,行为一样。
>
}
/>
@@ -557,8 +553,7 @@ const CUP_FRAMES: CupFrame[] = [
zh={
<>
第 04 章的规则在 class 上照常生效:编译器比较的是形状,不是名字
- 。这两个类声明的成员完全一样,所以要 PaperCup 的地方,
- 给一个 PlasticCup 也接受。
+ 。这两个类声明的成员完全一样,所以要 PaperCup 的地方,给一个 PlasticCup 也接受。
>
}
/>
@@ -582,8 +577,7 @@ const CUP_FRAMES: CupFrame[] = [
}
zh={
<>
- 现在两个类各自声明了一个 private stock。名字一样、
- 类型一样,赋值依然失败:
+ 现在两个类各自声明了一个 private stock。名字一样、类型一样,赋值依然失败:
Types have separate declarations of a private property
'stock'.
@@ -615,9 +609,7 @@ const CUP_FRAMES: CupFrame[] = [
zh={
<>
为什么?对 private 和 protected 成员,TypeScript 比较的是
- 它们在哪里声明,而不是名字和类型。两处声明就是两个不同的成员。
- private 的用意是「这是某一个类的内部细节」,如果别的类能顶替,
- 这个用意就落空了。这是 class 唯一一处按「出身」而不是按形状比较的地方。
+ 它们在哪里声明,而不是名字和类型。两处声明就是两个不同的成员。private 的用意是「这是某一个类的内部细节」,如果别的类能顶替,这个用意就落空了。这是 class 唯一一处按「出身」而不是按形状比较的地方。
>
}
/>
@@ -644,9 +636,7 @@ const CUP_FRAMES: CupFrame[] = [
}
zh={
<>
- 解法顺着规则就出来了:让两个类 extends 同一个基类。
- 这时 private stock 只有一处声明,两边都继承自它,
- 于是重新兼容。
+ 解法顺着规则就出来了:让两个类 extends 同一个基类。这时 private stock 只有一处声明,两边都继承自它,于是重新兼容。
>
}
/>
diff --git a/app/functions/page.tsx b/app/functions/page.tsx
index aa93f65..64a9327 100644
--- a/app/functions/page.tsx
+++ b/app/functions/page.tsx
@@ -837,9 +837,7 @@ export default function FunctionsPage() {
),
zh: (
<>
- 一行签名就记下了每个参数的形状和返回值的形状。
- 写的人不用另作解释,调的人不用猜,
- 而编译器会盯着双方各自守约。
+ 一行签名就记下了每个参数的形状和返回值的形状。写的人不用另作解释,调的人不用猜,而编译器会盯着双方各自守约。
>
),
}}
@@ -885,8 +883,7 @@ export default function FunctionsPage() {
}
zh={
<>
- 先看差距。同一个函数写两遍:左边你得读函数体才知道它想要什么,
- 右边第一行就写明白了。
+ 先看差距。同一个函数写两遍:左边你得读函数体才知道它想要什么,右边第一行就写明白了。
>
}
/>
@@ -931,8 +928,7 @@ export default function FunctionsPage() {
}
zh={
<>
- 参数类型通常必须写:编译器没有别的材料可以读。
- 返回值类型通常不必写,因为编译器会从{" "}
+ 参数类型通常必须写:编译器没有别的材料可以读。返回值类型通常不必写,因为编译器会从{" "}
return 语句里读出来。
>
}
@@ -964,10 +960,7 @@ export default function FunctionsPage() {
}
zh={
<>
- 两种情况。第一种是对外边界:会被其他模块、其他团队,
- 或者发布出去的包调用的函数。
- 这时注解就是那份承诺,能挡住「函数体一改,
- 所有人依赖的类型跟着悄悄变了」。
+ 两种情况。第一种是对外边界:会被其他模块、其他团队,或者发布出去的包调用的函数。这时注解就是那份承诺,能挡住「函数体一改,所有人依赖的类型跟着悄悄变了」。
>
}
/>
@@ -987,9 +980,7 @@ export default function FunctionsPage() {
zh={
<>
第二种是报错的位置会更好。写了注解,错误就报在
- 定义处,也就是你正在改的这一行。不写注解,
- 错误的类型会流出去,报错出现在很远的某个调用处,
- 甚至根本不报。最糟的情况是 any:
+ 定义处,也就是你正在改的这一行。不写注解,错误的类型会流出去,报错出现在很远的某个调用处,甚至根本不报。最糟的情况是 any:
>
}
/>
@@ -1012,8 +1003,7 @@ export default function FunctionsPage() {
}
zh={
<>
- 写上 : Order 相当于在出口装了一道闸。
- 函数体内部仍然可能推断出 any,但调用方拿到的是{" "}
+ 写上 : Order 相当于在出口装了一道闸。函数体内部仍然可能推断出 any,但调用方拿到的是{" "}
Order,写错字段当场被抓。
>
}
@@ -1061,9 +1051,7 @@ export default function FunctionsPage() {
zh={
<>
两者都允许调用方不写这个实参。区别在于参数在函数体
- 内部长什么样。写 topping?: string,
- 它的类型是 string | undefined,用之前得先检查。
- 写 sugar = 50,它的类型就是干净的{" "}
+ 内部长什么样。写 topping?: string,它的类型是 string | undefined,用之前得先检查。写 sugar = 50,它的类型就是干净的{" "}
number:实参没给的时候默认值先跑一遍,
undefined 根本进不到函数体里。
>
@@ -1084,11 +1072,9 @@ export default function FunctionsPage() {
}
zh={
<>
- 显式传 undefined 同样会触发默认值,
- 所以{" "}
+ 显式传 undefined 同样会触发默认值,所以{" "}
pourSugar("Milk Green", undefined){" "}
- 拿到的也是 50。当一个参数存在合理的默认值时,
- 优先用默认值而不是 ? —— 函数体里少一道检查。
+ 拿到的也是 50。当一个参数存在合理的默认值时,优先用默认值而不是 ? —— 函数体里少一道检查。
>
}
/>
@@ -1119,9 +1105,7 @@ export default function FunctionsPage() {
}
zh={
<>
- 实参是按位置对号入座的。可选参数一旦插在中间,
- 它后面的必选参数就再也认不出来了。所以规矩是:
- 必选在前,可选和带默认值的在后。
+ 实参是按位置对号入座的。可选参数一旦插在中间,它后面的必选参数就再也认不出来了。所以规矩是:必选在前,可选和带默认值的在后。
>
}
/>
@@ -1164,8 +1148,7 @@ export default function FunctionsPage() {
zh: (
<>
? 管的是这个实参能不能整个不写;
- | undefined 管的是这个实参能放哪些值。
- 后者仍然要求你传点什么进来。
+ | undefined 管的是这个实参能放哪些值。后者仍然要求你传点什么进来。
>
),
}}
@@ -1185,8 +1168,7 @@ export default function FunctionsPage() {
zh={
<>
rest 参数的类型是一个数组。它也可以是一个元组 ——
- 固定住前面几个位置各自的类型,同时仍然接受后面数量不定的实参。
- 变长元组类型的实用价值就在这里。
+ 固定住前面几个位置各自的类型,同时仍然接受后面数量不定的实参。变长元组类型的实用价值就在这里。
>
}
/>
@@ -1237,10 +1219,7 @@ export default function FunctionsPage() {
}
zh={
<>
- 注意那个回调的返回值写的是 void。
- 在函数类型里,void 的意思不是「你不许返回任何东西」,
- 而是「返回什么调用方都不会用」。
- 所以一个真的返回了东西的函数,照样可以放进来。
+ 注意那个回调的返回值写的是 void。在函数类型里,void 的意思不是「你不许返回任何东西」,而是「返回什么调用方都不会用」。所以一个真的返回了东西的函数,照样可以放进来。
>
}
/>
@@ -1261,10 +1240,7 @@ export default function FunctionsPage() {
),
zh: (
<>
- 这是有意设计的。单行箭头函数会把表达式的值返回出去,
- 不管你要不要。如果 (…) => void 拒绝这种函数,
- 大部分 forEach 回调都得改写成花括号函数体,
- 只为了把那个值丢掉。
+ 这是有意设计的。单行箭头函数会把表达式的值返回出去,不管你要不要。如果 (…) => void 拒绝这种函数,大部分 forEach 回调都得改写成花括号函数体,只为了把那个值丢掉。
>
),
}}
@@ -1327,9 +1303,7 @@ export default function FunctionsPage() {
}
zh={
<>
- 这个方向很好接受:调用方要一个 Animal,
- 拿到的东西是 Animal 而且还多点别的。
- 参数方向则正好反过来 —— 这里最容易让人意外。
+ 这个方向很好接受:调用方要一个 Animal,拿到的东西是 Animal 而且还多点别的。参数方向则正好反过来 —— 这里最容易让人意外。
>
}
/>
@@ -1356,10 +1330,7 @@ export default function FunctionsPage() {
}
zh={
<>
- 从调用方的角度读一遍。持有 FeedAnimal 的那段代码,
- 可以拿任意一只动物来调用它,比如一只猫。
- 而一个要读 d.breed 的函数这时就会出错,
- 所以编译器把它拦下。参数按这种反方向检查,叫做
+ 从调用方的角度读一遍。持有 FeedAnimal 的那段代码,可以拿任意一只动物来调用它,比如一只猫。而一个要读 d.breed 的函数这时就会出错,所以编译器把它拦下。参数按这种反方向检查,叫做
逆变(contravariance),由{" "}
strictFunctionTypes 开关控制,而{" "}
strict 会打开它。
@@ -1393,8 +1364,7 @@ export default function FunctionsPage() {
类型所在的位置。用方法语法声明的成员 ——
也就是写成 feed(a: Animal): void 而不是{" "}
feed: (a: Animal) => void ——
- 沿用的还是旧规则:参数往哪个方向都放得进去。
- 这叫双变(bivariance)。
+ 沿用的还是旧规则:参数往哪个方向都放得进去。这叫双变(bivariance)。
>
}
/>
@@ -1422,14 +1392,9 @@ export default function FunctionsPage() {
}
zh={
<>
- 这是一处已知的、刻意保留的不健全,
- 而不是「恰好安全」的规则。上面最后两行能编译通过,
- 然后把一个普通 Animal 塞进了一个实际上是{" "}
- Dog[] 的数组里。TypeScript 之所以接受,
- 是因为拒绝它就等于让 Array<Dog>{" "}
- 不能当作 Array<Animal> 使用,
- 同时废掉大量现存的 JavaScript 写法。
- 官方在这里选择了可用性而不是健全性,并且把这个取舍写进了文档。
+ 这是一处已知的、刻意保留的不健全,而不是「恰好安全」的规则。上面最后两行能编译通过,然后把一个普通 Animal 塞进了一个实际上是{" "}
+ Dog[] 的数组里。TypeScript 之所以接受,是因为拒绝它就等于让 Array<Dog>{" "}
+ 不能当作 Array<Animal> 使用,同时废掉大量现存的 JavaScript 写法。官方在这里选择了可用性而不是健全性,并且把这个取舍写进了文档。
>
}
/>
@@ -1446,9 +1411,7 @@ export default function FunctionsPage() {
}
zh={
<>
- 实际怎么用:想让 interface 的某个成员走严格检查,
- 就用属性语法声明它。反过来,当某个你以为会报错的赋值被放行时,
- 先看一眼是不是方法语法。
+ 实际怎么用:想让 interface 的某个成员走严格检查,就用属性语法声明它。反过来,当某个你以为会报错的赋值被放行时,先看一眼是不是方法语法。
>
}
/>
@@ -1500,10 +1463,7 @@ export default function FunctionsPage() {
}
zh={
<>
- map 调用回调时会传三个实参,
- 而你几乎总是只写一个参数的回调。在 JavaScript 里,
- 多余的实参会被直接忽略,所以「声明的参数更少」的回调
- 永远可以安全调用。类型系统允许的,正是语言本来就允许的。
+ map 调用回调时会传三个实参,而你几乎总是只写一个参数的回调。在 JavaScript 里,多余的实参会被直接忽略,所以「声明的参数更少」的回调永远可以安全调用。类型系统允许的,正是语言本来就允许的。
>
}
/>
@@ -1519,8 +1479,7 @@ export default function FunctionsPage() {
}
zh={
<>
- 反过来就不安全了。声明了第四个参数的回调,
- 会去读一个根本没人传的实参,所以编译器把它拦下。
+ 反过来就不安全了。声明了第四个参数的回调,会去读一个根本没人传的实参,所以编译器把它拦下。
>
}
/>
@@ -1554,8 +1513,7 @@ export default function FunctionsPage() {
}
zh={
<>
- 有的函数会根据传进来的东西表现不同:传字符串还回来一种结果,
- 传数组还回来另一种。这可以用几条重载签名加一条
+ 有的函数会根据传进来的东西表现不同:传字符串还回来一种结果,传数组还回来另一种。这可以用几条重载签名加一条
实现签名来描述。
>
}
@@ -1588,11 +1546,8 @@ export default function FunctionsPage() {
}
zh={
<>
- 第一条:实现签名不能从外部调用。
- 它只需要和上面每一条重载都兼容。例子里 price{" "}
- 的实现接受 string | string[],
- 但拿一个 string | string[] 的值去调用它却会报错,
- 因为两条重载谁都不接受这个类型。
+ 第一条:实现签名不能从外部调用。它只需要和上面每一条重载都兼容。例子里 price{" "}
+ 的实现接受 string | string[],但拿一个 string | string[] 的值去调用它却会报错,因为两条重载谁都不接受这个类型。
>
}
/>
@@ -1609,9 +1564,7 @@ export default function FunctionsPage() {
}
zh={
<>
- 第二条:TypeScript 按你书写的顺序,取第一条匹配上的重载,
- 而不是去找「最合适的那条」。所以重载的顺序是 API 的一部分:
- 越具体的签名越要写在前面。
+ 第二条:TypeScript 按你书写的顺序,取第一条匹配上的重载,而不是去找「最合适的那条」。所以重载的顺序是 API 的一部分:越具体的签名越要写在前面。
>
}
/>
@@ -1635,8 +1588,7 @@ export default function FunctionsPage() {
}
zh={
<>
- 另一件事是一个名字叫 this 的参数。
- 它不是真的参数,而是告诉编译器:这个函数运行时,
+ 另一件事是一个名字叫 this 的参数。它不是真的参数,而是告诉编译器:这个函数运行时,
this 必须是什么。
>
}
@@ -1658,9 +1610,7 @@ export default function FunctionsPage() {
),
zh: (
<>
- this 参数和其他类型一起被擦除,
- 编译出来的 JavaScript 里是一个单参数函数。
- 它在运行时不改变任何东西,只是让编译器能拦下那些{" "}
+ this 参数和其他类型一起被擦除,编译出来的 JavaScript 里是一个单参数函数。它在运行时不改变任何东西,只是让编译器能拦下那些{" "}
this 会出错的调用。
>
),
@@ -1688,10 +1638,8 @@ export default function FunctionsPage() {
zh={
<>
返回值类型还可以写成 x is Dog 或{" "}
- asserts x is Dog。
- 这是类型谓词和断言签名 ——
- 用签名告诉编译器「这次检查证明了什么」。
- 它们属于收窄的范畴,03 章会讲。
+ asserts x is Dog。这是类型谓词和断言签名 ——
+ 用签名告诉编译器「这次检查证明了什么」。它们属于收窄的范畴,03 章会讲。
>
}
/>
@@ -1735,11 +1683,7 @@ export default function FunctionsPage() {
}
zh={
<>
- readonly 只在编译期拦你。
- 它在产出的 JavaScript 里不留任何痕迹,
- 所以运行时这个属性照样能写。想要真正的运行时冻结,
- 得用 Object.freeze。
- 但编译期这道检查依然值得要:误改几乎都被它挡住了。
+ readonly 只在编译期拦你。它在产出的 JavaScript 里不留任何痕迹,所以运行时这个属性照样能写。想要真正的运行时冻结,得用 Object.freeze。但编译期这道检查依然值得要:误改几乎都被它挡住了。
>
}
/>
@@ -1763,11 +1707,8 @@ export default function FunctionsPage() {
zh={
<>
这个说法不用信,可以当场验。下面这个窗口里,给 id{" "}
- 赋值那一行被画了波浪线;切到编译产物 JS 页签,
- 再找 readonly 这个词 —— 产物里根本没有它。
- 检查发生过,然后被擦掉了。第二条红线来自另一个修饰:
- desc 可能不存在,用之前必须先检查。
- 把 readonly 这个词删掉,它那条报错就跟着没了。
+ 赋值那一行被画了波浪线;切到编译产物 JS 页签,再找 readonly 这个词 —— 产物里根本没有它。检查发生过,然后被擦掉了。第二条红线来自另一个修饰:
+ desc 可能不存在,用之前必须先检查。把 readonly 这个词删掉,它那条报错就跟着没了。
>
}
/>
@@ -1793,9 +1734,7 @@ export default function FunctionsPage() {
}
zh={
<>
- 有时候键是事先列不出来的:库存表明天可能上任何一个新货号。
- 这时用索引签名(index signature),
- 只约定键的类型和值的类型。
+ 有时候键是事先列不出来的:库存表明天可能上任何一个新货号。这时用索引签名(index signature),只约定键的类型和值的类型。
>
}
/>
@@ -1827,10 +1766,8 @@ export default function FunctionsPage() {
}
zh={
<>
- 字段名写得全,就老老实实一个个写出来,编译器会帮你查拼写。
- 只有当键是运行时才定的 ——
- 货号、用户输入、由数据拼出来的字典 —— 才用索引签名。
- 它更宽松,代价正是你刚刚放弃的那道拼写检查。
+ 字段名写得全,就老老实实一个个写出来,编译器会帮你查拼写。只有当键是运行时才定的 ——
+ 货号、用户输入、由数据拼出来的字典 —— 才用索引签名。它更宽松,代价正是你刚刚放弃的那道拼写检查。
>
}
/>
@@ -1879,8 +1816,7 @@ export default function FunctionsPage() {
}
zh={
<>
- 右边用的 & 是交叉类型。
- 值得单独看一眼,因为下一章会拿它和联合类型作对比。
+ 右边用的 & 是交叉类型。值得单独看一眼,因为下一章会拿它和联合类型作对比。
>
}
/>
@@ -1910,9 +1846,7 @@ export default function FunctionsPage() {
),
zh: (
<>
- 两个冲突的属性类型交叉之后会被归约成 never,
- 声明处并不报错,问题要到后面真正使用这个属性的地方才暴露。
- 这就是「扩展对象类型时优先用 extends」的实际理由。
+ 两个冲突的属性类型交叉之后会被归约成 never,声明处并不报错,问题要到后面真正使用这个属性的地方才暴露。这就是「扩展对象类型时优先用 extends」的实际理由。
>
),
}}
@@ -2067,10 +2001,7 @@ export default function FunctionsPage() {
}
zh={
<>
- TypeScript handbook 现行的建议很朴素:
- 两个随便挑一个,在同一个代码库里保持一致。
- 需要联合类型或映射类型时,只有 type 写得出来;
- 写库、并且希望使用者能扩展你的声明时,用{" "}
+ TypeScript handbook 现行的建议很朴素:两个随便挑一个,在同一个代码库里保持一致。需要联合类型或映射类型时,只有 type 写得出来;写库、并且希望使用者能扩展你的声明时,用{" "}
interface。至于「interface{" "}
编译一定更快」这种说法,不足以作为选型依据。
>
@@ -2112,8 +2043,7 @@ export default function FunctionsPage() {
zh: (
<>
高亮的这段签名就是本章开头那一份。注意 toppings{" "}
- 这次用的是默认值而不是 ?:
- 函数体里它的类型就是 Topping[],不含{" "}
+ 这次用的是默认值而不是 ?:函数体里它的类型就是 Topping[],不含{" "}
undefined,所以 toppings.length{" "}
不需要任何检查。
>
@@ -2132,8 +2062,7 @@ export default function FunctionsPage() {
}
zh={
<>
- 签名定下来了,现在换你当编译器。下面六种调用,
- 先猜「能不能通过」,再看编译器的原话。
+ 签名定下来了,现在换你当编译器。下面六种调用,先猜「能不能通过」,再看编译器的原话。
>
}
/>
@@ -2155,11 +2084,8 @@ export default function FunctionsPage() {
}
zh={
<>
- 猜是一半,另一半是自己动手违约。
- 下面这个窗口里是同一份签名,最后一行那次调用守约。
- 把杯型改成名单外的字符串、删掉一个实参、再多塞一个 ——
- 你拿到的每一句都是当场生成的,出自编译器本人。
- 标题栏那三份稿子,是最常见的三种错法。
+ 猜是一半,另一半是自己动手违约。下面这个窗口里是同一份签名,最后一行那次调用守约。把杯型改成名单外的字符串、删掉一个实参、再多塞一个 ——
+ 你拿到的每一句都是当场生成的,出自编译器本人。标题栏那三份稿子,是最常见的三种错法。
>
}
/>
@@ -2205,9 +2131,7 @@ export default function FunctionsPage() {
两份把参数个数搞错的稿子,报的都是 TS2554:
Expected 2-3 arguments, but got 1. 和{" "}
Expected 2-3 arguments, but got 4.
- 那个 2-3 的范围,就是这份签名念出来的样子:
- 两个必填参数,加一个可以不写的。这个范围没有人写在任何地方,
- 是编译器从签名上数出来的 —— 你改一下签名,它当场重数一遍。
+ 那个 2-3 的范围,就是这份签名念出来的样子:两个必填参数,加一个可以不写的。这个范围没有人写在任何地方,是编译器从签名上数出来的 —— 你改一下签名,它当场重数一遍。
>
}
/>
@@ -2244,10 +2168,7 @@ export default function FunctionsPage() {
makeOrder(item: MenuItem, size: Size, toppings?: Topping[]):
Order
- 。第三个参数声明时写的是默认值,
- 显示出来却带着一个问号。
- 因为站在调用方这一侧,两者确实是同一件事:这个实参可以不写。
- 区别在函数体内部,§02 讲过 —— 有默认值,就没有{" "}
+ 。第三个参数声明时写的是默认值,显示出来却带着一个问号。因为站在调用方这一侧,两者确实是同一件事:这个实参可以不写。区别在函数体内部,§02 讲过 —— 有默认值,就没有{" "}
undefined 需要检查了。
>
}
@@ -2275,8 +2196,7 @@ export default function FunctionsPage() {
zh={
<>
03 章里,这个 Order 会长出一个状态字段:
- pending、paid、delivered。
- 写成可辨识联合之后,编译器就能在每个分支里自己算出有哪些字段。
+ pending、paid、delivered。写成可辨识联合之后,编译器就能在每个分支里自己算出有哪些字段。
>
}
/>
@@ -2324,9 +2244,7 @@ export default function FunctionsPage() {
),
zh: (
<>
- 参数类型通常必须写,返回值类型通常靠推断。
- 对外边界上要写,想让错误报在定义处而不是很远的调用处时也要写。
- 它同时能挡住 any 从函数里漏出去。
+ 参数类型通常必须写,返回值类型通常靠推断。对外边界上要写,想让错误报在定义处而不是很远的调用处时也要写。它同时能挡住 any 从函数里漏出去。
>
),
},
@@ -2343,11 +2261,8 @@ export default function FunctionsPage() {
),
zh: (
<>
- ? 让调用方可以不写这个实参,
- 而函数体里的类型会带上 | undefined。
- 默认值同样允许不写,但函数体里的类型不含{" "}
- undefined。可选参数必须排在最后。
- rest 参数是数组,也可以是元组。
+ ? 让调用方可以不写这个实参,而函数体里的类型会带上 | undefined。默认值同样允许不写,但函数体里的类型不含{" "}
+ undefined。可选参数必须排在最后。rest 参数是数组,也可以是元组。
>
),
},
@@ -2363,10 +2278,7 @@ export default function FunctionsPage() {
),
zh: (
<>
- 函数类型里的 void 表示调用方不看返回值,
- 所以真的返回了东西的函数照样放得进去;
- 声明上的 void 则禁止返回值;
- 类型为 void 的变量只接受 undefined。
+ 函数类型里的 void 表示调用方不看返回值,所以真的返回了东西的函数照样放得进去;声明上的 void 则禁止返回值;类型为 void 的变量只接受 undefined。
>
),
},
@@ -2388,8 +2300,7 @@ export default function FunctionsPage() {
返回值按协变检查:返回 Dog 可以放进「要{" "}
Animal」的位置。参数在{" "}
strictFunctionTypes 下按逆变检查 ——
- 但用方法语法写的成员是例外,仍然双变。
- 这个例外是刻意保留的不健全,也正是{" "}
+ 但用方法语法写的成员是例外,仍然双变。这个例外是刻意保留的不健全,也正是{" "}
Array<Dog> 能赋给{" "}
Array<Animal> 的原因。
>
@@ -2406,10 +2317,8 @@ export default function FunctionsPage() {
),
zh: (
<>
- 参数更少的函数可以放进「要求参数更多」的位置,
- 因为 JavaScript 本来就会忽略多余的实参 —— 这就是{" "}
- array.map(x => x) 能编译的原因。
- 反过来,声明的参数比目标提供的还多,会被拒绝。
+ 参数更少的函数可以放进「要求参数更多」的位置,因为 JavaScript 本来就会忽略多余的实参 —— 这就是{" "}
+ array.map(x => x) 能编译的原因。反过来,声明的参数比目标提供的还多,会被拒绝。
>
),
},
@@ -2426,9 +2335,7 @@ export default function FunctionsPage() {
zh: (
<>
重载里,实现签名不能从外部调用;
- TypeScript 按书写顺序取第一条匹配的重载,
- 而不是最合适的那条。this 参数在编译期被擦除,
- 箭头函数不能声明它。
+ TypeScript 按书写顺序取第一条匹配的重载,而不是最合适的那条。this 参数在编译期被擦除,箭头函数不能声明它。
>
),
},
@@ -2465,8 +2372,7 @@ export default function FunctionsPage() {
interface 和 type{" "}
大部分时候可以互换。联合类型和映射类型只能用{" "}
type;declaration merging 只有{" "}
- interface 有。A & B 是交叉类型:
- 一个同时满足两边的值。
+ interface 有。A & B 是交叉类型:一个同时满足两边的值。
>
),
},
diff --git a/app/functions/viz.tsx b/app/functions/viz.tsx
index 5f7b65c..8869d03 100644
--- a/app/functions/viz.tsx
+++ b/app/functions/viz.tsx
@@ -86,9 +86,7 @@ const SIG_SEGS: SigSeg[] = [
}
zh={
<>
- 这个函数叫什么。用动词短语最好读,
- 因为调用处应该一眼看出发生了什么:makeOrder、addTopping、calcTotal。
- 开门括号一开,参数列表就开始了。
+ 这个函数叫什么。用动词短语最好读,因为调用处应该一眼看出发生了什么:makeOrder、addTopping、calcTotal。开门括号一开,参数列表就开始了。
>
}
/>
@@ -110,9 +108,7 @@ const SIG_SEGS: SigSeg[] = [
}
zh={
<>
- 冒号左边是参数名,函数体里用这个名字称呼它;
- 右边是类型,也就是调用方必须交出的形状。
- 传一个不是 MenuItem 的东西进来,这次调用就编译不过。
+ 冒号左边是参数名,函数体里用这个名字称呼它;右边是类型,也就是调用方必须交出的形状。传一个不是 MenuItem 的东西进来,这次调用就编译不过。
>
}
/>
@@ -165,10 +161,8 @@ const SIG_SEGS: SigSeg[] = [
}
zh={
<>
- 名字后面挂一个 ?,表示这个实参可以整个不写。
- 代价在函数体里:那里它的类型是{" "}
- Topping[] | undefined,用之前得先检查。
- 可选参数还必须排在所有必选参数后面 —— 原因见 §02。
+ 名字后面挂一个 ?,表示这个实参可以整个不写。代价在函数体里:那里它的类型是{" "}
+ Topping[] | undefined,用之前得先检查。可选参数还必须排在所有必选参数后面 —— 原因见 §02。
>
}
/>
@@ -191,10 +185,7 @@ const SIG_SEGS: SigSeg[] = [
}
zh={
<>
- 关门括号后面的 : Order 是这个函数承诺交出的东西。
- return 的对象少一个字段,错误就报在这里、报在定义处。
- TypeScript 本来能推断出它,
- 但在会被别人调用的函数上写出来,这份承诺才稳定 —— 见 §01 的提示框。
+ 关门括号后面的 : Order 是这个函数承诺交出的东西。return 的对象少一个字段,错误就报在这里、报在定义处。TypeScript 本来能推断出它,但在会被别人调用的函数上写出来,这份承诺才稳定 —— 见 §01 的提示框。
>
}
/>
@@ -268,9 +259,7 @@ const CC_CASES: CcCase[] = [
}
zh={
<>
- toppings 带了 ?,所以整个不写是允许的。
- 函数体里 toppings 是 undefined,
- 返回值仍然是一张完整的 Order。
+ toppings 带了 ?,所以整个不写是允许的。函数体里 toppings 是 undefined,返回值仍然是一张完整的 Order。
>
}
/>
@@ -292,8 +281,7 @@ const CC_CASES: CcCase[] = [
}
zh={
<>
- size 没有 ?,是必填的。
- 两个必选参数加一个可选参数 —— 报错里那个 2-3 的范围就是这么来的。
+ size 没有 ?,是必填的。两个必选参数加一个可选参数 —— 报错里那个 2-3 的范围就是这么来的。
>
}
/>
@@ -315,9 +303,7 @@ const CC_CASES: CcCase[] = [
}
zh={
<>
- Size 只认三个字符串:small、medium、large。
- "grande" 不在其中。字面量联合比的是整个字符串,
- 差一个字母就已经是另一个类型了。
+ Size 只认三个字符串:small、medium、large。"grande" 不在其中。字面量联合比的是整个字符串,差一个字母就已经是另一个类型了。
>
}
/>
@@ -338,9 +324,7 @@ const CC_CASES: CcCase[] = [
}
zh={
<>
- 第三个参数要的是 Topping[]:
- 一个数组,里面每一项都得是 Topping 联合里的字符串。
- 这两个值都在册。
+ 第三个参数要的是 Topping[]:一个数组,里面每一项都得是 Topping 联合里的字符串。这两个值都在册。
>
}
/>
@@ -362,8 +346,7 @@ const CC_CASES: CcCase[] = [
}
zh={
<>
- 第一个参数要的是一件商品的完整形状:id、name、price。
- 光一个商品名只是个字符串。编译器比的是形状,不是意思 ——
+ 第一个参数要的是一件商品的完整形状:id、name、price。光一个商品名只是个字符串。编译器比的是形状,不是意思 ——
04 章专门讲这件事。
>
}
@@ -388,8 +371,7 @@ const CC_CASES: CcCase[] = [
zh={
<>
就算只加一样配料,也得装进数组:
- ["boba"]。第三个参数的类型是数组,
- 而「一个字符串」和「装着一个字符串的数组」是两种类型。
+ ["boba"]。第三个参数的类型是数组,而「一个字符串」和「装着一个字符串的数组」是两种类型。
>
}
/>
@@ -535,8 +517,7 @@ export function CallCheck() {
}
zh={
<>
- 六单全审完,你就有了编译器的习惯:
- 看到一次调用,先在脑子里对一遍签名,再去运行。
+ 六单全审完,你就有了编译器的习惯:看到一次调用,先在脑子里对一遍签名,再去运行。
>
}
/>
diff --git a/app/generics/page.tsx b/app/generics/page.tsx
index 9e9da6f..e068e3f 100644
--- a/app/generics/page.tsx
+++ b/app/generics/page.tsx
@@ -538,8 +538,7 @@ export default function GenericsPage() {
),
zh: (
<>
- 先别急着说是什么类型 —— 留一个洞 <T>,
- 调用的人来填。同一个签名里的每个 T{" "}
+ 先别急着说是什么类型 —— 留一个洞 <T>,调用的人来填。同一个签名里的每个 T{" "}
都是同一个,所以返回值的类型始终和参数的类型绑在一起。
>
),
@@ -607,10 +606,8 @@ export default function GenericsPage() {
}
zh={
<>
- 一个小店的点单系统要「取订单列表的第一单」
- 「取菜单的第一项」「取杯型选项的第一个」——
- 同一个动作做三遍,面对三种不同的数组类型。
- 用现有的写法,你有三条路,三条都不体面。
+ 一个小店的点单系统要「取订单列表的第一单」「取菜单的第一项」「取杯型选项的第一个」——
+ 同一个动作做三遍,面对三种不同的数组类型。用现有的写法,你有三条路,三条都不体面。
>
}
/>
@@ -669,9 +666,7 @@ export default function GenericsPage() {
看右边这版:一份代码通吃所有数组,而且输入和输出是
连着的 —— 喂它 string[],拿回来的东西和{" "}
string 有关;喂它 Order[], 拿回来的就和{" "}
- Order 有关。
- 中间那一版丢掉的正是这条联系。下一节把一次调用放慢,
- 看它是怎么建立起来的。
+ Order 有关。中间那一版丢掉的正是这条联系。下一节把一次调用放慢,看它是怎么建立起来的。
>
}
/>
@@ -712,8 +707,7 @@ export default function GenericsPage() {
zh={
<>
函数名后面的尖括号声明了一个占位符,并给它取名 T
- 。它叫类型参数:
- 和普通参数一样是参数,只不过里面装的是类型,不是值。
+ 。它叫类型参数:和普通参数一样是参数,只不过里面装的是类型,不是值。
>
}
/>
@@ -736,8 +730,7 @@ export default function GenericsPage() {
}
zh={
<>
- 签名里每个 T 指的都是那一个占位符。
- 这就是它的承诺:数组里装的和返回的是同一种类型。
+ 签名里每个 T 指的都是那一个占位符。这就是它的承诺:数组里装的和返回的是同一种类型。
any 说不出这句话。
>
}
@@ -763,8 +756,7 @@ export default function GenericsPage() {
}
zh={
<>
- 写这个函数的人不知道 T 会是什么,也不需要知道。
- 每次调用各自带着实参过来,编译器就为这一次调用单独解出{" "}
+ 写这个函数的人不知道 T 会是什么,也不需要知道。每次调用各自带着实参过来,编译器就为这一次调用单独解出{" "}
T。
>
}
@@ -795,9 +787,7 @@ export default function GenericsPage() {
}
zh={
<>
- 编译器从第一个实参读出 T = string,
- 再拿第二个实参去对。想让两个位置各是各的类型,
- 那就声明两个占位符:{"pair(a: A, b: B)"} ——
+ 编译器从第一个实参读出 T = string,再拿第二个实参去对。想让两个位置各是各的类型,那就声明两个占位符:{"pair(a: A, b: B)"} ——
动手任务里有一题练这个。
>
}
@@ -832,8 +822,7 @@ export default function GenericsPage() {
T 取自 Type。你也可以叫它 Item、
Row,就像普通参数可以叫 x 也可以叫{" "}
count。常见的还有 K / V
- (键和值)、E(元素)。名字短,是因为这个占位符
- 常常真的什么都可能是。一旦它有明确含义,就该起个真名: 使用处写{" "}
+ (键和值)、E(元素)。名字短,是因为这个占位符常常真的什么都可能是。一旦它有明确含义,就该起个真名: 使用处写{" "}
{"Paginated 比{" "}
{"Paginated 好读。
>
@@ -879,9 +868,7 @@ export default function GenericsPage() {
<>
推断的原料只有一个来源:实参。空数组带不出元素类型, 于是{" "}
T 被推成 never,结果是{" "}
- undefined。这不是报错,但通常不是你要的。
- 在意料之外的地方看到 never,
- 就该想到去把类型实参写出来。
+ undefined。这不是报错,但通常不是你要的。在意料之外的地方看到 never,就该想到去把类型实参写出来。
>
),
}}
@@ -905,9 +892,7 @@ export default function GenericsPage() {
}
zh={
<>
- 到这里为止,「T 是什么」都是注释里的一句断言。
- 下面这个窗口里跑的是编译器本身,断言可以当场核对。
- 点四次调用里的任意一个 first:弹出来的签名里{" "}
+ 到这里为止,「T 是什么」都是注释里的一句断言。下面这个窗口里跑的是编译器本身,断言可以当场核对。点四次调用里的任意一个 first:弹出来的签名里{" "}
T 已经被换成了实际类型,价格那一行是{" "}
{"function first
,{"function first
,因为类型是你自己点名的;first([]) 显示{" "}
{"function first
- ,因为空数组带不出元素类型,剩下的只有 never。
- 这一行不报错 —— 正因为不报错,才值得练成一眼认出来。
+ ,因为空数组带不出元素类型,剩下的只有 never。这一行不报错 —— 正因为不报错,才值得练成一眼认出来。
>
}
/>
@@ -974,9 +958,7 @@ export default function GenericsPage() {
}
zh={
<>
- 空数组好歹还是个实参,编译器手里有东西。
- 更极端的情况是:类型参数只出现在返回值类型里。
- 这时没有任何实参提到它,编译器就回落到 unknown。
+ 空数组好歹还是个实参,编译器手里有东西。更极端的情况是:类型参数只出现在返回值类型里。这时没有任何实参提到它,编译器就回落到 unknown。
>
}
/>
@@ -1005,10 +987,7 @@ export default function GenericsPage() {
zh={
<>
这种写法在解析数据、请求数据的代码里很常见,但要小心:
- parseJson 承诺返回一个 T,
- 却没有任何东西检查解析出来的内容真的长成那样。
- 类型实参是调用方的一句断言,不是编译器给的保证。
- 拿到这种值之后怎么检查,是第 03 章的内容。
+ parseJson 承诺返回一个 T,却没有任何东西检查解析出来的内容真的长成那样。类型实参是调用方的一句断言,不是编译器给的保证。拿到这种值之后怎么检查,是第 03 章的内容。
>
}
/>
@@ -1028,8 +1007,7 @@ export default function GenericsPage() {
}
zh={
<>
- 经验法则:默认交给推断。
- 只有在实参给不出信息时才手写类型实参 ——
+ 经验法则:默认交给推断。只有在实参给不出信息时才手写类型实参 ——
比如空数组、没有参数的调用、类型参数只出现在返回值里 ——
或者推出来的类型不是你想要的那个。
>
@@ -1152,8 +1130,7 @@ export default function GenericsPage() {
zh: (
<>
两个占位符。T 是那个对象;K 被约束成{" "}
- keyof T,也就是这个对象的键名联合。
- 安全性正是这个约束给的:既然 K 只能是 T{" "}
+ keyof T,也就是这个对象的键名联合。安全性正是这个约束给的:既然 K 只能是 T{" "}
真有的键,索引访问 T[K]{" "}
取到的就一定是存在的类型,写错键名也会在程序运行前被拦下。
keyof 和 T[K] 的完整用法在第 07 章。
@@ -1192,9 +1169,7 @@ export default function GenericsPage() {
to parameter of type '{"{ length: number; }"}'.
getProp(menu, "topping"){" "}
- 拿到的是同一个错误码,里面列着那个对象真有的两个键名。
- 点报错可以跳到出问题的调用。然后按一下把约束去掉:
- 调用处的两处报错消失,函数体里换成了{" "}
+ 拿到的是同一个错误码,里面列着那个对象真有的两个键名。点报错可以跳到出问题的调用。然后按一下把约束去掉:调用处的两处报错消失,函数体里换成了{" "}
TS2339: Property 'length' does not exist on type
'T'.
@@ -1238,8 +1213,7 @@ export default function GenericsPage() {
这一次切换,就是整笔交易。
约束把抱怨从函数体挪到了调用处 ——
而调用处才是人能动手的地方:写 {" "}
- 这一处报错本身就是三种写法的差别,由编译器说出口:
- 只有 longest 的人可以放心写{" "}
- a.length,不用和编译器争辩;
- 传了 number 进来的人,在自己写的那一行上就被拦住。
+ a.length,不用和编译器争辩;传了 number 进来的人,在自己写的那一行上就被拦住。
>
}
/>
@@ -1306,10 +1280,8 @@ export default function GenericsPage() {
),
zh: (
<>
- 区别在于占位符什么时候定下来。
- 泛型类是每个实例定一次,所有成员共用它,所以一个{" "}
- Basket<string> 从头到尾只收 string。
- 泛型方法是每次调用定一次,所以同一个 Counter{" "}
+ 区别在于占位符什么时候定下来。泛型类是每个实例定一次,所有成员共用它,所以一个{" "}
+ Basket<string> 从头到尾只收 string。泛型方法是每次调用定一次,所以同一个 Counter{" "}
实例可以先数 number,再数 string。
>
),
@@ -1338,7 +1310,7 @@ export default function GenericsPage() {
<>
{" 限制哪些类型被允许;
{"{" "}
- 是在既没写、也推不出来时补一个类型。 两者回答的是不同的问题,
+ 是在既没写、也推不出来时补一个类型。两者回答的是不同的问题,
{' 就是同时用上。
>
),
@@ -1371,8 +1343,7 @@ export default function GenericsPage() {
的简写,而 Array 就是一个泛型接口。
Promise<Order> 表示「将来会是一个{" "}
Order」。Map<string, number>{" "}
- 有两个占位符。读懂了尖括号,
- 标准库的类型签名就变成了可以读的说明书。
+ 有两个占位符。读懂了尖括号,标准库的类型签名就变成了可以读的说明书。
>
}
/>
@@ -1395,7 +1366,7 @@ export default function GenericsPage() {
Array<Animal>, 哪怕这样就能往原数组里{" "}
push 一个非 Dog。这是关于{" "}
Array{" "}
- 的方法如何比较的一个有意为之的决定,不是泛型的通则。 第 02 章
+ 的方法如何比较的一个有意为之的决定,不是泛型的通则。第 02 章
§04 讲透了这件事。
>
}
@@ -1462,8 +1433,7 @@ export default function GenericsPage() {
和所有注解,都在编译过程中被去掉。所以程序运行时没法问{" "}
T 是什么,也写不出 new T() 或{" "}
if (T === String)。泛型函数并不知道自己的类型实参
- —— 知道的是编译器,而且是在编译之前就知道。
- 真要在运行时按类型分支,那得对值做真正的运行时检查, 那是第
+ —— 知道的是编译器,而且是在编译之前就知道。真要在运行时按类型分支,那得对值做真正的运行时检查, 那是第
03 章的内容。
>
}
@@ -1497,9 +1467,7 @@ export default function GenericsPage() {
{"function log{" "}
- 声明了 T,然后只提到它一次。
- 它没把任何东西和任何东西联系起来,也就什么都没承诺。
- 一条好用的规矩:类型参数至少要出现两次 ——
+ 声明了 T,然后只提到它一次。它没把任何东西和任何东西联系起来,也就什么都没承诺。一条好用的规矩:类型参数至少要出现两次 ——
联系两个参数,或者联系参数和返回值。只出现一次的话, 直接写{" "}
{"(x: unknown)"} 更诚实。
>
@@ -1529,11 +1497,8 @@ export default function GenericsPage() {
}
zh={
<>
- any 是放弃类型:不管进去的是什么,
- 出来的都不再受检查。泛型是保住类型:
- T 在调用那一刻被解成一个具体类型,
- 输入和输出全程受检。两者方向相反,
- 看起来像,只是因为它们都「什么类型都收」。
+ any 是放弃类型:不管进去的是什么,出来的都不再受检查。泛型是保住类型:
+ T 在调用那一刻被解成一个具体类型,输入和输出全程受检。两者方向相反,看起来像,只是因为它们都「什么类型都收」。
>
}
/>
@@ -1616,8 +1581,7 @@ export default function GenericsPage() {
),
zh: (
<>
- 类型参数是一个类型的占位符,在调用处才被填上。
- 同一个签名里的每个 T 都是同一个 ——
+ 类型参数是一个类型的占位符,在调用处才被填上。同一个签名里的每个 T 都是同一个 ——
返回值的类型就是这样和参数的类型绑在一起的。
>
),
@@ -1649,8 +1613,7 @@ export default function GenericsPage() {
),
zh: (
<>
- 推断是默认,它的原料是实参。
- 实参给不出信息时才手写类型实参 —— 空数组会得到{" "}
+ 推断是默认,它的原料是实参。实参给不出信息时才手写类型实参 —— 空数组会得到{" "}
never,只出现在返回值里的类型参数会得到{" "}
unknown。
>
@@ -1689,8 +1652,7 @@ export default function GenericsPage() {
zh: (
<>
类型参数会被擦除:编译之后没有 T{" "}
- 可查,泛型函数在运行时并不知道自己的类型实参。
- 泛型类的占位符按实例定一次,泛型方法的按调用定一次。
+ 可查,泛型函数在运行时并不知道自己的类型实参。泛型类的占位符按实例定一次,泛型方法的按调用定一次。
>
),
},
diff --git a/app/generics/viz.tsx b/app/generics/viz.tsx
index af72cd8..b72dd41 100644
--- a/app/generics/viz.tsx
+++ b/app/generics/viz.tsx
@@ -100,8 +100,7 @@ export function HoleFiller() {
zh={
<>
先只看签名。<T> 是在声明占位符, 后面两个{" "}
- T 是同一个占位符的两次使用。
- 现在还什么都没填。点「下一步」,让一次调用进场。
+ T 是同一个占位符的两次使用。现在还什么都没填。点「下一步」,让一次调用进场。
>
}
/>,
@@ -119,7 +118,7 @@ export function HoleFiller() {
<>
编译器去读实参:{run.argLit} 的类型是{" "}
{run.argType}。拿它对上声明的参数 arr: T[]
- ,得出 T = {run.fill}。 这就是类型实参推断,而它的原料只有实参。
+ ,得出 T = {run.fill}。这就是类型实参推断,而它的原料只有实参。
>
}
/>,
@@ -136,8 +135,7 @@ export function HoleFiller() {
zh={
<>
只有一个占位符,所以所有使用处同时被填上:这个签名里的每个{" "}
- T 现在都是 {run.fill}。
- 同一次调用里,不可能这头是 string、那头是{" "}
+ T 现在都是 {run.fill}。同一次调用里,不可能这头是 string、那头是{" "}
number。
>
}
@@ -155,7 +153,7 @@ export function HoleFiller() {
zh={
<>
返回值类型跟着落定:x 的类型是 {run.result}
- 。类型信息从实参一路走到了结果。 这就是它和 any 的区别 ——{" "}
+ 。类型信息从实参一路走到了结果。这就是它和 any 的区别 ——{" "}
any 把类型丢在门口。
>
}
@@ -297,8 +295,7 @@ const CANDS: Cand[] = [
}
zh={
<>
- string 本来就有 .length,
- 可以赋值给这个约束,调用通过,T = string。注意 T 并不会被削成{" "}
+ string 本来就有 .length,可以赋值给这个约束,调用通过,T = string。注意 T 并不会被削成{" "}
{"{ length: number }"} —— 它仍然是完整的{" "}
string。
>
@@ -322,8 +319,7 @@ const CANDS: Cand[] = [
}
zh={
<>
- 数组也有 .length,同样通过,T = string[]。
- 约束问的是这个类型「有什么」,不是它「叫什么」。
+ 数组也有 .length,同样通过,T = string[]。约束问的是这个类型「有什么」,不是它「叫什么」。
>
}
/>
@@ -346,8 +342,7 @@ const CANDS: Cand[] = [
zh={
<>
一个没名字、也没声明过类型的对象字面量,但它确实有{" "}
- length: number。
- 上一章那套结构化检查,正是约束在这里用的检查。
+ length: number。上一章那套结构化检查,正是约束在这里用的检查。
>
}
/>
@@ -373,8 +368,7 @@ const CANDS: Cand[] = [
}
zh={
<>
- number 身上没有 length,
- 所以你还在敲代码的时候这次调用就被拒绝了:
+ number 身上没有 length,所以你还在敲代码的时候这次调用就被拒绝了:
Argument of type 'number' is not assignable to parameter
of type {"'{ length: number; }'"}.
@@ -401,8 +395,7 @@ const CANDS: Cand[] = [
}
zh={
<>
- boolean 同样没有 length,拒绝。
- 约束是加在占位符上的一个条件:不是什么类型都能填进去。
+ boolean 同样没有 length,拒绝。约束是加在占位符上的一个条件:不是什么类型都能填进去。
>
}
/>
@@ -478,8 +471,7 @@ export function ConstraintGate() {
}
zh={
<>
- longest 要比较两个值的 .length,
- 所以它的占位符带了一个条件:填进来的类型必须有{" "}
+ longest 要比较两个值的 .length,所以它的占位符带了一个条件:填进来的类型必须有{" "}
length: number。点上面的候选值,挨个送去检查。
>
}
diff --git a/app/home-viz.tsx b/app/home-viz.tsx
index e56c60f..c97bc96 100644
--- a/app/home-viz.tsx
+++ b/app/home-viz.tsx
@@ -260,8 +260,7 @@ const TL_FRAMES: TLFrame[] = [
}
zh={
<>
- 凌晨三点:客服电话、翻日志、一行行加 console.log —— 两小时后才发现,
- 凶手是 5 个拼错的字母。同一时刻,TS 工程师在睡觉。
+ 凌晨三点:客服电话、翻日志、一行行加 console.log —— 两小时后才发现,凶手是 5 个拼错的字母。同一时刻,TS 工程师在睡觉。
>
}
/>
@@ -387,8 +386,7 @@ const E_CAPTION: Record: Config)是「请你检查」:
- 编译器检查这个值,然后把推断结果换成你声明的类型。
- 断言(as Config)是「我说了算」:
- 检查被跳过。
+ 给一个值「定类型」,其实是在对编译器说话,而这三种写法说的是三句不同的话。注解(
+ : Config)是「请你检查」:编译器检查这个值,然后把推断结果换成你声明的类型。
+ 断言(as Config)是「我说了算」:检查被跳过。
satisfies
- (TypeScript 4.9 加入)是「检查完别改我」:
- 形状照查,推断结果保留。下面直接对比。
+ (TypeScript 4.9 加入)是「检查完别改我」:形状照查,推断结果保留。下面直接对比。
theme{" "}
- 这个词 —— 编辑器下面那条显示的是编译器推断出的类型,
- 不是我们写上去的。
+ 编译器,就在你的浏览器里。同一个对象的四份稿子藏在标题栏的按钮后面,彼此只差一行。切着看,并且点一下 theme{" "}
+ 这个词 —— 编辑器下面那条显示的是编译器推断出的类型,不是我们写上去的。
>
}
/>
@@ -724,8 +716,7 @@ export default function MindsetPage() {
the types '"dark"' and
'"light"' have no overlap.
satisfies 之后,它还记得 theme{" "}
+ 这一处报错本身就是三种写法的差别,由编译器说出口:只有 satisfies 之后,它还记得 theme{" "}
是 "dark"。
>
}
@@ -783,11 +774,8 @@ export default function MindsetPage() {
}
zh={
<>
- 第四份稿子值得花一分钟。maxSugar 根本不在里面,
- 而这个文件一处报错都没有 —— 包括调用{" "}
- config.maxSugar.toFixed(1) 那一行,
- 它在运行时是一个必然发生的 TypeError。
- 现在你自己在编辑器里把那个 as Config 改成{" "}
+ 第四份稿子值得花一分钟。maxSugar 根本不在里面,而这个文件一处报错都没有 —— 包括调用{" "}
+ config.maxSugar.toFixed(1) 那一行,它在运行时是一个必然发生的 TypeError。现在你自己在编辑器里把那个 as Config 改成{" "}
: Config,编译器立刻回话:
TS2741: Property 'maxSugar' is missing in type
@@ -827,16 +815,14 @@ export default function MindsetPage() {
<>
但 as 不是万能通行证 ——
这一点值得弄准,不要靠猜。它要求两个类型仍然
- 可比较(comparable):其中一个能赋给另一个。
- 少一个字段是过关的,因为 Config{" "}
+ 可比较(comparable):其中一个能赋给另一个。少一个字段是过关的,因为 Config{" "}
可以赋给你写下的那个更小的形状。而把 maxSugar: 7{" "}
改成 maxSugar: "7" 就过不去:
TS2352: Conversion of type … may be a mistake because
neither type sufficiently overlaps with the other.
{" "}
- 所以关于 as 的老实说法比「什么都放行」
- 更窄、也更有用:
+ 所以关于 as 的老实说法比「什么都放行」更窄、也更有用:
它不会替你发现漏掉的东西,但明摆着矛盾的它仍然拒绝。
>
}
@@ -946,9 +932,7 @@ export default function MindsetPage() {
值得注解的地方有两处。函数参数 ——
参数来自外部,推断没有材料可读。对外的边界 ——
导出的函数、模块 API、共享数据 ——
- 那里的类型是给别的代码的承诺,写下来,
- 编译器才会去检查这份承诺,而不是照抄你碰巧返回了什么。
- 除此之外,交给推断。
+ 那里的类型是给别的代码的承诺,写下来,编译器才会去检查这份承诺,而不是照抄你碰巧返回了什么。除此之外,交给推断。
as 不是坏东西 ——
- 它就是为「你确实比编译器知道得多」的场合准备的。
- 判断标准只有一条:你的信息从哪来?
- 来自你亲自确认过的事实,这个断言就合理;
- 来自「另一种情况我懒得处理」,就不合理。
+ 它就是为「你确实比编译器知道得多」的场合准备的。判断标准只有一条:你的信息从哪来?
+ 来自你亲自确认过的事实,这个断言就合理;来自「另一种情况我懒得处理」,就不合理。
>
}
/>
@@ -1018,10 +1000,7 @@ export default function MindsetPage() {
zh={
<>
querySelector 返回{" "}
- Element | null。编译器没读过你的 HTML,
- 你读过 —— 这份信息差让断言变得合理。
- 但它仍然是一份承诺:哪天页面结构改了,
- 这一行就会静悄悄地变成错的。
+ Element | null。编译器没读过你的 HTML,你读过 —— 这份信息差让断言变得合理。但它仍然是一份承诺:哪天页面结构改了,这一行就会静悄悄地变成错的。
>
}
/>
@@ -1051,9 +1030,7 @@ export default function MindsetPage() {
zh={
<>
请求回来的 JSON、localStorage、用户输入 ——
- 这些数据的形状不由你的代码库决定。
- 这里的 as 不是知识,
- 而是把一个猜测写成了事实。正确做法见 §03。
+ 这些数据的形状不由你的代码库决定。这里的 as 不是知识,而是把一个猜测写成了事实。正确做法见 §03。
>
}
/>
@@ -1090,13 +1067,9 @@ export default function MindsetPage() {
zh={
<>
- 当两个类型毫无交集时,直接 as 会被拒绝,
- 于是有人写 x as unknown as T 分两步硬转。
- 它的真实含义是:
+ 当两个类型毫无交集时,直接 as 会被拒绝,于是有人写 x as unknown as T 分两步硬转。它的真实含义是:
「把编译器对这个值的全部认知丢掉,然后让我重新贴标签」
- 。在测试代码里,它有时是个合理的逃生口;
- 出现在业务代码里,通常说明类型建模本身有问题,
- 该改的是模型。
+ 。在测试代码里,它有时是个合理的逃生口;出现在业务代码里,通常说明类型建模本身有问题,该改的是模型。
- 一个类型系统是健全的(sound), - 意思是通过检查的程序不会以类型宣称不可能的方式出错。 - TypeScript 并不健全,而这是设计决定,不是缺陷 —— + 一个类型系统是健全的(sound),意思是通过检查的程序不会以类型宣称不可能的方式出错。TypeScript 并不健全,而这是设计决定,不是缺陷 —— 完全健全的系统会拒绝大量普通的 JavaScript, TypeScript 用一部分保证换取了在真实代码上可用。
- 知道具体的口子在哪很有用, - 因为正是在这些地方,「编译通过」什么也不能说明。 + 知道具体的口子在哪很有用,因为正是在这些地方,「编译通过」什么也不能说明。
> } @@ -1171,12 +1141,9 @@ export default function MindsetPage() { } zh={ <> - 这几行现在都不报错,而四种情况在运行时都可能出问题。 - 这不是不该信任 TypeScript 的理由, - 而是这一章后面几节存在的理由: + 这几行现在都不报错,而四种情况在运行时都可能出问题。这不是不该信任 TypeScript 的理由,而是这一章后面几节存在的理由: - 少用any 和 as,
- 在边界上自己检查数据,项目扛得住时打开{" "}
+ 少用 any 和 as,在边界上自己检查数据,项目扛得住时打开{" "}
noUncheckedIndexedAccess
。
@@ -1214,11 +1181,7 @@ export default function MindsetPage() {
}
zh={
<>
- 类型在编译时会被擦除。这带来一个冷酷的结论:
- 你写的每一个 interface 在线上都不存在,
- 从外面进来的数据没有任何东西替你校验。
- 一个描述接口返回值的 interface,
- 是对数据的一个声称,不是保证。逐帧看一遍。
+ 类型在编译时会被擦除。这带来一个冷酷的结论:你写的每一个 interface 在线上都不存在,从外面进来的数据没有任何东西替你校验。一个描述接口返回值的 interface,是对数据的一个声称,不是保证。逐帧看一遍。
>
}
/>
@@ -1258,15 +1221,11 @@ export default function MindsetPage() {
很多人以为类型报错会让一切停下,其实不会。默认情况下,
tsc 会报出错误,同时照样写出 JavaScript 文件
- ;想让它停下,需要开 noEmitOnError。
- 不少构建工具走得更远:它们直接删掉类型,根本不做检查,
- 于是这个类型错误在构建过程中连出现的机会都没有。
+ ;想让它停下,需要开 noEmitOnError。不少构建工具走得更远:它们直接删掉类型,根本不做检查,于是这个类型错误在构建过程中连出现的机会都没有。
- 所以那条红线是一条消息,不是一堵墙。 - 这一点值得直说,因为它解释了一个常见的困惑: - 有类型错误的代码照样能跑,也照样能发布。 - 把类型检查放进持续集成,才是把消息变成墙。 + 所以那条红线是一条消息,不是一堵墙。这一点值得直说,因为它解释了一个常见的困惑: + 有类型错误的代码照样能跑,也照样能发布。把类型检查放进持续集成,才是把消息变成墙。
> } @@ -1285,8 +1244,7 @@ export default function MindsetPage() { zh={ <> 站在边界上的那个isOrder,就是第 03 章学过的
- 类型谓词(type predicate):
- 在值这一层做检查,换来类型这一层的收窄。
+ 类型谓词(type predicate):在值这一层做检查,换来类型这一层的收窄。
>
}
/>
@@ -1312,9 +1270,7 @@ export default function MindsetPage() {
}
zh={
<>
- 注意第 10 行的 as:它只活在函数内部,
- 只为逐个读取字段服务,而且每个字段之后真的被检查了。
- 这是断言给检查打下手,不是断言替检查上岗。
+ 注意第 10 行的 as:它只活在函数内部,只为逐个读取字段服务,而且每个字段之后真的被检查了。这是断言给检查打下手,不是断言替检查上岗。
>
}
/>
@@ -1332,8 +1288,7 @@ export default function MindsetPage() {
}
zh={
<>
- 这些都是断言,读到断言就该想验一下。
- 下面这个窗口就是验它的:同一道边界的三份稿子,由真编译器判。
+ 这些都是断言,读到断言就该想验一下。下面这个窗口就是验它的:同一道边界的三份稿子,由真编译器判。
>
}
/>
@@ -1385,10 +1340,8 @@ export default function MindsetPage() {
TS18046: 'data' is of type 'unknown'.
{" "}
这不是编译器为难你,是编译器实话实说:
- JSON.parse 返回的就是字符串里当时装着的东西,
- 在它上面读 .total 是猜。切到
- 加类型谓词,报错没了 —— 不是因为你把它捂住了,
- 而是因为 isOrder 真的先看了一眼那个值。
+ JSON.parse 返回的就是字符串里当时装着的东西,在它上面读 .total 是猜。切到
+ 加类型谓词,报错没了 —— 不是因为你把它捂住了,而是因为 isOrder 真的先看了一眼那个值。
>
}
/>
@@ -1416,12 +1369,10 @@ export default function MindsetPage() {
data.totl.toFixed(2),以及连装都不装的{" "}
data.anything.at.all()。
- unknown 和 any 说的都是「我不知道这是什么」,
- 差别在下一步
+ unknown 和 any 说的都是「我不知道这是什么」,差别在下一步
:unknown 逼你去弄清楚,any{" "}
- 是你说什么它都点头。站在边界上,你选的就是这两句话之一,
- 而其中一句等于决定不再检查。
+ 是你说什么它都点头。站在边界上,你选的就是这两句话之一,而其中一句等于决定不再检查。
>
}
/>
@@ -1460,14 +1411,12 @@ export default function MindsetPage() {
<>
真实项目里形状会变大,手写的校验函数又长又容易写漏。 - zod 这类运行时校验库解决的正是这件事: - 用代码描述一次形状,校验函数和 TypeScript + zod 这类运行时校验库解决的正是这件事:用代码描述一次形状,校验函数和 TypeScript 类型都从这一份描述里生成。
本课不展开,记一句就够:
- 它们存在,是因为类型被擦除了,运行时得有人来查。
- 思路和你刚读的 isOrder 完全一样。
+ 它们存在,是因为类型被擦除了,运行时得有人来查。思路和你刚读的 isOrder 完全一样。
any」,这里说句公道话:
any 有它的正当用途 —— 迁移期的旧代码
- (第 10 章讲的过渡状态),以及真正动态的边界,
- 比如 eval 的结果、
- 形状没有任何文档的第三方回调。
- 纪律只有一条:把它锁在能跑通的最小范围里。
+ (第 10 章讲的过渡状态),以及真正动态的边界,比如 eval 的结果、形状没有任何文档的第三方回调。纪律只有一条:把它锁在能跑通的最小范围里。
>
}
/>
@@ -1537,8 +1483,7 @@ export default function MindsetPage() {
zh={
<>
any 会扩散:从 any{" "}
- 值上取出来的东西还是 any。
- 一旦它进入导出签名,项目里所有调用方也一起失去了检查。
+ 值上取出来的东西还是 any。一旦它进入导出签名,项目里所有调用方也一起失去了检查。
>
}
/>
@@ -1605,15 +1550,11 @@ export default function MindsetPage() {
<>
两者都能接收任何值,差别在于之后你能拿它做什么。
- unknown 必须先收窄才能使用,
- 编译器会逼你补上检查;any 允许任何用法,
- 所以不需要检查,也就没有检查。
+ unknown 必须先收窄才能使用,编译器会逼你补上检查;any 允许任何用法,所以不需要检查,也就没有检查。
因此拿不准类型时,第一反应是 unknown ——
- 它是类型安全的「我还不知道」。
- 只有当 unknown 确实让代码写不下去时,
- 才降级到 any,并且锁在尽可能小的作用域里。
+ 它是类型安全的「我还不知道」。只有当 unknown 确实让代码写不下去时,才降级到 any,并且锁在尽可能小的作用域里。
switch (order.status),
- 每个分支里恰好有该状态的字段,漏掉的状态由{" "}
- never 兜住。第 03 章给了你技术,
- 这一章给的是用它的理由:
+ 写完 switch (order.status),每个分支里恰好有该状态的字段,漏掉的状态由{" "}
+ never 兜住。第 03 章给了你技术,这一章给的是用它的理由:
把约定写进类型,而不是写在旁边的注释里。
>
}
@@ -1759,16 +1694,12 @@ export default function MindsetPage() {
<>
前四条心法讲的是怎么和编译器相处,这一条讲的是 - 类型系统真正的用途:它不是拼写检查器, - 而是一门描述你的问题域规则的语言。 - 「订单没付钱就不可能已送达」就是这样一条规则 —— + 类型系统真正的用途:它不是拼写检查器,而是一门描述你的问题域规则的语言。「订单没付钱就不可能已送达」就是这样一条规则 —— 写进类型之后,今后每一次破坏它的改动都编译不过。
- 注意这个联合是靠什么区分的:status,
- 一个运行时真实存在的属性。它没法靠类型的名字来区分 ——
- 因为 TypeScript 比较的是类型的形状,不是名字,
- 而名字在编译之后就没了。这套比较规则,第 04 章讲得很细。
+ 注意这个联合是靠什么区分的:status,一个运行时真实存在的属性。它没法靠类型的名字来区分 ——
+ 因为 TypeScript 比较的是类型的形状,不是名字,而名字在编译之后就没了。这套比较规则,第 04 章讲得很细。
Omit。零件全来自第 07 章:
- 映射类型遍历键,as 键重映射(TypeScript 4.1)
- 负责把不要的键去掉。
+ Omit。零件全来自第 07 章:映射类型遍历键,as 键重映射(TypeScript 4.1)负责把不要的键去掉。
>
}
/>
@@ -1828,8 +1757,7 @@ export default function MindsetPage() {
}
zh={
<>
- never 在这里又立了一功:
- 被映射成 never 的键会从结果里消失 ——
+ never 在这里又立了一功:被映射成 never 的键会从结果里消失 ——
在类型层面的代码里,never 是通用的「删除」手段。
>
}
@@ -1850,8 +1778,7 @@ export default function MindsetPage() {
zh={
<>
第二个,难一些:DeepReadonly。内置的{" "}
- Readonly 只锁最外一层,嵌套对象里面照样能改。
- 想全锁住,让映射类型调用它自己。
+ Readonly 只锁最外一层,嵌套对象里面照样能改。想全锁住,让映射类型调用它自己。
>
}
/>
@@ -1876,10 +1803,7 @@ export default function MindsetPage() {
}
zh={
<>
- 条件类型(第 07 章)+ 映射类型(第 07 章)+ 递归,
- 就得到一个新工具。严谨的版本还要处理函数和数组:
- 函数也是 object,往里递归没有意义。
- 那是 type-challenges 上 medium 难度的题,值得你自己做一遍。
+ 条件类型(第 07 章)+ 映射类型(第 07 章)+ 递归,就得到一个新工具。严谨的版本还要处理函数和数组:函数也是 object,往里递归没有意义。那是 type-challenges 上 medium 难度的题,值得你自己做一遍。
>
}
/>
@@ -1906,9 +1830,7 @@ export default function MindsetPage() {
zh={
<>
github.com/type-challenges/type-challenges
- 是社区维护的类型题库,难度从 easy 到 extreme。
- 每道题都能在 Playground 里做,自带测试用例,当场就有结果。
- 你刚写的 MyOmit 和 DeepReadonly 都是里面的原题。
+ 是社区维护的类型题库,难度从 easy 到 extreme。每道题都能在 Playground 里做,自带测试用例,当场就有结果。你刚写的 MyOmit 和 DeepReadonly 都是里面的原题。
>
}
/>
@@ -1931,9 +1853,7 @@ export default function MindsetPage() {
}
zh={
<>
- typescriptlang.org/docs 下的 Handbook 是第一手资料:
- 本课讲过的每个概念,那里都有权威版本。
- 以你现在的水平读它不再吃力 —— 这正是这门课的目的之一。
+ typescriptlang.org/docs 下的 Handbook 是第一手资料:本课讲过的每个概念,那里都有权威版本。以你现在的水平读它不再吃力 —— 这正是这门课的目的之一。
>
}
/>
@@ -1957,10 +1877,7 @@ export default function MindsetPage() {
}
zh={
<>
- typescriptlang.org/play 免注册、可分享链接、
- 能切换 TypeScript 版本、能看编译产物。
- 行为拿不准的时候别猜,贴进去看答案。
- 这个习惯比这门课里的任何一个知识点都值钱。
+ typescriptlang.org/play 免注册、可分享链接、能切换 TypeScript 版本、能看编译产物。行为拿不准的时候别猜,贴进去看答案。这个习惯比这门课里的任何一个知识点都值钱。
>
}
/>
@@ -2046,13 +1963,8 @@ export default function MindsetPage() {
zh={
<>
- 序章那天,你还在问「类型是干嘛的,JavaScript 不是跑得好好的」。
- 现在你能读懂编译器报错里的每一个词,
- 能为请求回来的数据设计校验边界,
- 能用可辨识联合让非法状态写不出来,能自己实现{" "}
- Omit,
- 还能给一个老的 JavaScript 项目排一份分步迁移计划。
- 十二章没有白走。
+ 序章那天,你还在问「类型是干嘛的,JavaScript 不是跑得好好的」。现在你能读懂编译器报错里的每一个词,能为请求回来的数据设计校验边界,能用可辨识联合让非法状态写不出来,能自己实现{" "}
+ Omit,还能给一个老的 JavaScript 项目排一份分步迁移计划。十二章没有白走。
MenuItem、
- 用可辨识联合写的 Order、一个泛型容器、
+ 用 Vite + TypeScript 起一个项目,把贯穿全书的点单系统实现出来:MenuItem、用可辨识联合写的 Order、一个泛型容器、
Partial 草稿单、边界上的{" "}
- isOrder。strict 全开,
- 再把 noUncheckedIndexedAccess 也加上。
+ isOrder。strict 全开,再把 noUncheckedIndexedAccess 也加上。
>
}
/>
@@ -2122,9 +2031,7 @@ export default function MindsetPage() {
}
zh={
<>
- type-challenges 从 easy 刷起,每天一道;做不出来就看讨论区,
- 那里有社区里很聪明的写法。刷完 easy 和 medium,
- 再读开源库的类型定义就不会发怵了。
+ type-challenges 从 easy 刷起,每天一道;做不出来就看讨论区,那里有社区里很聪明的写法。刷完 easy 和 medium,再读开源库的类型定义就不会发怵了。
>
}
/>
@@ -2150,11 +2057,8 @@ export default function MindsetPage() {
}
zh={
<>
- 打开你最常用的 JavaScript 库,
- 去 node_modules 里读它的 .d.ts;
- 再去 DefinitelyTyped 看看 @types{" "}
- 是怎么给没有类型的库补类型的。
- 先读懂别人的类型设计,才谈得上设计自己的。
+ 打开你最常用的 JavaScript 库,去 node_modules 里读它的 .d.ts;再去 DefinitelyTyped 看看 @types{" "}
+ 是怎么给没有类型的库补类型的。先读懂别人的类型设计,才谈得上设计自己的。
>
}
/>
@@ -2203,21 +2107,16 @@ export default function MindsetPage() {
<>
最后说一句。这门课教的不是语法,是一种看待程序的方式: - 每个值都有形状,而形状可以被描述、被检查、被推导。 - 你脑子里那些默契 ——「这里不会是 null」 - 「这个状态下一定有那个字段」—— + 每个值都有形状,而形状可以被描述、被检查、被推导。你脑子里那些默契 ——「这里不会是 null」「这个状态下一定有那个字段」—— 统统可以写下来,交给一个不会累、也不会忘的审查员。
- 它不会替你抓住所有问题。类型会被擦除, - 类型系统并不完全健全,外部进来的数据仍然归你负责。 - 但它能抓住的那些,会抓得很早 —— + 它不会替你抓住所有问题。类型会被擦除,类型系统并不完全健全,外部进来的数据仍然归你负责。但它能抓住的那些,会抓得很早 —— 早到你还在看着造成问题的那段代码。
- 往后你写任何语言,都会先问一句「这里的约定是什么」。 - 这个习惯,才是这门课真正教给你的东西。 + 往后你写任何语言,都会先问一句「这里的约定是什么」。这个习惯,才是这门课真正教给你的东西。
> @@ -2242,8 +2141,7 @@ export default function MindsetPage() { ), zh: ( <> - 类型是写下来的约定:写下来的,编译器替你守; - 只记在脑子里的,只能靠运气。 + 类型是写下来的约定:写下来的,编译器替你守;只记在脑子里的,只能靠运气。 > ), }, @@ -2259,8 +2157,7 @@ export default function MindsetPage() { zh: ( <> 三种写法:注解检查但拓宽,as 既不检查也不保留,
- satisfies 两样都做。需要精确保留字面量时,
- 再加 as const。
+ satisfies 两样都做。需要精确保留字面量时,再加 as const。
>
),
},
@@ -2276,9 +2173,7 @@ export default function MindsetPage() {
),
zh: (
<>
- 类型在编译期被擦除,所以运行时没有任何类型检查。
- 外部数据一律用 unknown 接收,
- 再用你自己写的检查收窄 ——
+ 类型在编译期被擦除,所以运行时没有任何类型检查。外部数据一律用 unknown 接收,再用你自己写的检查收窄 ——
让错误发生在边界,而不是三个文件之外。
>
),
@@ -2294,8 +2189,7 @@ export default function MindsetPage() {
),
zh: (
<>
- 类型系统是刻意不完全健全的:any、断言、
- 数组协变、未开检查的下标访问,都是真实存在的口子 ——
+ 类型系统是刻意不完全健全的:any、断言、数组协变、未开检查的下标访问,都是真实存在的口子 ——
「编译通过」不等于「代码正确」。
>
),
@@ -2311,9 +2205,7 @@ export default function MindsetPage() {
),
zh: (
<>
- any 在迁移期和真正动态的边界上是正当的。
- 纪律是控制范围:内部可以乱,导出签名必须精确。
- 先用 unknown。
+ any 在迁移期和真正动态的边界上是正当的。纪律是控制范围:内部可以乱,导出签名必须精确。先用 unknown。
>
),
},
@@ -2328,9 +2220,7 @@ export default function MindsetPage() {
),
zh: (
<>
- 类型最有价值的用途是建模:可辨识联合让非法状态写不出来,
- 约定就不再只是一句注释。拿不准的时候,
- 去 Playground 验证,别猜。
+ 类型最有价值的用途是建模:可辨识联合让非法状态写不出来,约定就不再只是一句注释。拿不准的时候,去 Playground 验证,别猜。
>
),
},
diff --git a/app/mindset/viz.tsx b/app/mindset/viz.tsx
index c971f81..d158ed6 100644
--- a/app/mindset/viz.tsx
+++ b/app/mindset/viz.tsx
@@ -188,8 +188,7 @@ const TRIO_TESTS: {
),
zh: (
<>
- 断言只要求两个类型有足够的重叠,能互相当成对方。
- 多余属性检查完全不做,拼错的字段就这样带着 Config{" "}
+ 断言只要求两个类型有足够的重叠,能互相当成对方。多余属性检查完全不做,拼错的字段就这样带着 Config{" "}
的名分通过了。
>
),
@@ -265,10 +264,8 @@ const TRIO_TESTS: {
),
zh: (
<>
- Config 可以赋给你写的这个更小的形状,
- 两个类型有重叠,断言就通过了。运行时{" "}
- config.maxSugar 是 undefined,
- 拿它做算术会得到 NaN。
+ Config 可以赋给你写的这个更小的形状,两个类型有重叠,断言就通过了。运行时{" "}
+ config.maxSugar 是 undefined,拿它做算术会得到 NaN。
>
),
},
@@ -320,9 +317,7 @@ const TRIO_TESTS: {
),
zh: (
<>
- 注解把推断结果换成了你声明的类型。
- 编译器从此只知道「是两者之一」,不知道具体是哪一个。
- 写 {'if (config.theme === "light")'}{" "}
+ 注解把推断结果换成了你声明的类型。编译器从此只知道「是两者之一」,不知道具体是哪一个。写 {'if (config.theme === "light")'}{" "}
它仍然认为有可能成立。
>
),
@@ -341,8 +336,7 @@ const TRIO_TESTS: {
),
zh: (
<>
- 断言同样把字面量信息换成了 Config。
- 检查没做,精确的推断也丢了 —— 两头都没落着。
+ 断言同样把字面量信息换成了 Config。检查没做,精确的推断也丢了 —— 两头都没落着。
>
),
},
@@ -363,8 +357,7 @@ const TRIO_TESTS: {
zh: (
<>
检查照做,推断不动 —— config.theme 依然是字面量类型{" "}
- "dark"。「既要校验又要精确推断」,
- 正是 TypeScript 4.9 加入 satisfies 的原因。
+ "dark"。「既要校验又要精确推断」,正是 TypeScript 4.9 加入 satisfies 的原因。
>
),
},
@@ -451,7 +444,7 @@ export function TrioLab() {
}
zh={
<>
- 总结:注解 检查 ✓ 推断 ✕ · as 检查 ✕ 推断 ✕ ·
+ 总结:注解检查 ✓ 推断 ✕ · as 检查 ✕ 推断 ✕ ·
satisfies 检查 ✓ 推断 ✓ —— 两样都想要,用 satisfies。
>
}
@@ -535,9 +528,7 @@ const EF_FRAMES: FlowFrame[] = [
}
zh={
<>
- 一次请求带回一段 JSON。你写下 as Order,
- 编译器不再检查这个值,直接接受。
- 从这里开始,在编译器眼里它的类型就是 Order。
+ 一次请求带回一段 JSON。你写下 as Order,编译器不再检查这个值,直接接受。从这里开始,在编译器眼里它的类型就是 Order。
>
}
/>
@@ -567,9 +558,7 @@ const EF_FRAMES: FlowFrame[] = [
}
zh={
<>
- 编译产物是普通 JavaScript。Order 这个名字在运行时并不存在,
- 门口也没有任何检查。这就是类型擦除:
- 编译器能检查你写的代码,检查不了别人发来的数据。
+ 编译产物是普通 JavaScript。Order 这个名字在运行时并不存在,门口也没有任何检查。这就是类型擦除:编译器能检查你写的代码,检查不了别人发来的数据。
>
}
/>
@@ -607,13 +596,11 @@ const EF_FRAMES: FlowFrame[] = [
}
zh={
<>
- 某天后端把 total 改名成 amount。
- JSON 照样进来,
+ 某天后端把 total 改名成 amount。JSON 照样进来,
order.total 变成 undefined
- ,一路传过三个文件,直到报表页才出错。
- 出错的地方离原因越远,排查越费时间。
+ ,一路传过三个文件,直到报表页才出错。出错的地方离原因越远,排查越费时间。
>
}
/>
@@ -642,9 +629,7 @@ const EF_FRAMES: FlowFrame[] = [
}
zh={
<>
- 换一种做法:外部数据先标成 unknown。
- 这样编译器会拒绝一切用法,直到你把它收窄,
- 于是你在边界上写一个检查函数 ——
+ 换一种做法:外部数据先标成 unknown。这样编译器会拒绝一切用法,直到你把它收窄,于是你在边界上写一个检查函数 ——
这个检查就是类型系统替你做不了的那部分。
>
}
@@ -682,10 +667,8 @@ const EF_FRAMES: FlowFrame[] = [
}
zh={
<>
- 坏数据再来一次,isOrder 在门口拦下,
- 一行日志就能说清问题。错误仍然会发生,只是
- 发生在边界,而不是三个文件之外。
- 「用 unknown 接收,再用检查收窄」的价值就在这里。
+ 坏数据再来一次,isOrder 在门口拦下,一行日志就能说清问题。错误仍然会发生,只是
+ 发生在边界,而不是三个文件之外。「用 unknown 接收,再用检查收窄」的价值就在这里。
>
}
/>
diff --git a/app/modules/page.tsx b/app/modules/page.tsx
index b1fe1e5..c100685 100644
--- a/app/modules/page.tsx
+++ b/app/modules/page.tsx
@@ -655,11 +655,8 @@ export default function ModulesPage() {
),
zh: (
<>
- 模块把自己的顶层名字留在自己手里;
- 声明文件描述一个模块导出了什么,而不包含任何代码。
- 这一章讲这几件事:什么让一个文件成为模块、一行 import
- 到底带着什么,以及当一个 JavaScript 库自己没有类型时,
- 它的类型从哪里来。
+ 模块把自己的顶层名字留在自己手里;声明文件描述一个模块导出了什么,而不包含任何代码。这一章讲这几件事:什么让一个文件成为模块、一行 import
+ 到底带着什么,以及当一个 JavaScript 库自己没有类型时,它的类型从哪里来。
>
),
}}
@@ -717,10 +714,8 @@ export default function ModulesPage() {
zh={
<>
先说其他一切都建立在上面的那条规则。一个文件只有在顶层写了{" "}
- import 或 export 时,才是模块;
- 两者都没有的文件是脚本,它的顶层声明会进入
- 全局作用域 —— 而程序里所有脚本共用这一个作用域。
- 这就是为什么两个从没打算共享名字的文件会撞车。
+ import 或 export 时,才是模块;两者都没有的文件是脚本,它的顶层声明会进入
+ 全局作用域 —— 而程序里所有脚本共用这一个作用域。这就是为什么两个从没打算共享名字的文件会撞车。
>
}
/>
@@ -745,8 +740,7 @@ export default function ModulesPage() {
}
zh={
<>
- export {"{}"} 什么都不导出,
- 它唯一的作用就是让这个文件获得模块作用域。
+ export {"{}"} 什么都不导出,它唯一的作用就是让这个文件获得模块作用域。
§04 还会再见到它 —— declare global{" "}
需要这个前提。
>
@@ -785,15 +779,12 @@ export default function ModulesPage() {
tsconfig.json 里的 module{" "}
选的是输出格式:esnext、
- commonjs 之类。
- 它不决定某个文件是不是模块 ——
+ commonjs 之类。它不决定某个文件是不是模块 ——
只有文件自己能决定,靠顶层的 import 或 export。
- 这一点在 .d.ts 里最要紧,
- 因为那种文件很容易两者都没有。同一段{" "}
- declare module 放在哪种文件里,
- 含义完全不同。§04 会把两种都演一遍。
+ 这一点在 .d.ts 里最要紧,因为那种文件很容易两者都没有。同一段{" "}
+ declare module 放在哪种文件里,含义完全不同。§04 会把两种都演一遍。
import 和 export,
- 它的导入会被提升 ——
+ import 和 export,它的导入会被提升 ——
所有被导入的模块都在当前文件第一条语句之前加载并执行完 ——
而且导入清单不需要运行任何代码就能读出来。CommonJS
- (CJS)用 require,那是一次普通的函数调用,
- 写在哪里就在哪里执行。
+ (CJS)用 require,那是一次普通的函数调用,写在哪里就在哪里执行。
>
}
/>
@@ -874,15 +863,11 @@ export default function ModulesPage() {
zh={
<>
- ESM 的 import / export 声明必须写在顶层,名字也是固定的, - 所以打包器不运行程序就能算出哪些导出被用到, - 再把没用到的导出从产物里去掉。这种去除叫{" "} + ESM 的 import / export 声明必须写在顶层,名字也是固定的,所以打包器不运行程序就能算出哪些导出被用到,再把没用到的导出从产物里去掉。这种去除叫{" "} tree shaking。
- require 给不了这个保证:
- 它可以写在 if 里面,可以接一个算出来的路径,
- 每次还可能返回不同的对象。所以打包器通常只能把整个模块留下。
+ require 给不了这个保证:它可以写在 if 里面,可以接一个算出来的路径,每次还可能返回不同的对象。所以打包器通常只能把整个模块留下。
export default x 在 ES 模块上创建一个名叫{" "}
default 的具名导出;
module.exports = x{" "}
- 则是把 CommonJS 模块的整个导出对象换掉。
- 所以一个把函数赋给 module.exports 的 CommonJS
+ 则是把 CommonJS 模块的整个导出对象换掉。所以一个把函数赋给 module.exports 的 CommonJS
模块,并没有一个 default 属性给 ESM
的默认导入去读。
- esModuleInterop 就是搭在这两者之间的桥:
- 它让编译器产出一个小助手 __importDefault,
- 把非 ESM 的导出包成 {"{ default: … }"},
- 默认导入这才有东西可拿。它同时也改了类型规则 ——
+ esModuleInterop 就是搭在这两者之间的桥:它让编译器产出一个小助手 __importDefault,把非 ESM 的导出包成 {"{ default: … }"},默认导入这才有东西可拿。它同时也改了类型规则 ——
不开这个开关,那句默认导入会直接被拒绝。
import 会怎么样?
- 把下面的面板从「编译前」切到「编译后」看看。
+ import 会怎么样?把下面的面板从「编译前」切到「编译后」看看。
>
}
/>
@@ -1039,9 +1019,7 @@ export default function ModulesPage() {
<>
tsc 看得到整个项目,所以它自己就能算出{" "}
- Order 只是类型、这行导入可以删。
- 而 Vite、esbuild、SWC 一次只转译一个文件,
- 它们分不清 Order 是值还是类型 ——
+ Order 只是类型、这行导入可以删。而 Vite、esbuild、SWC 一次只转译一个文件,它们分不清 Order 是值还是类型 ——
删错一行导入,程序会在运行时坏掉。
@@ -1050,15 +1028,12 @@ export default function ModulesPage() {
语句会被原样输出,导入类型时不写 type 会报{" "}
ts(1484),"'Order' is a type and
must be imported using a type-only import when
- 'verbatimModuleSyntax' is enabled.";
- 转手导出类型时不写 export type 则报对应的{" "}
+ 'verbatimModuleSyntax' is enabled.";转手导出类型时不写 export type 则报对应的{" "}
ts(1205)。
还有第二个理由,和工具链无关:.js{" "}
- 模块被加载时可以执行代码。
- 一行纯类型导入如果活到了产物里,
- 就会在运行时白白加载那个模块一次。
+ 模块被加载时可以执行代码。一行纯类型导入如果活到了产物里,就会在运行时白白加载那个模块一次。
type。
- 编辑器的自动导入现在默认就是这么做的。
+ 一个好习惯:某个名字只出现在类型位置上,导入时就给它标上 type。编辑器的自动导入现在默认就是这么做的。
>
}
/>
@@ -1108,13 +1081,9 @@ export default function ModulesPage() {
}
zh={
<>
- 下面这个窗口里跑的是真编译器,它手上只有一个文件,
- 所以没有别的模块可以 import。
- 同一条分界线在导出这一侧照样看得见:四个声明,其中两个是类型。
- 切到编译产物 JS —— TAX 和{" "}
+ 下面这个窗口里跑的是真编译器,它手上只有一个文件,所以没有别的模块可以 import。同一条分界线在导出这一侧照样看得见:四个声明,其中两个是类型。切到编译产物 JS —— TAX 和{" "}
receiptLine 在,Size、
- Order 和整行 export type 都不在。
- 产物就是浏览器要跑的东西,它压根不知道那两个类型存在过。
+ Order 和整行 export type 都不在。产物就是浏览器要跑的东西,它压根不知道那两个类型存在过。
>
}
/>
@@ -1154,13 +1123,10 @@ export default function ModulesPage() {
zh={
<>
现在换上把 type 去掉。把 Order 和{" "}
- Size 当值导出,会得到两条 ts(1205)。
- 有个细节值得说清:这个实验室一次只编译一个文件 ——
- 和打包器一样 —— 所以 isolatedModules 一直开着,
- 报错里点名的就是它。把 verbatimModuleSyntax{" "}
+ Size 当值导出,会得到两条 ts(1205)。有个细节值得说清:这个实验室一次只编译一个文件 ——
+ 和打包器一样 —— 所以 isolatedModules 一直开着,报错里点名的就是它。把 verbatimModuleSyntax{" "}
打开,同一条报错只是改口点了另一个的名字。两个选项,同一条规则
- —— 类型必须以类型的身份导出,
- 因为负责删掉这一行的工具,手上只有这一个文件可看。
+ —— 类型必须以类型的身份导出,因为负责删掉这一行的工具,手上只有这一个文件可看。
>
}
/>
@@ -1195,16 +1161,11 @@ export default function ModulesPage() {
zh={
<>
- 两个 ES 模块可以互相导入,规范允许,
- 类型检查也不会报任何东西。仍然可能出问题的是顺序:
- 模块是一个接一个执行的,
- 如果一方在另一方还没走到初始化那一行时就去读它的绑定,
- 这次读取会抛 ReferenceError。
+ 两个 ES 模块可以互相导入,规范允许,类型检查也不会报任何东西。仍然可能出问题的是顺序:模块是一个接一个执行的,如果一方在另一方还没走到初始化那一行时就去读它的绑定,这次读取会抛 ReferenceError。
晚一点再读没问题 —— - 比如放在两个模块都执行完之后才被调用的函数里。 - 实际的解法是把共享的声明挪到第三个模块,两边都从那里导入。 + 比如放在两个模块都执行完之后才被调用的函数里。实际的解法是把共享的声明挪到第三个模块,两边都从那里导入。
> } @@ -1247,9 +1208,7 @@ export default function ModulesPage() { } zh={ <> - 声明文件(.d.ts)描述一个模块对外的形状:
- 每个导出,连带它的类型,除此之外什么都没有。
- 这种文件不一定要手写 —— 你让编译器生成,它就会从源码里生成:
+ 声明文件(.d.ts)描述一个模块对外的形状:每个导出,连带它的类型,除此之外什么都没有。这种文件不一定要手写 —— 你让编译器生成,它就会从源码里生成:
>
}
/>
@@ -1289,8 +1248,7 @@ export default function ModulesPage() {
}
zh={
<>
- 上面那一对是事先摆好的,下面这个不是:
- 一段奶茶店 SDK 的切片,底下是真编译器。
+ 上面那一对是事先摆好的,下面这个不是:一段奶茶店 SDK 的切片,底下是真编译器。
编译产物 JS 页签里是去掉类型的实现,
声明文件 .d.ts 页签里是去掉实现的类型 ——
同一个文件的两半,哪一半都不用手写。
@@ -1334,15 +1292,10 @@ export default function ModulesPage() {
}
zh={
<>
- 那份 .d.ts 里有四处值得回头看。
- 没有人给 priceOf 写过返回类型,
- 声明里却写着 : number ——
+ 那份 .d.ts 里有四处值得回头看。没有人给 priceOf 写过返回类型,声明里却写着 : number ——
编译器自己算出来,然后替你写给所有人看。
- find 出来是 MenuItem | undefined,
- 因为 Array.prototype.find 可能空手而归。
- private items 只列了名字、没有类型:
- 它在不在会影响类型兼容(第 08 章 §06),
- 它是什么类型则不关外人的事。而 EXTRA{" "}
+ find 出来是 MenuItem | undefined,因为 Array.prototype.find 可能空手而归。
+ private items 只列了名字、没有类型:它在不在会影响类型兼容(第 08 章 §06),它是什么类型则不关外人的事。而 EXTRA{" "}
整个不见了 —— 它从来没被导出过。
>
}
@@ -1363,11 +1316,9 @@ export default function ModulesPage() {
}
zh={
<>
- 换上撤掉 priceOf 的导出,看它去了哪里。
- 这个函数还在编译产物 JS 里 —— tsc{" "}
+ 换上撤掉 priceOf 的导出,看它去了哪里。这个函数还在编译产物 JS 里 —— tsc{" "}
会把你写的每一行都输出去;它从 .d.ts 里消失了 ——
- 那份文件只描述这个模块对外提供什么。
- 一次改动,就说完了声明文件的全部职责。
+ 那份文件只描述这个模块对外提供什么。一次改动,就说完了声明文件的全部职责。
>
}
/>
@@ -1403,8 +1354,7 @@ export default function ModulesPage() {
<>
.d.ts 里的一切都是环境声明(ambient
- declaration):关于「别处存在某样东西」的陈述。
- 在里面写一个函数体,你会得到{" "}
+ declaration):关于「别处存在某样东西」的陈述。在里面写一个函数体,你会得到{" "}
error TS1183: An implementation cannot be declared in ambient
contexts.
@@ -1458,9 +1408,7 @@ export default function ModulesPage() {
选项决定加载哪几本。
- 在编辑器里按住 Ctrl 或 Cmd 点一下 fetch,
- 你会跳进 lib.dom.d.ts。
- 亲眼看一次,「内置」就不再像是什么特殊情况了。
+ 在编辑器里按住 Ctrl 或 Cmd 点一下 fetch,你会跳进 lib.dom.d.ts。亲眼看一次,「内置」就不再像是什么特殊情况了。
declare 这个词:实现不在这里,
- 但运行时它会存在。§04 专门讲这个词。
+ 注意 declare 这个词:实现不在这里,但运行时它会存在。§04 专门讲这个词。
>
}
/>
@@ -1528,8 +1475,7 @@ export default function ModulesPage() {
}
zh={
<>
- 用 TypeScript 写的库,编译时开 --declaration,
- 再用 package.json 的 types{" "}
+ 用 TypeScript 写的库,编译时开 --declaration,再用 package.json 的 types{" "}
字段(或 exports 里的 types{" "}
条件)指向产物。装完就有类型。
>
@@ -1552,10 +1498,8 @@ export default function ModulesPage() {
}
zh={
<>
- 一个社区仓库,为自己不带类型的库写声明,
- 发布成 @types/* 包:
- npm i -D @types/lodash。
- 几乎所有老牌 JavaScript 库都被收录了。
+ 一个社区仓库,为自己不带类型的库写声明,发布成 @types/* 包:
+ npm i -D @types/lodash。几乎所有老牌 JavaScript 库都被收录了。
>
}
/>
@@ -1632,8 +1576,7 @@ export default function ModulesPage() {
<>
两种写法在真实项目里都常见。顶层的 types{" "}
字段是较早的写法;exports 里的{" "}
- types 条件由较新的解析模式读取,
- 而且可以按子路径分别指定 ——
+ types 条件由较新的解析模式读取,而且可以按子路径分别指定 ——
所以一个包能为它的 ESM 入口和 CommonJS
入口提供不同的类型。
>
@@ -1703,13 +1646,11 @@ export default function ModulesPage() {
zh={
<>
- DefinitelyTyped 里的声明是志愿者写的,可能落后于库本体。
- 你装了新的大版本 lodash,而 @types/lodash{" "}
+ DefinitelyTyped 里的声明是志愿者写的,可能落后于库本体。你装了新的大版本 lodash,而 @types/lodash{" "}
还停在旧版,类型描述的就会是库已经不再有的行为。
- 约定是 @types 包的 major 和 minor 版本跟随库本体。 - 类型看起来不对的时候,先比这两个版本号。 + 约定是 @types 包的 major 和 minor 版本跟随库本体。类型看起来不对的时候,先比这两个版本号。
> } @@ -1760,15 +1701,11 @@ export default function ModulesPage() {
找到声明文件这件事叫模块解析,规则由{" "}
moduleResolution 选择。node10{" "}
- 是旧的 Node 算法:沿着 node_modules 往上找,
- 读 main 和 types,并且
+ 是旧的 Node 算法:沿着 node_modules 往上找,读 main 和 types,并且
忽略 exports 字段。node16{" "}
- 和 nodenext 按现代 Node 的真实行为来:
- 认 exports,而且在 ESM 文件里,
- 相对导入必须写文件扩展名(不写会报 ts(2835))。
+ 和 nodenext 按现代 Node 的真实行为来:认 exports,而且在 ESM 文件里,相对导入必须写文件扩展名(不写会报 ts(2835))。
bundler 对应打包器的行为:认{" "}
- exports,但允许省略扩展名。
- 这几种没有哪一种放到哪里都对 ——
+ exports,但允许省略扩展名。这几种没有哪一种放到哪里都对 ——
正确的那一个,是和真正加载你代码的那一方对得上的那个。
@@ -1776,11 +1713,8 @@ export default function ModulesPage() {
tsc 不会改写模块标识,所以{" "}
import {"{ TAX }"} from "@/tax"{" "}
在产物里还是这么写着,Node 于是报{" "}
- Cannot find package '@/tax'。
- Next.js、Vite、webpack 会读这些 paths,
- 或者提供等价的别名选项 ——
- 这就是为什么别名在应用里好用,
- 却在你第一次用纯 node 跑产物时失败。
+ Cannot find package '@/tax'。Next.js、Vite、webpack 会读这些 paths,或者提供等价的别名选项 ——
+ 这就是为什么别名在应用里好用,却在你第一次用纯 node 跑产物时失败。
bundler,因为代码是 Next.js 加载的。
- 一个要发布到 npm 的库通常想要 nodenext,
- 这样它产出的声明才和 Node 实际的处理方式对得上。
+ 这个项目用 bundler,因为代码是 Next.js 加载的。一个要发布到 npm 的库通常想要 nodenext,这样它产出的声明才和 Node 实际的处理方式对得上。
>
}
/>
@@ -1844,15 +1776,12 @@ export default function ModulesPage() {
<>
skipLibCheck: true 跳过的是{" "}
- .d.ts 文件内部的类型检查,
- 包括它们之间的互相检查。
- 你自己代码对这些库的调用,照样完整检查。
+ .d.ts 文件内部的类型检查,包括它们之间的互相检查。你自己代码对这些库的调用,照样完整检查。
换来的是更快的编译,以及躲开你根本改不了的报错 ——
比如两个 @types{" "}
- 包用不同的类型声明了同一个全局变量。
- 代价是声明文件内部真有错时不会被报出来。
+ 包用不同的类型声明了同一个全局变量。代价是声明文件内部真有错时不会被报出来。
<script>{" "}
+ 有些东西运行时确实存在,但编译器看不见:构建工具注入的全局常量、由 <script>{" "}
标签加载进来的函数、没有类型的 npm 包。declare{" "}
就是你陈述它们类型的方式:它存在,形状是这样,请采信我。
>
@@ -1916,8 +1844,7 @@ export default function ModulesPage() {
}
zh={
<>
- 最常见的真实需求:有东西被挂到了 window 上,
- 你希望它有类型。在模块内部,这需要 declare global。
+ 最常见的真实需求:有东西被挂到了 window 上,你希望它有类型。在模块内部,这需要 declare global。
>
}
/>
@@ -1948,9 +1875,7 @@ export default function ModulesPage() {
zh={
<>
为什么要写 export {"{}"}?
- declare global 只允许出现在模块里,
- 而顶层没有 import / export 的文件是脚本。
- 写在脚本里会得到{" "}
+ declare global 只允许出现在模块里,而顶层没有 import / export 的文件是脚本。写在脚本里会得到{" "}
error TS2669: Augmentations for the global scope can only be
directly nested in external modules or ambient module
@@ -2006,21 +1931,16 @@ export default function ModulesPage() {
写在脚本文件里(顶层没有 import / export)的{" "}
declare module "x"{" "}
是环境模块声明,含义是:模块{" "}
- "x" 存在,类型是这些。
- 用于自己没有类型的包。
+ "x" 存在,类型是这些。用于自己没有类型的包。
- 同样一段写在模块文件里,就是模块扩充,
- 含义是:模块 "x"{" "}
- 已经有类型了,把这些追加进去。用于扩展一个有类型的库,
- 比如给某个 theme 接口加一个字段。
+ 同样一段写在模块文件里,就是模块扩充,含义是:模块 "x"{" "}
+ 已经有类型了,把这些追加进去。用于扩展一个有类型的库,比如给某个 theme 接口加一个字段。
- 两者搞反会得到一个令人困惑的报错。
- 某个文件里的 declare module{" "}
+ 两者搞反会得到一个令人困惑的报错。某个文件里的 declare module{" "}
本意是描述一个没有类型的包,你却给它加了{" "}
- export {"{}"},
- 编译器就会把它当成扩充,而那里没有东西可扩充:
+ export {"{}"},编译器就会把它当成扩充,而那里没有东西可扩充:
error TS2665: Invalid module name in augmentation. Module
'boba-sdk' resolves to an untyped module …, which
@@ -2075,9 +1995,7 @@ export default function ModulesPage() {
gtag is not defined 失败。
- 类型检查通过、一运行就失败时,查三件事:
- 脚本真的加载了吗?那个包真的导出了这个名字吗?
- 名字拼对了吗?
+ 类型检查通过、一运行就失败时,查三件事:脚本真的加载了吗?那个包真的导出了这个名字吗?名字拼对了吗?
>
}
@@ -2114,13 +2032,10 @@ export default function ModulesPage() {
在较早的声明文件里你会看到 declare namespace。
namespace 早于 ES 模块 ——
- 在 JavaScript 还没有自己的模块之前,
- 它是 TypeScript 自己的名字分组方式。它现在依然有效,
- 而且描述「把所有东西挂在一个全局对象上」的库时依然最自然。
+ 在 JavaScript 还没有自己的模块之前,它是 TypeScript 自己的名字分组方式。它现在依然有效,而且描述「把所有东西挂在一个全局对象上」的库时依然最自然。
- 新写的代码则推荐用模块。
- 模块本身就给了你一个私有作用域和一份明确的导出清单 ——
+ 新写的代码则推荐用模块。模块本身就给了你一个私有作用域和一份明确的导出清单 ——
那正是 namespace 当年要解决的事。
>
@@ -2171,9 +2086,7 @@ export default function ModulesPage() {
}
zh={
<>
- 设定:项目要接一个(假想的)老 JavaScript 包 boba-sdk,
- 它没有类型,@types/boba-sdk 也不存在。
- 第一次 import 就失败:
+ 设定:项目要接一个(假想的)老 JavaScript 包 boba-sdk,它没有类型,@types/boba-sdk 也不存在。第一次 import 就失败:
>
}
/>
@@ -2199,8 +2112,7 @@ export default function ModulesPage() {
zh={
<>
第一步:先让它能编译。建一个{" "}
- types/boba-sdk.d.ts。放在哪里都行,
- 只要在 tsconfig.json 的 include{" "}
+ types/boba-sdk.d.ts。放在哪里都行,只要在 tsconfig.json 的 include{" "}
范围内。
>
}
@@ -2227,11 +2139,8 @@ export default function ModulesPage() {
}
zh={
<>
- 这种简写只声明模块存在,不说它的内容,
- 所以从它导入的一切都是 any。
- 让这个文件保持脚本身份:不要写顶层的{" "}
- export,否则这段会变成模块扩充,
- 报 ts(2665)。
+ 这种简写只声明模块存在,不说它的内容,所以从它导入的一切都是 any。让这个文件保持脚本身份:不要写顶层的{" "}
+ export,否则这段会变成模块扩充,报 ts(2665)。
>
}
/>
@@ -2277,8 +2186,7 @@ export default function ModulesPage() {
<>
从这一步开始,fetchMenu{" "}
有了真实的返回类型,给 order{" "}
- 传错杯型会报错 —— 这就是「不受检查的导入」和
- 「受检查的导入」的分界线。
+ 传错杯型会报错 —— 这就是「不受检查的导入」和「受检查的导入」的分界线。
>
}
/>
@@ -2297,8 +2205,7 @@ export default function ModulesPage() {
zh={
<>
第三步:补齐其余部分。boba-sdk
- 的文档里还提到一个默认导出的客户端和一个事件回调,
- 一并补上:
+ 的文档里还提到一个默认导出的客户端和一个事件回调,一并补上:
>
}
/>
@@ -2342,8 +2249,7 @@ export default function ModulesPage() {
如果你真写出了一份完整可靠的,把它提给 DefinitelyTyped。
- @types/boba-sdk 一发布,
- 下一个用这个库的人就完全不用再做这些事了。
+ @types/boba-sdk 一发布,下一个用这个库的人就完全不用再做这些事了。
>
}
@@ -2462,8 +2368,7 @@ export default function ModulesPage() {
写在哪里就在哪里执行。export default 和{" "}
module.exports = 是两回事,
esModuleInterop 会产出{" "}
- __importDefault 包装,
- 让默认导入能读到 CommonJS 的导出。
+ __importDefault 包装,让默认导入能读到 CommonJS 的导出。
>
),
},
@@ -2480,10 +2385,8 @@ export default function ModulesPage() {
zh: (
<>
import type 和 export type{" "}
- 把一行标为「只有类型」,于是整行被删除,
- 运行时不会加载任何模块。verbatimModuleSyntax{" "}
- 把这个标注变成必需:导入报 ts(1484),
- 转手导出报 ts(1205)。
+ 把一行标为「只有类型」,于是整行被删除,运行时不会加载任何模块。verbatimModuleSyntax{" "}
+ 把这个标注变成必需:导入报 ts(1484),转手导出报 ts(1205)。
>
),
},
@@ -2498,9 +2401,7 @@ export default function ModulesPage() {
),
zh: (
<>
- .d.ts 描述导出,不产出任何东西,
- 也放不了实现:ts(1183)。
- 它同样不能替代模块本身 —— 有声明而没有对应的{" "}
+ .d.ts 描述导出,不产出任何东西,也放不了实现:ts(1183)。它同样不能替代模块本身 —— 有声明而没有对应的{" "}
.js,类型检查会通过,运行时会失败。
>
),
@@ -2542,13 +2443,10 @@ export default function ModulesPage() {
),
zh: (
<>
- declare 只陈述类型、不产出代码,
- 所以断言错了会在运行时失败。declare global{" "}
+ declare 只陈述类型、不产出代码,所以断言错了会在运行时失败。declare global{" "}
是模块触及全局作用域的方式(写在别处报{" "}
ts(2669))。declare module "x"{" "}
- 写在脚本文件里是声明一个没有类型的模块,
- 写在模块文件里是扩充一个已有类型的模块;
- 去扩充一个没有类型的模块会报 ts(2665)。
+ 写在脚本文件里是声明一个没有类型的模块,写在模块文件里是扩充一个已有类型的模块;去扩充一个没有类型的模块会报 ts(2665)。
>
),
},
@@ -2566,8 +2464,7 @@ export default function ModulesPage() {
<>
解析只发生在编译期。moduleResolution 决定是否遵守{" "}
exports 和文件扩展名规则,而 paths{" "}
- 不会改动产物里的模块标识。
- 真正运行你代码的那一方,需要一份自己的对应配置。
+ 不会改动产物里的模块标识。真正运行你代码的那一方,需要一份自己的对应配置。
>
),
},
diff --git a/app/modules/viz.tsx b/app/modules/viz.tsx
index b8af5d4..c8d20b7 100644
--- a/app/modules/viz.tsx
+++ b/app/modules/viz.tsx
@@ -167,11 +167,8 @@ export function LibShelf() {
zh={
<>
不写 lib 时,TypeScript 会按 target{" "}
- 选一套默认的声明文件,而这套默认里包含 DOM。
- 自己写 lib 是替换默认值,不是往上追加。
- 所以一个只跑 Node 的项目写了{" "}
- "lib": ["es2022"] 之后就没有 DOM,
- 这时用 document 会报 ts(2584)。
+ 选一套默认的声明文件,而这套默认里包含 DOM。自己写 lib 是替换默认值,不是往上追加。所以一个只跑 Node 的项目写了{" "}
+ "lib": ["es2022"] 之后就没有 DOM,这时用 document 会报 ts(2584)。
>
}
/>
@@ -277,9 +274,7 @@ export function ErasedImports() {
}
zh={
<>
- 类型会被擦除,所以纯类型导入没有东西可导入,整行就消失了;
- 值导入则被原样保留。把只当类型用的名字标上 type,
- 这个结果在任何构建工具下都一样。
+ 类型会被擦除,所以纯类型导入没有东西可导入,整行就消失了;值导入则被原样保留。把只当类型用的名字标上 type,这个结果在任何构建工具下都一样。
>
}
/>
@@ -294,8 +289,7 @@ export function ErasedImports() {
}
zh={
<>
- 三行导入,两种东西:值 —— 代码运行时要用到;
- 类型 —— 只存在于编译期。切到 编译后 .js{" "}
+ 三行导入,两种东西:值 —— 代码运行时要用到;类型 —— 只存在于编译期。切到编译后 .js{" "}
看它们各自的结果。
>
}
@@ -369,8 +363,7 @@ const QUEST_FRAMES: QuestFrameDef[] = [
zh={
<>
你写下 import {"{ debounce }"} from "lodash"
- 。编译器现在需要知道 debounce 的类型,
- 于是开始找它的声明。它会按顺序看三个地方,哪一个有结果就停。
+ 。编译器现在需要知道 debounce 的类型,于是开始找它的声明。它会按顺序看三个地方,哪一个有结果就停。
>
}
/>
@@ -394,9 +387,7 @@ const QUEST_FRAMES: QuestFrameDef[] = [
zh={
<>
第一站:包里面。编译器读 lodash 的{" "}
- package.json,找 types 字段,
- 或者 exports 里的 types 条件 —— 没有。
- lodash 是纯 JavaScript 写的,自己不带声明。
+ package.json,找 types 字段,或者 exports 里的 types 条件 —— 没有。lodash 是纯 JavaScript 写的,自己不带声明。
>
}
/>
@@ -445,10 +436,8 @@ const QUEST_FRAMES: QuestFrameDef[] = [
}
zh={
<>
- 假如第二站也是空的(冷门库很常见),还剩最后一个地方:
- 你项目里自己的声明文件。写一段{" "}
- declare module "lodash",
- 把你真正用到的部分描出来。
+ 假如第二站也是空的(冷门库很常见),还剩最后一个地方:你项目里自己的声明文件。写一段{" "}
+ declare module "lodash",把你真正用到的部分描出来。
>
}
/>
@@ -474,8 +463,7 @@ const QUEST_FRAMES: QuestFrameDef[] = [
<>
三个地方全是空的。那 debounce 就是隐式{" "}
any;打开 noImplicitAny
- (strict 的一部分)时会报 ts(7016)。
- 报错原文自己就写出了剩下两条路:"Try{" "}
+ (strict 的一部分)时会报 ts(7016)。报错原文自己就写出了剩下两条路:"Try{" "}
npm i --save-dev @types/lodash if it exists or add a
new declaration (.d.ts) file containing{" "}
declare module 'lodash';"
@@ -499,8 +487,7 @@ const QUEST_FRAMES: QuestFrameDef[] = [
zh={
<>
再看一遍顺序:① 包自带的类型 → ② @types → ③ 你自己写的声明
- ,第一个有结果的胜出。给项目挑库时按这个顺序查一遍,
- 装之前就知道它的类型支持要付多少代价。
+ ,第一个有结果的胜出。给项目挑库时按这个顺序查一遍,装之前就知道它的类型支持要付多少代价。
>
}
/>
diff --git a/app/narrowing/page.tsx b/app/narrowing/page.tsx
index ecf02ed..47668eb 100644
--- a/app/narrowing/page.tsx
+++ b/app/narrowing/page.tsx
@@ -695,8 +695,7 @@ export default function NarrowingPage() {
}
zh={
<>
- 想用某一种类型独有的成员,先写一道检查。
- 这道检查会告诉编译器:这条路上只可能是它。
+ 想用某一种类型独有的成员,先写一道检查。这道检查会告诉编译器:这条路上只可能是它。
>
}
/>
@@ -731,9 +730,7 @@ export default function NarrowingPage() {
<>
string | number 表示这个值是 string{" "}
或是 number,二者取一,而不是「同时具备两者的能力」
- —— 那是交叉类型 &,02 章已经讲过。
- 所以联合的成员越多,不检查时能做的事越少:
- 成员越多,共有成员越少。
+ —— 那是交叉类型 &,02 章已经讲过。所以联合的成员越多,不检查时能做的事越少:成员越多,共有成员越少。
>
}
/>
@@ -779,12 +776,9 @@ export default function NarrowingPage() {
zh={
<>
在分支里把类型变得更具体,这件事叫收窄(narrowing)
- 。编译器的做法是:按执行顺序走一遍你的代码,
- 在每一行记住此刻还剩哪些可能。这趟遍历叫
- 控制流分析(control flow analysis)。
- 同一个变量,第 3 行和第 5
- 行的类型可以不一样。在编辑器里把光标悬停在不同行的同一个变量上,
- 就能看到类型在变。
+ 。编译器的做法是:按执行顺序走一遍你的代码,在每一行记住此刻还剩哪些可能。这趟遍历叫
+ 控制流分析(control flow analysis)。同一个变量,第 3 行和第 5
+ 行的类型可以不一样。在编辑器里把光标悬停在不同行的同一个变量上,就能看到类型在变。
>
}
/>
@@ -830,9 +824,7 @@ export default function NarrowingPage() {
<>
1. 真值检查会连 0 和空字符串一起排除。
if (count) 看起来只排除了{" "}
- undefined,但 0 同样是 falsy,
- 也会走进 else 分支。「0 份配料」是真实数据,不是没填。
- 真正想表达的意思,就写 count !== undefined。
+ undefined,但 0 同样是 falsy,也会走进 else 分支。「0 份配料」是真实数据,不是没填。真正想表达的意思,就写 count !== undefined。
>
}
/>
@@ -879,11 +871,8 @@ export default function NarrowingPage() {
}
zh={
<>
- 第二个错误,与其读,不如亲眼看一次。下面这个窗口跑的是真编译器。
- 订单备注可能是一段自由文本、一个结构化备注,也可能根本没有;
- 而 typeof note === "object"{" "}
- 守住的那个分支,把 null 和对象一起放了进来。
- 第 6 行直接点名:
+ 第二个错误,与其读,不如亲眼看一次。下面这个窗口跑的是真编译器。订单备注可能是一段自由文本、一个结构化备注,也可能根本没有;而 typeof note === "object"{" "}
+ 守住的那个分支,把 null 和对象一起放了进来。第 6 行直接点名:
'note' is possibly 'null'.{" "}
两个预设分别是「完全不检查」的版本,和「先排除 null」的版本。
>
@@ -961,8 +950,7 @@ export default function NarrowingPage() {
zh={
<>
最常见的是回调函数。编译器无法知道这个回调什么时候
- 执行 —— 它可能在变量被改动之后才跑起来,
- 那么你先前写的那道检查就不再能说明任何事。
+ 执行 —— 它可能在变量被改动之后才跑起来,那么你先前写的那道检查就不再能说明任何事。
>
}
/>
@@ -990,8 +978,7 @@ export default function NarrowingPage() {
zh={
<>
把最后一行删掉,报错就消失了:只要 value{" "}
- 在整个函数里从未被重新赋值,编译器就把它当作固定不变的,
- 回调里的收窄依然有效。这条规则真正看的是
+ 在整个函数里从未被重新赋值,编译器就把它当作固定不变的,回调里的收窄依然有效。这条规则真正看的是
这个变量还会不会变,而不是「是不是回调」。
>
}
@@ -1011,8 +998,7 @@ export default function NarrowingPage() {
}
zh={
<>
- 被收窄的属性也一样,而且更严格:
- 对象属性随时可能被别处改掉,所以编译器从不把属性的收窄结果带进回调。
+ 被收窄的属性也一样,而且更严格:对象属性随时可能被别处改掉,所以编译器从不把属性的收窄结果带进回调。
>
}
/>
@@ -1049,9 +1035,7 @@ export default function NarrowingPage() {
}
zh={
<>
- 这是标准做法,而且它是诚实的:
- 你捕获的就是当时检查过的那个值,
- 而不是期望那个属性稍后还装着同一个值。
+ 这是标准做法,而且它是诚实的:你捕获的就是当时检查过的那个值,而不是期望那个属性稍后还装着同一个值。
>
}
/>
@@ -1084,14 +1068,10 @@ export default function NarrowingPage() {
}
zh={
<>
- 在同一个函数里,被收窄的属性能挺过一次普通的函数调用。
- 写成{" "}
+ 在同一个函数里,被收窄的属性能挺过一次普通的函数调用。写成{" "}
if (d.note) { clear(d); d.note.trim(); }
,编译器一声不吭 —— 哪怕 clear 可能已经把{" "}
- d.note 改成了 undefined。
- TypeScript 是有意留下这个缺口的:
- 如果要追踪每一次可能的修改,会有大量正确代码被误判。
- 这是少数几个「编译通过 ≠ 值还在」的地方。
+ d.note 改成了 undefined。TypeScript 是有意留下这个缺口的:如果要追踪每一次可能的修改,会有大量正确代码被误判。这是少数几个「编译通过 ≠ 值还在」的地方。
>
}
/>
@@ -1119,10 +1099,7 @@ export default function NarrowingPage() {
}
zh={
<>
- 给 let 赋值,会从那一行起清掉它的收窄结果。
- 回调里能否保留收窄,取决于那个变量有没有被重新赋值过。
- 属性的收窄则永远不会带进回调。
- 这三种情况的解法几乎都一样:把检查过的值复制进一个{" "}
+ 给 let 赋值,会从那一行起清掉它的收窄结果。回调里能否保留收窄,取决于那个变量有没有被重新赋值过。属性的收窄则永远不会带进回调。这三种情况的解法几乎都一样:把检查过的值复制进一个{" "}
const。
>
}
@@ -1155,8 +1132,7 @@ export default function NarrowingPage() {
}
zh={
<>
- 先看不好用的那种写法。所有字段挤在一个类型里,
- 「哪个状态有哪些字段」这条规则只存在于你的脑子里。
+ 先看不好用的那种写法。所有字段挤在一个类型里,「哪个状态有哪些字段」这条规则只存在于你的脑子里。
>
}
/>
@@ -1183,8 +1159,7 @@ export default function NarrowingPage() {
}
zh={
<>
- 那个 ! 就是类型系统帮不上忙的地方。
- 而且 status 的类型是宽泛的{" "}
+ 那个 ! 就是类型系统帮不上忙的地方。而且 status 的类型是宽泛的{" "}
string,把它拼成{" "}
"pald" 也能通过编译。
>
@@ -1218,8 +1193,7 @@ export default function NarrowingPage() {
每种状态一个成员,而且 status 的类型是
字面量类型 —— 就是{" "}
"paid" 这个词本身,而不是{" "}
- string。
- 只要比对这一个字段,整个对象的形状就确定了。
+ string。只要比对这一个字段,整个对象的形状就确定了。
>
}
/>
@@ -1282,9 +1256,7 @@ export default function NarrowingPage() {
都行。第二,这个字段是字面量类型,比如{" "}
"paid"、1、
true,而不是 string{" "}
- 这样的宽泛类型。第三,各成员的字面量互不相同。
- 三条都满足,比对一次就能收窄整个对象。
- 这是 TypeScript 里写状态机的常规做法。
+ 这样的宽泛类型。第三,各成员的字面量互不相同。三条都满足,比对一次就能收窄整个对象。这是 TypeScript 里写状态机的常规做法。
>
}
/>
@@ -1320,10 +1292,8 @@ export default function NarrowingPage() {
zh={
<>
把收窄推到尽头。每个 case{" "}
- 从联合里去掉一个成员,最后一个成员被去掉之后,就什么都不剩了。
- 「什么都不剩」这个类型就是 never。而{" "}
- never 不接受任何值,所以只有当编译器认同这一行
- 永远不会被执行到时,这个赋值才能通过。
+ 从联合里去掉一个成员,最后一个成员被去掉之后,就什么都不剩了。「什么都不剩」这个类型就是 never。而{" "}
+ never 不接受任何值,所以只有当编译器认同这一行永远不会被执行到时,这个赋值才能通过。
>
}
/>
@@ -1349,8 +1319,7 @@ export default function NarrowingPage() {
zh={
<>
把这行赋值读成一句断言:「走到 default 时,
- order 已经没有任何可能了。」
- 现在这句断言成立,所以代码编译通过,一切平静。
+ order 已经没有任何可能了。」现在这句断言成立,所以代码编译通过,一切平静。
>
}
/>
@@ -1379,9 +1348,7 @@ export default function NarrowingPage() {
}
zh={
<>
- 它的价值在后面。三个月后店里开始支持退款,
- 类型要加第四个状态 —— 而那个函数没人打算动。
- 就在下面这个窗口里做,它跑的是真编译器:往联合里加一行{" "}
+ 它的价值在后面。三个月后店里开始支持退款,类型要加第四个状态 —— 而那个函数没人打算动。就在下面这个窗口里做,它跑的是真编译器:往联合里加一行{" "}
| { status: "refunded"; createdAt: Date;
refundedAt: Date }
@@ -1432,12 +1399,9 @@ export default function NarrowingPage() {
zh={
<>
把这条消息读成一句话,它就是一条待办:走到{" "}
- default 时 refunded 还在场,
- 所以「这里什么都不剩了」这句断言不再成立。
- 加状态前后各点一次 default 里的{" "}
+ default 时 refunded 还在场,所以「这里什么都不剩了」这句断言不再成立。加状态前后各点一次 default 里的{" "}
order:它的类型会从 never{" "}
- 变成你漏掉的那个成员。补上缺的 case,
- 报错自己就消失了。
+ 变成你漏掉的那个成员。补上缺的 case,报错自己就消失了。
>
}
/>
@@ -1463,11 +1427,7 @@ export default function NarrowingPage() {
}
zh={
<>
- 如果项目里有二十个函数在 switch order.status,
- 加一个状态就会让这二十处同时报错、逐个点名。
- 你顺着报错清单改下去,不会漏掉任何一处。
- 没有这道检查,新状态会安静地落进 default,
- 或者根本没有分支接住它,函数返回 undefined。
+ 如果项目里有二十个函数在 switch order.status,加一个状态就会让这二十处同时报错、逐个点名。你顺着报错清单改下去,不会漏掉任何一处。没有这道检查,新状态会安静地落进 default,或者根本没有分支接住它,函数返回 undefined。
>
}
/>
@@ -1498,8 +1458,7 @@ export default function NarrowingPage() {
保护来自那行 never 赋值,而不是「有 default
分支」。在上面那个窗口里试一次:留着第四个状态,把整个{" "}
default 块换成{" "}
- default: return "unknown status";,
- 所有报错都会消失,包括那个你根本没处理的状态 ——
+ default: return "unknown status";,所有报错都会消失,包括那个你根本没处理的状态 ——
你等于告诉编译器,这事你管过了。
>
}
@@ -1549,13 +1508,8 @@ export default function NarrowingPage() {
}
zh={
<>
- o is Paid 是你对编译器的承诺,
- 编译器不看函数体就接受它。
- 哪怕你不小心写成{" "}
- return o.status === "pending",
- 它照样相信。从此类型就不再描述这个程序了。
- 这是少数几个能让 TypeScript 判断错误的地方,
- 所以谓词的函数体要写得短、写得一目了然。
+ o is Paid 是你对编译器的承诺,编译器不看函数体就接受它。哪怕你不小心写成{" "}
+ return o.status === "pending",它照样相信。从此类型就不再描述这个程序了。这是少数几个能让 TypeScript 判断错误的地方,所以谓词的函数体要写得短、写得一目了然。
>
}
/>
@@ -1575,8 +1529,7 @@ export default function NarrowingPage() {
zh={
<>
谓词是在 if 分支里收窄。断言函数
- 则是让调用之后的每一行都收窄:检查不通过就抛异常,
- 所以只要代码还在往下走,就说明检查通过了。
+ 则是让调用之后的每一行都收窄:检查不通过就抛异常,所以只要代码还在往下走,就说明检查通过了。
>
}
/>
@@ -1603,9 +1556,7 @@ export default function NarrowingPage() {
zh={
<>
这条标注规则很容易在不经意间撞上。用 function{" "}
- 声明写出来就没问题;
- 但如果把断言函数放进变量里,这个变量必须写出显式类型,
- 否则调用它不会产生收窄,编译器还会直接报错。
+ 声明写出来就没问题;但如果把断言函数放进变量里,这个变量必须写出显式类型,否则调用它不会产生收窄,编译器还会直接报错。
>
}
/>
@@ -1642,8 +1593,7 @@ export default function NarrowingPage() {
}
zh={
<>
- 只有「单参数、直接返回一个收窄表达式」的简短回调,
- 编译器才会推断出谓词。更复杂的逻辑仍然要手写{" "}
+ 只有「单参数、直接返回一个收窄表达式」的简短回调,编译器才会推断出谓词。更复杂的逻辑仍然要手写{" "}
is —— 而手写就意味着,那句承诺又归你负责了。
>
}
@@ -1683,8 +1633,7 @@ export default function NarrowingPage() {
}
zh={
<>
- ?? 有一个长得很像的前辈 ||,
- 两者的区别正是 §03 里那个真值检查的问题。
+ ?? 有一个长得很像的前辈 ||,两者的区别正是 §03 里那个真值检查的问题。
>
}
/>
@@ -1743,11 +1692,8 @@ export default function NarrowingPage() {
<>
?. 和 ?? 是 JavaScript
的正式操作符,编译后是真实的运行时检查。而 !{" "}
- 是 TypeScript 的标注,编译后会消失。
- 它不做任何检查,只是让编译器不再报错。
- 运行时那个值如果真的是 null,你照样会拿到和
- JavaScript 里一样的 TypeError,
- 而且这次连编译器的提醒都没有了。规矩:能用 ?.{" "}
+ 是 TypeScript 的标注,编译后会消失。它不做任何检查,只是让编译器不再报错。运行时那个值如果真的是 null,你照样会拿到和
+ JavaScript 里一样的 TypeError,而且这次连编译器的提醒都没有了。规矩:能用 ?.{" "}
和 ?? 表达的,就不要用 !。
>
}
@@ -1795,8 +1741,7 @@ export default function NarrowingPage() {
}
zh={
<>
- 联合类型只允许你使用每一种成员类型都有的成员,
- 因为编译器按最坏情况处理。想用其余的,先收窄。
+ 联合类型只允许你使用每一种成员类型都有的成员,因为编译器按最坏情况处理。想用其余的,先收窄。
>
}
/>,
@@ -1813,8 +1758,7 @@ export default function NarrowingPage() {
}
zh={
<>
- 收窄的本质是控制流分析:编译器按顺序走一遍代码,
- 在每个分支里给变量一个更具体的类型。typeof
+ 收窄的本质是控制流分析:编译器按顺序走一遍代码,在每个分支里给变量一个更具体的类型。typeof
、真值检查、相等比较、in、
instanceof、Array.isArray{" "}
和字面量比对,都能收窄。
@@ -1850,9 +1794,7 @@ export default function NarrowingPage() {
}
zh={
<>
- 收窄会在这些地方失效:let 被重新赋值之后;
- 回调内部,只要那个变量可能被重新赋值,或者它是对象属性。
- 解法是把检查过的值复制进一个 const。
+ 收窄会在这些地方失效:let 被重新赋值之后;回调内部,只要那个变量可能被重新赋值,或者它是对象属性。解法是把检查过的值复制进一个 const。
>
}
/>,
@@ -1867,8 +1809,7 @@ export default function NarrowingPage() {
}
zh={
<>
- 可辨识联合需要:一个公共字段、字面量类型、各成员的字面量互不相同。
- 三条齐了,比对一次就能收窄整个对象。
+ 可辨识联合需要:一个公共字段、字面量类型、各成员的字面量互不相同。三条齐了,比对一次就能收窄整个对象。
>
}
/>,
@@ -1886,9 +1827,7 @@ export default function NarrowingPage() {
zh={
<>
在 default 分支里写{" "}
- const _x: never = order 就是穷尽检查。
- 所有 case 都处理完时,剩下的类型是空联合,赋值成立;
- 一旦联合里多出一个成员,这行立刻编译失败。
+ const _x: never = order 就是穷尽检查。所有 case 都处理完时,剩下的类型是空联合,赋值成立;一旦联合里多出一个成员,这行立刻编译失败。
>
}
/>,
@@ -1904,8 +1843,7 @@ export default function NarrowingPage() {
zh={
<>
o is Paid 把一段检查打包复用,
- asserts o is Paid 则让调用之后的代码收窄。
- 两者的函数体编译器都不验证 —— 那句承诺由你负责。
+ asserts o is Paid 则让调用之后的代码收窄。两者的函数体编译器都不验证 —— 那句承诺由你负责。
>
}
/>,
@@ -1922,8 +1860,7 @@ export default function NarrowingPage() {
zh={
<>
?. 和 ?? 会编译成真实的运行时检查;
- ! 编译后什么都不剩,也什么都不检查。
- 当默认值有可能合法地取 0 或{" "}
+ ! 编译后什么都不剩,也什么都不检查。当默认值有可能合法地取 0 或{" "}
"" 时,绝不要用 ||。
>
}
diff --git a/app/narrowing/viz.tsx b/app/narrowing/viz.tsx
index 0def8be..b4dd655 100644
--- a/app/narrowing/viz.tsx
+++ b/app/narrowing/viz.tsx
@@ -157,8 +157,7 @@ const FRAME_MSGS: ReactNode[] = [
}
zh={
<>
- 第三道检查:数字也走了。每过一道检查,类型就少一个成员。
- 这就是收窄的含义,而且编译器是逐行跟踪的。
+ 第三道检查:数字也走了。每过一道检查,类型就少一个成员。这就是收窄的含义,而且编译器是逐行跟踪的。
>
}
/>,
@@ -192,8 +191,7 @@ const FRAME_MSGS: ReactNode[] = [
}
zh={
<>
- 再假想加一道检查,把 boolean 也排除掉。
- 之后的类型就是 never:空联合
+ 再假想加一道检查,把 boolean 也排除掉。之后的类型就是 never:空联合
,一个没有任何可能取值的类型。记住它 —— §06
的穷尽检查完全建立在这个「空」之上。
>
@@ -344,8 +342,7 @@ const GUARDS: Guard[] = [
}
zh={
<>
- typeof x === "string" 为真时,
- 编译器排除掉所有不是字符串的可能;else 分支里再把字符串排除掉。
+ typeof x === "string" 为真时,编译器排除掉所有不是字符串的可能;else 分支里再把字符串排除掉。
typeof 的返回值只有八种:
"string"、"number"、
"bigint"、"boolean"、
@@ -371,8 +368,7 @@ const GUARDS: Guard[] = [
zh={
<>
typeof null === "object"。这个行为从 1995 年
- JavaScript 的第一个版本就是如此,改动会破坏现有代码,所以不会再改。
- 也就是说 typeof x === "object" 排除不掉{" "}
+ JavaScript 的第一个版本就是如此,改动会破坏现有代码,所以不会再改。也就是说 typeof x === "object" 排除不掉{" "}
null,得先写一句 x !== null。
>
}
@@ -411,8 +407,7 @@ const GUARDS: Guard[] = [
<>
if (count) 会排除所有 falsy 值:
undefined、null、0、
- ""、NaN。
- 它一行就能收窄最多,这也正是它最容易用错的原因。
+ ""、NaN。它一行就能收窄最多,这也正是它最容易用错的原因。
>
}
/>
@@ -430,8 +425,7 @@ const GUARDS: Guard[] = [
zh={
<>
0 和 "" 通常是真实数据:
- 0 份配料、空的备注。真值检查却把它们当成「没填」。
- 如果你只想排除 undefined,就写{" "}
+ 0 份配料、空的备注。真值检查却把它们当成「没填」。如果你只想排除 undefined,就写{" "}
count !== undefined。
>
}
@@ -520,9 +514,7 @@ function brew(drink: Tea | Coffee) {
}
zh={
<>
- "steep" in drink 问的是这个对象上有没有这个属性。
- 编译器保留声明了该属性的成员,排除掉其余的。
- 它适合形状不同的对象联合,尤其是你不想额外加一个标签字段的时候。
+ "steep" in drink 问的是这个对象上有没有这个属性。编译器保留声明了该属性的成员,排除掉其余的。它适合形状不同的对象联合,尤其是你不想额外加一个标签字段的时候。
>
}
/>
@@ -540,11 +532,9 @@ function brew(drink: Tea | Coffee) {
}
zh={
<>
- 如果那个属性声明为可选,else 分支就不会收窄。
- 对于{" "}
+ 如果那个属性声明为可选,else 分支就不会收窄。对于{" "}
type A = { kind: "a"; extra?: string }
- ,一个 A 完全可以没有 extra,
- 所以编译器在 else 分支里仍然保留 A。
+ ,一个 A 完全可以没有 extra,所以编译器在 else 分支里仍然保留 A。
>
}
/>
@@ -584,8 +574,7 @@ function brew(drink: Tea | Coffee) {
x instanceof Date 沿着 x{" "}
的原型链往上找,看 Date.prototype{" "}
在不在上面。它适合类的实例:Date、
- Error、你自己写的 class。
- 对字面量对象无效,因为它们不是由构造函数创建的。
+ Error、你自己写的 class。对字面量对象无效,因为它们不是由构造函数创建的。
>
}
/>
@@ -624,8 +613,7 @@ function brew(drink: Tea | Coffee) {
<>
typeof [] === "object",所以{" "}
typeof 分不出数组和普通对象。
- Array.isArray 分得出,TypeScript 也把它当作收窄检查。
- 它在标准库里就是用类型谓词声明的 —— §07 会讲这个机制。
+ Array.isArray 分得出,TypeScript 也把它当作收窄检查。它在标准库里就是用类型谓词声明的 —— §07 会讲这个机制。
>
}
/>
@@ -659,9 +647,7 @@ function brew(drink: Tea | Coffee) {
}
zh={
<>
- 前六种检查收窄的是一个值;这一种只比对一个字段,
- 却能把整个对象收窄到联合里的某一个成员,
- 连它带的其他字段一起确定。在真实代码里它最有用,§05 整节讲它。
+ 前六种检查收窄的是一个值;这一种只比对一个字段,却能把整个对象收窄到联合里的某一个成员,连它带的其他字段一起确定。在真实代码里它最有用,§05 整节讲它。
>
}
/>
@@ -751,8 +737,7 @@ const OS_CASES: OsCase[] = [
zh={
<>
订单刚下,还没付款,所以在这个分支里读{" "}
- order.paidAt 是编译错误。
- 换成纯 JavaScript,同样的代码会安静地给你一个{" "}
+ order.paidAt 是编译错误。换成纯 JavaScript,同样的代码会安静地给你一个{" "}
undefined,然后在{" "}
.toLocaleTimeString() 上抛错。
>
@@ -777,8 +762,7 @@ const OS_CASES: OsCase[] = [
zh={
<>
只比对了一句 status === "paid",
- paidAt 就从「可能没有」变成了「一定有」。
- 你没有多写一个 if,也不需要 !。
+ paidAt 就从「可能没有」变成了「一定有」。你没有多写一个 if,也不需要 !。
>
}
/>
@@ -800,9 +784,7 @@ const OS_CASES: OsCase[] = [
}
zh={
<>
- 终态有全部三个时间戳。已送达的订单一定付过款,
- 所以这个成员里也有 paidAt。
- 类型把这条业务规则原样记了下来。
+ 终态有全部三个时间戳。已送达的订单一定付过款,所以这个成员里也有 paidAt。类型把这条业务规则原样记了下来。
>
}
/>
diff --git a/app/page.tsx b/app/page.tsx
index 20a82f2..703ccf2 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -191,14 +191,10 @@ export default function HomePage() {
但 JavaScript 没看到。读一个不存在的属性?它不报错,安静地给你
undefined。undefined 乘 2?也不报错,算出 NaN。NaN
- 拼进字符串?照样不报错。于是深夜 1:47,一位买两杯四季春的用户,
- 看到了「合计:¥NaN」—— 截图、发群、@客服。你凌晨三点爬起来,
- 对着日志一行行 console.log,两小时后才找到那 5 个字母。
+ 拼进字符串?照样不报错。于是深夜 1:47,一位买两杯四季春的用户,看到了「合计:¥NaN」—— 截图、发群、@客服。你凌晨三点爬起来,对着日志一行行 console.log,两小时后才找到那 5 个字母。
- 同一段代码,如果文件后缀是 .ts —— 在你保存文件的那一刻,
- 编辑器就画出红线:「没有 totle,你是不是想写 total?」
- 连改法都替你想好了。
+ 同一段代码,如果文件后缀是 .ts —— 在你保存文件的那一刻,编辑器就画出红线:「没有 totle,你是不是想写 total?」连改法都替你想好了。
>
}
@@ -285,10 +281,7 @@ export default function HomePage() {
}
zh={
- 危险品免不了有人带 —— 问题是在登机口拦下,还是在天上发现。
- TypeScript 就是把安检口设在「保存文件」这一刻:所有拼错的属性、
- 传错的参数、想不到的 undefined,都在代码起飞前拦住。
- 安检慢三秒,好过空中冒烟。
+ 危险品免不了有人带 —— 问题是在登机口拦下,还是在天上发现。TypeScript 就是把安检口设在「保存文件」这一刻:所有拼错的属性、传错的参数、想不到的 undefined,都在代码起飞前拦住。安检慢三秒,好过空中冒烟。
}
/>
@@ -323,11 +316,7 @@ export default function HomePage() {
}
zh={
<>
- {"{ drink: string; total: number }"} 翻译成人话就是:
- 「凡是订单,必须带一个字符串的 drink 和一个数字的 total」。
- 这就是类型(type):对一个值的形状,事先立下的约定。
- 有了约定,编译器才有依据替你把关 —— 你写 order.totle,它翻一眼
- 说明书:约定里没有 totle 这一栏,红线伺候。
+ {"{ drink: string; total: number }"} 翻译成人话就是:「凡是订单,必须带一个字符串的 drink 和一个数字的 total」。这就是类型(type):对一个值的形状,事先立下的约定。有了约定,编译器才有依据替你把关 —— 你写 order.totle,它翻一眼说明书:约定里没有 totle 这一栏,红线伺候。
>
}
/>
@@ -397,11 +386,8 @@ export default function HomePage() {
}
zh={
- 意味着 TypeScript 不是一门要从头学的新语言。变量、函数、
- 数组、箭头函数……全都是你认识的 JS。TS 加的那一层 ——
- 类型注解、interface、泛型 —— 是「描述」而不是「逻辑」:
- 描述你的数据长什么样、你的函数收什么吐什么。这门课接下来的
- 十一章,教的全是这层「描述」怎么写、怎么想。
+ 意味着 TypeScript 不是一门要从头学的新语言。变量、函数、数组、箭头函数……全都是你认识的 JS。TS 加的那一层 ——
+ 类型注解、interface、泛型 —— 是「描述」而不是「逻辑」:描述你的数据长什么样、你的函数收什么吐什么。这门课接下来的十一章,教的全是这层「描述」怎么写、怎么想。
}
/>
@@ -440,8 +426,7 @@ export default function HomePage() {
),
zh: (
<>
- JS 的逻辑:totle 不存在?那就是 undefined。undefined 乘 2?
- 那就是 NaN。全程零报错,炸给用户看。
+ JS 的逻辑:totle 不存在?那就是 undefined。undefined 乘 2?那就是 NaN。全程零报错,炸给用户看。
>
),
}}
@@ -510,9 +495,7 @@ export default function HomePage() {
zh={
<>
不是录像,也不是我手写的报错文案 —— 真正的 TypeScript
- 编译器就在你的浏览器里跑,在后台线程。改代码,它重新检查;
- 把 totle 拼对,报错自己消失。点任意名字看它推断出的
- 类型,切到编译产物 JS 看真正运行的是什么。
+ 编译器就在你的浏览器里跑,在后台线程。改代码,它重新检查;把 totle 拼对,报错自己消失。点任意名字看它推断出的类型,切到编译产物 JS 看真正运行的是什么。
>
}
/>
@@ -567,8 +550,7 @@ export default function HomePage() {
>
typescriptlang.org/play
- ,免注册。左边贴代码,红线立刻出现;鼠标悬停变量能看类型;
- 右侧 .JS 标签是编译产物;还能一键分享链接 ——
+ ,免注册。左边贴代码,红线立刻出现;鼠标悬停变量能看类型;右侧 .JS 标签是编译产物;还能一键分享链接 ——
全书的动手任务都在这打。
>
}
@@ -635,8 +617,7 @@ export default function HomePage() {
),
zh: (
<>
- tsc 只干两件事:检查 + 翻译。检查:对照类型说明书找错,
- 有错就报;翻译:把 .ts 擦掉类型变成 .js。两件事可以分开干 ——
+ tsc 只干两件事:检查 + 翻译。检查:对照类型说明书找错,有错就报;翻译:把 .ts 擦掉类型变成 .js。两件事可以分开干 ——
--noEmit 就是「只安检,不放行产物」。
>
),
@@ -672,8 +653,7 @@ export default function HomePage() {
zh={
<>
很多人以为 TS 会把类型「带进」运行时,替你在线上把关。恰恰相反:
- 编译后,类型全部消失(type erasure),产物就是普通 JS,
- 运行时行为和你手写 JS 一模一样。点下面三个按钮,亲眼看一遍:
+ 编译后,类型全部消失(type erasure),产物就是普通 JS,运行时行为和你手写 JS 一模一样。点下面三个按钮,亲眼看一遍:
>
}
/>
@@ -700,10 +680,8 @@ export default function HomePage() {
}
zh={
- 类型检查的对象是你写的代码,不是运行时闯进来的数据。
- 接口返回的 JSON 长歪了、用户输入了奇怪的东西 ——
- 这些发生在类型被擦掉之后,TS 管不着。运行时的保护要靠真正的校验代码
- (亲手写 if 判断,或用 zod 这类校验库)。这个伏笔埋在这,
+ 类型检查的对象是你写的代码,不是运行时闯进来的数据。接口返回的 JSON 长歪了、用户输入了奇怪的东西 ——
+ 这些发生在类型被擦掉之后,TS 管不着。运行时的保护要靠真正的校验代码(亲手写 if 判断,或用 zod 这类校验库)。这个伏笔埋在这,
终章「类型思维」回收。
}
@@ -812,9 +790,7 @@ export default function HomePage() {
}
zh={
- 学习阶段,Playground 就是你的主场:免装、免配、实时报错。
- 等第 10 章讲 tsconfig 时,再回来认真配一条自己的生产路线。
- 现在只要记住一件事:三条路的终点都是 JS。
+ 学习阶段,Playground 就是你的主场:免装、免配、实时报错。等第 10 章讲 tsconfig 时,再回来认真配一条自己的生产路线。现在只要记住一件事:三条路的终点都是 JS。
}
/>
@@ -854,9 +830,7 @@ export default function HomePage() {
zh={
不是。TS 是 JS 的超集:你写过的每一行 JS 在 TS
- 里原样有效,改个后缀就是起点。要新学的只有「类型」这一层描述,
- 而且可以渐进 —— 今天给一个函数标注,明天给一个对象定形,
- 不需要推倒重来。
+ 里原样有效,改个后缀就是起点。要新学的只有「类型」这一层描述,而且可以渐进 —— 今天给一个函数标注,明天给一个对象定形,不需要推倒重来。
}
/>
@@ -882,8 +856,7 @@ export default function HomePage() {
zh={
类型在编译后全部擦除,运行时没有任何检查发生。TS
- 保证的是「你的代码内部自洽」,保证不了「外面进来的数据规矩」。
- 接口数据、用户输入,该校验还得校验 —— 终章教你怎么把两层防线接起来。
+ 保证的是「你的代码内部自洽」,保证不了「外面进来的数据规矩」。接口数据、用户输入,该校验还得校验 —— 终章教你怎么把两层防线接起来。
}
/>
@@ -907,9 +880,7 @@ export default function HomePage() {
}
zh={
- 不会。编译产物就是普通 JS,引擎看不到任何类型,谈不上按类型优化。
- TS 提速的不是程序,是你:重构敢下手、补全真的准、
- 半夜不用起来查 NaN —— 快在开发,不在运行。
+ 不会。编译产物就是普通 JS,引擎看不到任何类型,谈不上按类型优化。TS 提速的不是程序,是你:重构敢下手、补全真的准、半夜不用起来查 NaN —— 快在开发,不在运行。
}
/>
@@ -991,8 +962,7 @@ export default function HomePage() {
),
zh: (
<>
- 类型 = 值的形状说明书,一份事先立下的约定;
- 编译器拿着说明书,替你验每一次取值和传参。
+ 类型 = 值的形状说明书,一份事先立下的约定;编译器拿着说明书,替你验每一次取值和传参。
>
),
},
@@ -1022,8 +992,7 @@ export default function HomePage() {
),
zh: (
<>
- 编译后类型全部擦除,产物就是普通 JS:运行时不多一分保护,
- 也不多一分开销 —— 外部数据的校验要另外写。
+ 编译后类型全部擦除,产物就是普通 JS:运行时不多一分保护,也不多一分开销 —— 外部数据的校验要另外写。
>
),
},
@@ -1037,8 +1006,7 @@ export default function HomePage() {
),
zh: (
<>
- tsc = 检查 + 翻译;跑 TS 三条路(tsc / bundler / 直接跑),
- 终点都是 JS。学习阶段,Playground 是你的主场。
+ tsc = 检查 + 翻译;跑 TS 三条路(tsc / bundler / 直接跑),终点都是 JS。学习阶段,Playground 是你的主场。
>
),
},
diff --git a/app/structural/page.tsx b/app/structural/page.tsx
index 5eb54a3..addade5 100644
--- a/app/structural/page.tsx
+++ b/app/structural/page.tsx
@@ -427,10 +427,7 @@ export default function StructuralPage() {
),
zh: (
<>
- TypeScript 从不问一个类型叫什么名字,
- 它只把值实际拥有的成员和目标要求的成员比一遍。
- 这一章讲透这条贯穿全书的地基规则,
- 再拆掉几乎每个新手都会撞上的那个例外:多余属性检查。
+ TypeScript 从不问一个类型叫什么名字,它只把值实际拥有的成员和目标要求的成员比一遍。这一章讲透这条贯穿全书的地基规则,再拆掉几乎每个新手都会撞上的那个例外:多余属性检查。
>
),
}}
@@ -507,16 +504,10 @@ export default function StructuralPage() {
zh={
<>
- 奶茶店缺人,老板贴出要求:有名字,会做茶。
- 来了个应聘者,没带任何证书 —— 老板不在乎。名字?有。做茶?
- 当场做了一杯。录用。至于这人以前叫「正式工」还是
- 「隔壁店员工」,没人问。
+ 奶茶店缺人,老板贴出要求:有名字,会做茶。来了个应聘者,没带任何证书 —— 老板不在乎。名字?有。做茶?当场做了一杯。录用。至于这人以前叫「正式工」还是「隔壁店员工」,没人问。
- 这就是结构化类型(structural typing)的全部内容:
- 一个值属不属于某个类型,只取决于它有哪些成员,
- 而不取决于它曾经声明过自己是谁。
- 这个想法还有一个更早的名字:鸭子测试(duck test)——
+ 这就是结构化类型(structural typing)的全部内容:一个值属不属于某个类型,只取决于它有哪些成员,而不取决于它曾经声明过自己是谁。这个想法还有一个更早的名字:鸭子测试(duck test)——
走路像鸭子、叫声像鸭子,那就当它是鸭子。
>
@@ -544,8 +535,7 @@ export default function StructuralPage() {
}
zh={
<>
- zhen 和 Staff 之间没有任何声明过的关系。
- 编译器把 zhen 的成员和 Staff{" "}
+ zhen 和 Staff 之间没有任何声明过的关系。编译器把 zhen 的成员和 Staff{" "}
要求的成员比了一遍,全都找到了,于是放行。
>
}
@@ -572,8 +562,7 @@ export default function StructuralPage() {
}
zh={
<>
- Staff 是你给某个形状贴的标签,方便人读代码。
- 编译器比较类型时,标签不参与比较。
+ Staff 是你给某个形状贴的标签,方便人读代码。编译器比较类型时,标签不参与比较。
>
}
/>
@@ -598,8 +587,7 @@ export default function StructuralPage() {
zh={
<>
有哪些成员、每个成员是什么类型 ——
- 这份清单才是编译器眼里的类型。
- 清单相同的两个类型,是同一样东西的两个名字。
+ 这份清单才是编译器眼里的类型。清单相同的两个类型,是同一样东西的两个名字。
>
}
/>
@@ -624,8 +612,7 @@ export default function StructuralPage() {
}
zh={
<>
- 目标要求的成员你都有,而且类型也对得上,检查就通过。
- 不需要任何声明 —— 兼容关系是算出来的,不是登记出来的。
+ 目标要求的成员你都有,而且类型也对得上,检查就通过。不需要任何声明 —— 兼容关系是算出来的,不是登记出来的。
>
}
/>
@@ -658,9 +645,7 @@ export default function StructuralPage() {
不是。interface A {"{ x: number }"} 和{" "}
interface B {"{ x: number }"}{" "}
可以互相赋值,两个方向都行 —— 名字只是标签,形状相同。
- type 在这里和 interface 表现完全一致:
- 两者都不会仅仅因为名字不同就造出一个独立的类型。
- 真要让同形状的两个类型互不相通,§05 会给出标准做法。
+ type 在这里和 interface 表现完全一致:两者都不会仅仅因为名字不同就造出一个独立的类型。真要让同形状的两个类型互不相通,§05 会给出标准做法。
>
}
/>
@@ -723,12 +708,9 @@ export default function StructuralPage() {
}
zh={
<>
- 名义类型系统比较的是声明:两个类型有关系,
- 必须有一方用 extends 或 implements{" "}
- 说出来。结构化类型系统比较的是成员,什么都不用声明。
- 再看一眼右边:在 TypeScript 里,连 class 都按形状比 ——
- 这是从 Java 转过来的人最先撞上的一条。
- 例外只有一个,而且要类里出现 private 或{" "}
+ 名义类型系统比较的是声明:两个类型有关系,必须有一方用 extends 或 implements{" "}
+ 说出来。结构化类型系统比较的是成员,什么都不用声明。再看一眼右边:在 TypeScript 里,连 class 都按形状比 ——
+ 这是从 Java 转过来的人最先撞上的一条。例外只有一个,而且要类里出现 private 或{" "}
protected 成员才会触发,第 08 章会讲。
>
}
@@ -761,14 +743,8 @@ export default function StructuralPage() {
}
zh={
<>
- 因为它的任务是描述已经存在的 JavaScript。
- 想想日常的 JS 值长什么样:随手写下的对象字面量、
- JSON.parse 返回的数据、
- 几个函数拼出来的对象 —— 它们谁都没有声明过什么。
- 如果兼容必须先有声明,那所有现存的 JS 文件都得先改写一遍
- 才能被 TypeScript 标注,没人会这么干。
- 结构化类型让 TypeScript 能按 JS 本来的写法去描述它,
- 这正是「JavaScript 超集」这句承诺的技术前提。
+ 因为它的任务是描述已经存在的 JavaScript。想想日常的 JS 值长什么样:随手写下的对象字面量、
+ JSON.parse 返回的数据、几个函数拼出来的对象 —— 它们谁都没有声明过什么。如果兼容必须先有声明,那所有现存的 JS 文件都得先改写一遍才能被 TypeScript 标注,没人会这么干。结构化类型让 TypeScript 能按 JS 本来的写法去描述它,这正是「JavaScript 超集」这句承诺的技术前提。
>
}
/>
@@ -873,9 +849,7 @@ export default function StructuralPage() {
name: string; makeTea: () => void; years: number;
}' but required in type 'Staff'.
{" "}
- 注意:barista 本身一个字没改。
- 可见「兼容」不是某个值自带的属性,
- 而是每次对着目标的要求重新算出来的。
+ 注意:barista 本身一个字没改。可见「兼容」不是某个值自带的属性,而是每次对着目标的要求重新算出来的。
>
}
/>
@@ -925,10 +899,8 @@ export default function StructuralPage() {
描述的集合小得多 —— 要求越多,合格的值越少。
- 所以「成员多」=「更具体」=「集合更小」。
- 小集合里的每个值同时也在大集合里,所以 Barista{" "}
- 总能用在要 Staff 的位置,反过来不行。
- 第 03 章的联合类型、第 05 章的泛型约束,靠的都是这同一个方向。
+ 所以「成员多」=「更具体」=「集合更小」。小集合里的每个值同时也在大集合里,所以 Barista{" "}
+ 总能用在要 Staff 的位置,反过来不行。第 03 章的联合类型、第 05 章的泛型约束,靠的都是这同一个方向。
>
}
@@ -960,8 +932,7 @@ export default function StructuralPage() {
这两种写法很像,形状却不同。
{"{ note?: string }"} 表示这个键可以不存在;
{"{ note: string | undefined }"}{" "}
- 表示这个键必须存在,只是它的值可以是 undefined。
- 后者要求了一个前者并不要求的成员,所以前者不能赋给后者。
+ 表示这个键必须存在,只是它的值可以是 undefined。后者要求了一个前者并不要求的成员,所以前者不能赋给后者。
>
}
/>
@@ -991,8 +962,7 @@ export default function StructuralPage() {
}
zh={
<>
- 成员是函数时同样按形状比较,只是它的参数和返回值方向相反。
- 这部分第 02 章已经讲全了,包括{" "}
+ 成员是函数时同样按形状比较,只是它的参数和返回值方向相反。这部分第 02 章已经讲全了,包括{" "}
strictFunctionTypes 开关,以及方法语法的例外。
>
}
@@ -1055,13 +1025,9 @@ export default function StructuralPage() {
}
zh={
<>
- 两边的对象一模一样。写在调用处报错,先存进变量就通过。
- 这不是 bug,而是一道独立的、更严格的检查:
- 多余属性检查(excess property check)。
- 它只对新鲜的对象字面量生效 ——
- 也就是直接写在「需要某个类型」的位置上的字面量。
- 它不属于可赋值性规则本身,这正是先存进变量它就消失的原因。
- 这样设计的理由,是下面两种情形的区别:
+ 两边的对象一模一样。写在调用处报错,先存进变量就通过。这不是 bug,而是一道独立的、更严格的检查:
+ 多余属性检查(excess property check)。它只对新鲜的对象字面量生效 ——
+ 也就是直接写在「需要某个类型」的位置上的字面量。它不属于可赋值性规则本身,这正是先存进变量它就消失的原因。这样设计的理由,是下面两种情形的区别:
>
}
/>
@@ -1089,10 +1055,8 @@ export default function StructuralPage() {
}
zh={
<>
- 这个对象是当场写出来、当场交出去的,没有第二个用途。
- 所以多出来的属性只有两种可能:拼错了(
- sweetnes),或者误解了类型。
- 两种都是 bug,编译器于是报出来 ——
+ 这个对象是当场写出来、当场交出去的,没有第二个用途。所以多出来的属性只有两种可能:拼错了(
+ sweetnes),或者误解了类型。两种都是 bug,编译器于是报出来 ——
左边那杯半糖就是这么被救回来的。
>
}
@@ -1124,10 +1088,7 @@ export default function StructuralPage() {
}
zh={
<>
- 存在变量里的对象,可能在别处有完全正当的用途:
- 它也许本来就是信息更全的 Barista,
- 顺带被当成 Staff 用。§03 允许这件事,
- 编译器就允许。代价是拼错的属性会一路混过去 ——
+ 存在变量里的对象,可能在别处有完全正当的用途:它也许本来就是信息更全的 Barista,顺带被当成 Staff 用。§03 允许这件事,编译器就允许。代价是拼错的属性会一路混过去 ——
右边那杯半糖就是这么丢的。
>
}
@@ -1165,8 +1126,7 @@ export default function StructuralPage() {
{'makeOrder({ item: "Boba milk tea", sweetnes: "half sugar" } as Order)'}
{" "}
确实能让报错消失。但错字还在,半糖照样丢。
- as 只是让编译器别再检查,它不会改变这个对象。
- 正确的修法永远是同一个:看清报错里的属性名,把拼写改对。
+ as 只是让编译器别再检查,它不会改变这个对象。正确的修法永远是同一个:看清报错里的属性名,把拼写改对。
>
}
/>
@@ -1221,8 +1181,7 @@ export default function StructuralPage() {
- 当多出来的属性和目标类型里的任何成员都不像时,
- 就没有建议,错误码也不同:
+ 当多出来的属性和目标类型里的任何成员都不像时,就没有建议,错误码也不同:
Object literal may only specify known properties, and
'cup' does not exist in type 'Order'.
@@ -1230,8 +1189,7 @@ export default function StructuralPage() {
- 两条说的是同一件事:对象字面量只能写目标类型认识的属性。 - 看到 ts(2561) 反而是好事 —— + 两条说的是同一件事:对象字面量只能写目标类型认识的属性。看到 ts(2561) 反而是好事 —— 编译器已经替你算出了正确的拼写。
> @@ -1254,11 +1212,7 @@ export default function StructuralPage() { } zh={ <> - 到这里为止,这条规则都只是被描述了一遍。 - 下面这个窗口跑的是真编译器,它可以直接证明。 - 同一个对象在里面写了两遍:一遍是直接写在调用处的字面量, - 一遍先过了一次变量,两遍都把sweetness 拼错了。
- 只有一处被报出来,就是第 11 行那个字面量 ——
+ 到这里为止,这条规则都只是被描述了一遍。下面这个窗口跑的是真编译器,它可以直接证明。同一个对象在里面写了两遍:一遍是直接写在调用处的字面量,一遍先过了一次变量,两遍都把 sweetness 拼错了。只有一处被报出来,就是第 11 行那个字面量 ——
在那个位置,多出来的属性不可能有别的用途。
>
}
@@ -1304,14 +1258,9 @@ export default function StructuralPage() {
}
zh={
<>
- 在那个窗口里,有四件事值得动手做一遍。切到第一个预设:
- 字面量没了,报错也没了,顾客要的半糖同样没了 ——
+ 在那个窗口里,有四件事值得动手做一遍。切到第一个预设:字面量没了,报错也没了,顾客要的半糖同样没了 ——
这就是这道豁免的全部代价。切到第二个预设:cup{" "}
- 和 Order 里的任何成员都不像,
- 建议随之消失,错误码变成 ts(2353)。然后用正确的方式修:
- 给 sweetnes 补上那个 s。
- 最后,想知道 as 到底买到了什么,
- 就在字面量后面加一句 as Order ——
+ 和 Order 里的任何成员都不像,建议随之消失,错误码变成 ts(2353)。然后用正确的方式修:给 sweetnes 补上那个 s。最后,想知道 as 到底买到了什么,就在字面量后面加一句 as Order ——
报错走了,错字还在。
>
}
@@ -1356,13 +1305,7 @@ export default function StructuralPage() {
}
zh={
<>
- 店里有两个类型:外卖平台用的 DeliveryAddress,
- 和到店自取用的 PickupInfo。
- 两拨人分头定义,结果都是一个电话加一句备注。
- 某天有人把自取单传给了打快递面单的函数,
- 编译器一声没吭 —— 成员对得上,它没有理由拦。
- 当晚,骑手照着面单上的备注,
- 去店里取了一单根本不存在的外卖。
+ 店里有两个类型:外卖平台用的 DeliveryAddress,和到店自取用的 PickupInfo。两拨人分头定义,结果都是一个电话加一句备注。某天有人把自取单传给了打快递面单的函数,编译器一声没吭 —— 成员对得上,它没有理由拦。当晚,骑手照着面单上的备注,去店里取了一单根本不存在的外卖。
>
}
/>
@@ -1390,9 +1333,7 @@ export default function StructuralPage() {
}
zh={
<>
- 结构化类型的判断完全正确:两个类型的成员相同。
- 问题在于「外卖」和「自取」的区别只存在于我们脑子里,
- 从没写进形状 —— 而编译器只读形状。
+ 结构化类型的判断完全正确:两个类型的成员相同。问题在于「外卖」和「自取」的区别只存在于我们脑子里,从没写进形状 —— 而编译器只读形状。
>
}
/>
@@ -1414,9 +1355,7 @@ export default function StructuralPage() {
zh={
<>
更常见的版本是 ID 混用:UserId 和{" "}
- PostId 都是 string,
- 于是「拿帖子 ID 去查用户」全程编译通过。
- 真正的解法只有一个:把区别写进形状 ——
+ PostId 都是 string,于是「拿帖子 ID 去查用户」全程编译通过。真正的解法只有一个:把区别写进形状 ——
既然只比形状,那就让形状不同。
>
}
@@ -1450,14 +1389,10 @@ export default function StructuralPage() {
}
zh={
<>
- 让每个类型和一个带标记成员的对象类型求交集,
- 两个形状就不一样了,两个 string 也就不再能互换。
- __brand 只存在于编译期,运行时没有任何开销。
- 代价也要说清楚:普通 string 不能赋给{" "}
- UserId,所以造一个出来总得断言一次。
- 通常的做法是只写一个 toUserId(s: string){" "}
- 函数来做这次断言,只在数据进入系统的入口调用它,
- 别处一律不写 as UserId。§06 的第 3
+ 让每个类型和一个带标记成员的对象类型求交集,两个形状就不一样了,两个 string 也就不再能互换。
+ __brand 只存在于编译期,运行时没有任何开销。代价也要说清楚:普通 string 不能赋给{" "}
+ UserId,所以造一个出来总得断言一次。通常的做法是只写一个 toUserId(s: string){" "}
+ 函数来做这次断言,只在数据进入系统的入口调用它,别处一律不写 as UserId。§06 的第 3
个任务会带你走一遍。
>
}
@@ -1495,12 +1430,9 @@ export default function StructuralPage() {
把鸭子测试推到极限:{"{}"} 要求
零个成员,而一份空要求几乎人人满足。
42、"tea"、
- true、函数、数组、任何对象,
- 统统能赋给 {"{}"}。只有 null 和{" "}
+ true、函数、数组、任何对象,统统能赋给 {"{}"}。只有 null 和{" "}
undefined 进不来,而且这还是靠{" "}
- strictNullChecks 开着。所以:
- 想表达「任意对象」写 object;
- 想表达「什么值都可能、用之前先收窄」写{" "}
+ strictNullChecks 开着。所以:想表达「任意对象」写 object;想表达「什么值都可能、用之前先收窄」写{" "}
unknown。{"{}"}{" "}
看着像一条要求,其实什么都没要求。
>
@@ -1548,8 +1480,7 @@ export default function StructuralPage() {
),
zh: (
<>
- TypeScript 用的是结构化类型:兼容取决于成员,不取决于名字。
- 名字是标签,成员清单才是类型本体。class 也按同一套规则比较。
+ TypeScript 用的是结构化类型:兼容取决于成员,不取决于名字。名字是标签,成员清单才是类型本体。class 也按同一套规则比较。
>
),
},
@@ -1564,8 +1495,7 @@ export default function StructuralPage() {
),
zh: (
<>
- 方向很重要:成员多的类型可以赋给成员少的,反过来不行。
- 用集合观最好记 —— 要求越多,集合越小,小集合被包含在大集合里。
+ 方向很重要:成员多的类型可以赋给成员少的,反过来不行。用集合观最好记 —— 要求越多,集合越小,小集合被包含在大集合里。
>
),
},
@@ -1580,10 +1510,7 @@ export default function StructuralPage() {
),
zh: (
<>
- 多余属性检查只对新鲜的对象字面量生效:
- 写在调用处报错,先存进变量就放行。
- 它是叠加在可赋值性之上的一道额外检查,目标是抓拼写错误,
- 不是一条通用规则。
+ 多余属性检查只对新鲜的对象字面量生效:写在调用处报错,先存进变量就放行。它是叠加在可赋值性之上的一道额外检查,目标是抓拼写错误,不是一条通用规则。
>
),
},
@@ -1598,8 +1525,7 @@ export default function StructuralPage() {
),
zh: (
<>
- 用 as 压掉这个报错,只是把错字藏起来,并没有修好。
- 读报错里的属性名,把拼写改对。ts(2561){" "}
+ 用 as 压掉这个报错,只是把错字藏起来,并没有修好。读报错里的属性名,把拼写改对。ts(2561){" "}
连正确的名字都告诉你了,ts(2353) 是同一个错误,只是没有建议。
>
),
@@ -1620,8 +1546,7 @@ export default function StructuralPage() {
zh: (
<>
形状相同就能互换,哪怕两者的含义毫不相干。
- UserId 混 PostId 这类事故,
- 用品牌类型把区别写进形状。另外记住:
+ UserId 混 PostId 这类事故,用品牌类型把区别写进形状。另外记住:
{"{ a?: number }"} 不等于{" "}
{"{ a: number | undefined }"};{"{}"}{" "}
什么都不要求 —— 想说「任意对象」请写 object。
diff --git a/app/structural/viz.tsx b/app/structural/viz.tsx
index b20b2da..2f38c8b 100644
--- a/app/structural/viz.tsx
+++ b/app/structural/viz.tsx
@@ -96,9 +96,7 @@ export function CompatPlayground() {
zh={
<>
通过。
- Staff 只问一件事:有没有 name,
- 是不是 string?Barista 的回答是有。
- 多出来的 makeTea 和 years{" "}
+ Staff 只问一件事:有没有 name,是不是 string?Barista 的回答是有。多出来的 makeTea 和 years{" "}
不在要求里,直接被忽略。成员多的类型,可以顶替成员少的。
>
}
@@ -440,8 +438,7 @@ function buildFrames(preset: Preset, literal: boolean): MatchFrame[] {
}
zh={
<>
- 还剩一个 {extraPins[0].key},目标类型里没有声明它。
- 这个字面量只为这一次调用而写,多出来的属性几乎都是拼错或误解。
+ 还剩一个 {extraPins[0].key},目标类型里没有声明它。这个字面量只为这一次调用而写,多出来的属性几乎都是拼错或误解。
多余属性检查在这里报错。
>
}
@@ -459,9 +456,7 @@ function buildFrames(preset: Preset, literal: boolean): MatchFrame[] {
}
zh={
<>
- 还剩一个 {extraPins[0].key},目标类型里没有声明它。
- 但这个对象存在变量里,别处可能另有正当用途,
- 于是按普通规则判断:成员多是允许的。不报错。
+ 还剩一个 {extraPins[0].key},目标类型里没有声明它。但这个对象存在变量里,别处可能另有正当用途,于是按普通规则判断:成员多是允许的。不报错。
>
}
/>
@@ -507,8 +502,7 @@ function buildFrames(preset: Preset, literal: boolean): MatchFrame[] {
}
zh={
<>
- 通过。多出来的 {extraPins[0].key} 没有被追究。
- 想看另一种结果,把上面的开关拨到「字面量直传」,再走一遍。
+ 通过。多出来的 {extraPins[0].key} 没有被追究。想看另一种结果,把上面的开关拨到「字面量直传」,再走一遍。
>
}
/>
@@ -523,8 +517,7 @@ function buildFrames(preset: Preset, literal: boolean): MatchFrame[] {
}
zh={
<>
- 通过。每个必需成员都在,类型也对得上。
- 这个对象从没声明过自己是 Staff,而这并不影响结果。
+ 通过。每个必需成员都在,类型也对得上。这个对象从没声明过自己是 Staff,而这并不影响结果。
>
}
/>
@@ -540,8 +533,7 @@ function buildFrames(preset: Preset, literal: boolean): MatchFrame[] {
}
zh={
<>
- 拒绝。有必需成员没对上,而且无论对象怎么传都是这个结果。
- 编译器的原文就在上面 —— 它描述的正是刚才没通过的那个成员。
+ 拒绝。有必需成员没对上,而且无论对象怎么传都是这个结果。编译器的原文就在上面 —— 它描述的正是刚才没通过的那个成员。
>
}
/>
@@ -559,8 +551,7 @@ function buildFrames(preset: Preset, literal: boolean): MatchFrame[] {
zh={
<>
拒绝。同一个对象,先存进变量就能通过 ——
- 把开关拨到「先存变量」验证一下。这不是前后矛盾:
- 编译器对新鲜的字面量刻意更严,因为此处多出来的属性几乎都是 bug。
+ 把开关拨到「先存变量」验证一下。这不是前后矛盾:编译器对新鲜的字面量刻意更严,因为此处多出来的属性几乎都是 bug。
>
}
/>
diff --git a/app/tsconfig/page.tsx b/app/tsconfig/page.tsx
index cf844ea..ce5bb4e 100644
--- a/app/tsconfig/page.tsx
+++ b/app/tsconfig/page.tsx
@@ -604,9 +604,7 @@ export default function TsconfigPage() {
<>
tsconfig.json 有两个读者。一个是{" "}
tsc,你在终端或 CI 里跑的那个命令;另一个是
- 类型服务 —— 编辑器在后台跑的进程,
- 你打字时的红线就是它画的。两者读的是同一份文件,
- 所以编辑器里的红线和 CI 里的失败说的是同一句话。
+ 类型服务 —— 编辑器在后台跑的进程,你打字时的红线就是它画的。两者读的是同一份文件,所以编辑器里的红线和 CI 里的失败说的是同一句话。
>
}
/>
@@ -622,8 +620,7 @@ export default function TsconfigPage() {
}
zh={
<>
- 它还决定检查有多严 —— 这一点很像游戏里的难度设置:
- 同一份代码,规则不同,结论就不同。生成这个文件只要一句命令。
+ 它还决定检查有多严 —— 这一点很像游戏里的难度设置:同一份代码,规则不同,结论就不同。生成这个文件只要一句命令。
>
}
/>
@@ -663,11 +660,8 @@ export default function TsconfigPage() {
<>
有两点值得注意。一是那个多余的逗号和这些注释:
tsconfig.json 是 JSONC ——
- 允许写注释的 JSON,所以你可以在里面记下「为什么开这一项」。
- 二是段落名。strict 在「Recommended Options」里,
- 而 noUncheckedIndexedAccess 和{" "}
- exactOptionalPropertyTypes 被单列成另一段。
- 这个分家是语言层面的事实,§03 就讲它。
+ 允许写注释的 JSON,所以你可以在里面记下「为什么开这一项」。二是段落名。strict 在「Recommended Options」里,而 noUncheckedIndexedAccess 和{" "}
+ exactOptionalPropertyTypes 被单列成另一段。这个分家是语言层面的事实,§03 就讲它。
>
}
/>
@@ -695,8 +689,7 @@ export default function TsconfigPage() {
zh={
<>
noImplicitAny、strictNullChecks{" "}
- 以及另外七项,决定编译器拒绝多少东西。
- 这是本章的重点,§02 逐项讲。
+ 以及另外七项,决定编译器拒绝多少东西。这是本章的重点,§02 逐项讲。
>
}
/>
@@ -718,8 +711,7 @@ export default function TsconfigPage() {
}
zh={
<>
- 产出的 JavaScript 用哪一代语法、哪种模块格式、
- 放进哪个目录。§04 讲这一组 ——
+ 产出的 JavaScript 用哪一代语法、哪种模块格式、放进哪个目录。§04 讲这一组 ——
也讲它们在什么情况下完全不起作用。
>
}
@@ -777,11 +769,8 @@ export default function TsconfigPage() {
zh={
<>
tsc 做两件互相独立的事:检查类型,和
- 写出 JavaScript —— 每个输入文件对应一个输出文件。
- 它不是打包器,从不把你的模块合成一个文件。
- 而这两件事确实是独立的:
- 类型报错不会阻止 JavaScript 被写出来,
- 除非你同时开 noEmitOnError。
+ 写出 JavaScript —— 每个输入文件对应一个输出文件。它不是打包器,从不把你的模块合成一个文件。而这两件事确实是独立的:
+ 类型报错不会阻止 JavaScript 被写出来,除非你同时开 noEmitOnError。
>
}
/>
@@ -812,8 +801,7 @@ export default function TsconfigPage() {
<>
今天大多数项目会把这两件事分开:打包器,或者 swc{" "}
/ esbuild,负责擦掉类型、产出 JavaScript,而{" "}
- tsc --noEmit 只用来做类型检查。
- 这种配置下,tsconfig 里的产物类选项不影响上线的东西{" "}
+ tsc --noEmit 只用来做类型检查。这种配置下,tsconfig 里的产物类选项不影响上线的东西{" "}
—— 决定权在另一个工具的配置里。花时间调{" "}
target 之前,先确认你的项目属于哪一种。
>
@@ -871,9 +859,7 @@ export default function TsconfigPage() {
useUnknownInCatchVariables、
alwaysStrict。这个名单是会变长的 ——
strictBuiltinIteratorReturn 是 TypeScript 5.6
- 才加进来的 —— 所以要确认某个具体版本的成员,
- 查那个版本的文档,别凭记忆。还有几个听起来像成员、
- 其实不是的:noImplicitOverride、
+ 才加进来的 —— 所以要确认某个具体版本的成员,查那个版本的文档,别凭记忆。还有几个听起来像成员、其实不是的:noImplicitOverride、
noUnusedLocals、noImplicitReturns{" "}
都在模板另一段「Style Options」里,而且是关着的。
>
@@ -925,11 +911,8 @@ export default function TsconfigPage() {
}
zh={
<>
- 文件里埋了三处问题,每一处由不同的开关负责。
- 先全部关掉,读一下结论:没有报错。
- 再单独打开 noImplicitAny,再单独打开{" "}
- strictNullChecks,最后打开 strict。
- 代码一个字没动,变的只有规则。
+ 文件里埋了三处问题,每一处由不同的开关负责。先全部关掉,读一下结论:没有报错。再单独打开 noImplicitAny,再单独打开{" "}
+ strictNullChecks,最后打开 strict。代码一个字没动,变的只有规则。
>
}
/>
@@ -985,12 +968,9 @@ export default function TsconfigPage() {
'undefined'. 而数组只有两个元素,
menu[5] 依旧一声不响。
- 管这件事的是 noUncheckedIndexedAccess,
- 它不是这个家族的成员。
- {" "}
- 单独把它打开,picked 上才出现第三处报错。
- §03 讲的就是这句话,而你刚刚是问编译器要的答案,
- 不是听我们说的。
+ 管这件事的是 noUncheckedIndexedAccess,它不是这个家族的成员。
+
+ 单独把它打开,picked 上才出现第三处报错。§03 讲的就是这句话,而你刚刚是问编译器要的答案,不是听我们说的。
>
}
/>
@@ -1012,12 +992,10 @@ export default function TsconfigPage() {
}
zh={
<>
- 还有一种组合值得试:让 strictNullChecks 关着,
- 单独打开 noUncheckedIndexedAccess。开关是亮的,
+ 还有一种组合值得试:让 strictNullChecks 关着,单独打开 noUncheckedIndexedAccess。开关是亮的,
但什么也没发生 —— 整个文件一处报错都没有。没有{" "}
strictNullChecks,string | undefined{" "}
- 会退回成 string,这个选项也就无话可说。
- 「开着但什么也没做」,正是手工挑选成员时会踩的那个坑。
+ 会退回成 string,这个选项也就无话可说。「开着但什么也没做」,正是手工挑选成员时会踩的那个坑。
>
}
/>
@@ -1036,9 +1014,7 @@ export default function TsconfigPage() {
}
zh={
<>
- 九项里有两项值得细看。第一项是 noImplicitAny。
- 当编译器自己推不出类型时,这项检查会让它报错,
- 而不是悄悄按 any 处理:
+ 九项里有两项值得细看。第一项是 noImplicitAny。当编译器自己推不出类型时,这项检查会让它报错,而不是悄悄按 any 处理:
>
}
/>
@@ -1064,8 +1040,7 @@ export default function TsconfigPage() {
zh={
<>
推不出类型就当 any,而 any{" "}
- 会让由它派生出的一切都不受检查。
- 写错的字段、传错的参数,全都通过。
+ 会让由它派生出的一切都不受检查。写错的字段、传错的参数,全都通过。
那几行只是名义上的 TypeScript。
>
}
@@ -1094,8 +1069,7 @@ export default function TsconfigPage() {
}
zh={
<>
- 规则就是:编译器推不出来,你就得自己写。
- 一旦类型写下来了,pirce{" "}
+ 规则就是:编译器推不出来,你就得自己写。一旦类型写下来了,pirce{" "}
这个拼写错误就在它所在的那一行报错。
>
}
@@ -1118,10 +1092,8 @@ export default function TsconfigPage() {
}
zh={
<>
- 第二项是 strictNullChecks,
- 它是对存量代码影响最大的一员。它关着时,null 和{" "}
- undefined 可以赋给任何类型,
- 于是「这个值可能没有」这句话根本没法写进类型:
+ 第二项是 strictNullChecks,它是对存量代码影响最大的一员。它关着时,null 和{" "}
+ undefined 可以赋给任何类型,于是「这个值可能没有」这句话根本没法写进类型:
>
}
/>
@@ -1179,8 +1151,7 @@ export default function TsconfigPage() {
}
zh={
<>
- 「可能没有」现在写进了类型,所以取属性之前必须先检查。
- 凌晨两点的 TypeError{" "}
+ 「可能没有」现在写进了类型,所以取属性之前必须先检查。凌晨两点的 TypeError{" "}
变成了今天下午的一条红线。
>
}
@@ -1212,9 +1183,7 @@ export default function TsconfigPage() {
zh={
<>
null 引用是 Tony Hoare 在 1965 年引入的,他在 2009
- 年公开为此道歉,称之为自己的「十亿美元错误」。
- 问题不在这个值本身,而在于多数语言里每个类型都默默包含它,
- 于是编译器没法告诉你哪里漏了检查。
+ 年公开为此道歉,称之为自己的「十亿美元错误」。问题不在这个值本身,而在于多数语言里每个类型都默默包含它,于是编译器没法告诉你哪里漏了检查。
strictNullChecks 是 TypeScript 给出的回答:
值可能没有,就写进类型;要用,先检查。
如果这一族里只能开一项,就开它。
@@ -1234,8 +1203,7 @@ export default function TsconfigPage() {
}
zh={
<>
- 剩下七项,一张表过。要读的是最后一列:
- 这项检查关着时,什么会被漏掉。
+ 剩下七项,一张表过。要读的是最后一列:这项检查关着时,什么会被漏掉。
>
}
/>
@@ -1278,8 +1246,7 @@ export default function TsconfigPage() {
}
zh={
<>
- 只能处理 Cat 的函数,
- 被放到了要求处理任意 Animal 的位置。
+ 只能处理 Cat 的函数,被放到了要求处理任意 Animal 的位置。
TS2322
>
}
@@ -1347,8 +1314,7 @@ export default function TsconfigPage() {
}
zh={
<>
- 打算「回头再赋值」的属性一直是 undefined,
- 直到有人去读它。TS2564
+ 打算「回头再赋值」的属性一直是 undefined,直到有人去读它。TS2564
>
}
/>
@@ -1370,8 +1336,7 @@ export default function TsconfigPage() {
zh={
<>
内置迭代器的 next() 返回{" "}
- IteratorResult<T, undefined>,
- 而不是 <T, any>
+ IteratorResult<T, undefined>,而不是 <T, any>
>
}
/>
@@ -1425,8 +1390,7 @@ export default function TsconfigPage() {
}
zh={
<>
- 游离函数里的 this 是 any,
- 从它上面读的每个属性都不受检查。TS2683
+ 游离函数里的 this 是 any,从它上面读的每个属性都不受检查。TS2683
>
}
/>
@@ -1528,10 +1492,7 @@ export default function TsconfigPage() {
}
zh={
<>
- strictPropertyInitialization 没法单独成立。
- 「没赋值」的意思就是「值是 undefined」,
- 而 strictNullChecks 关着时这句话不成立。
- 编译器不是忽略这个组合,而是直接拒绝:
+ strictPropertyInitialization 没法单独成立。「没赋值」的意思就是「值是 undefined」,而 strictNullChecks 关着时这句话不成立。编译器不是忽略这个组合,而是直接拒绝:
TS5052: Option
'strictPropertyInitialization' cannot be specified
@@ -1554,9 +1515,7 @@ export default function TsconfigPage() {
}
zh={
<>
- 这也是为什么推荐直接写 strict: true,
- 而不是把成员一条条列出来:它们本来是配套设计的,
- 自己挑一部分,很容易挑出一个「开着但什么也没做」的选项。
+ 这也是为什么推荐直接写 strict: true,而不是把成员一条条列出来:它们本来是配套设计的,自己挑一部分,很容易挑出一个「开着但什么也没做」的选项。
>
}
/>
@@ -1591,9 +1550,7 @@ export default function TsconfigPage() {
zh={
<>
第一项是 noUncheckedIndexedAccess。读{" "}
- arr[0] 得到的是元素类型,
- 编译器默认下标不会越界。即便 strict 家族全开,
- 左边这段照样能编译:
+ arr[0] 得到的是元素类型,编译器默认下标不会越界。即便 strict 家族全开,左边这段照样能编译:
>
}
/>
@@ -1621,8 +1578,7 @@ export default function TsconfigPage() {
zh={
<>
数组只有三个元素,所以运行时 sizes[3] 是{" "}
- undefined。而类型说它是 string,
- 没有任何东西反驳这句话。
+ undefined。而类型说它是 string,没有任何东西反驳这句话。
>
}
/>
@@ -1651,8 +1607,7 @@ export default function TsconfigPage() {
}
zh={
<>
- 每次下标取值都变成 T | undefined,
- 想用就得先收窄。注意它的依赖:这个选项需要{" "}
+ 每次下标取值都变成 T | undefined,想用就得先收窄。注意它的依赖:这个选项需要{" "}
strictNullChecks。后者关着时,
string | undefined 会退回成{" "}
string,于是这个选项完全没有效果。
@@ -1676,9 +1631,7 @@ export default function TsconfigPage() {
}
zh={
<>
- 第二项是 exactOptionalPropertyTypes。
- 可选属性平时把两件事混在一起,它把它们分开:
- 键不存在,和键存在但值是 undefined。
+ 第二项是 exactOptionalPropertyTypes。可选属性平时把两件事混在一起,它把它们分开:键不存在,和键存在但值是 undefined。
>
}
/>
@@ -1705,8 +1658,7 @@ export default function TsconfigPage() {
<>
这个区分听着像抬杠,直到你写一个运行时判断。
{`"topping" in o`} 和{" "}
- Object.keys(o) 问的是「键在不在」。
- 只有开了这个选项,答案才和类型对得上 —— 用 in{" "}
+ Object.keys(o) 问的是「键在不在」。只有开了这个选项,答案才和类型对得上 —— 用 in{" "}
收窄之后拿到的是 string,而不是{" "}
string | undefined。
>
@@ -1739,10 +1691,7 @@ export default function TsconfigPage() {
<>
这两项针对的都是存量代码里到处都有的写法。
noUncheckedIndexedAccess 对每一次
- 下标读取都有话说,所以在一个大项目里打开它,
- 可能冒出几百个错 —— 单个都很小,加起来是一个项目。
- 把它们放在 strict 之外,
- 存量项目就可以先采用 strict,不必同时接下这件事。
+ 下标读取都有话说,所以在一个大项目里打开它,可能冒出几百个错 —— 单个都很小,加起来是一个项目。把它们放在 strict 之外,存量项目就可以先采用 strict,不必同时接下这件事。
>
}
/>
@@ -1762,8 +1711,7 @@ export default function TsconfigPage() {
}
zh={
<>
- 新项目没有这个包袱,tsc --init 也体现了这一点:
- 它把两个选项都写成 true。另外注意,
+ 新项目没有这个包袱,tsc --init 也体现了这一点:它把两个选项都写成 true。另外注意,
exactOptionalPropertyTypes 和{" "}
strictPropertyInitialization 有同样的硬依赖 ——
不开 strictNullChecks 就设它,编译器会报{" "}
@@ -1799,9 +1747,7 @@ export default function TsconfigPage() {
}
zh={
<>
- target 决定编译器写出哪一代 JavaScript 语法。
- 比 target 更新的写法,都会被改写成较旧的等价形式。
- 拨一下对比看看:
+ target 决定编译器写出哪一代 JavaScript 语法。比 target 更新的写法,都会被改写成较旧的等价形式。拨一下对比看看:
>
}
/>
@@ -1821,9 +1767,7 @@ export default function TsconfigPage() {
}
zh={
<>
- 上面那份对照是手画的。下面是把同一个问题交给编译器:
- 在下拉框里挑一个 target,切到编译产物 JS,
- 读真正写出来的东西。源码全程不变。
+ 上面那份对照是手画的。下面是把同一个问题交给编译器:在下拉框里挑一个 target,切到编译产物 JS,读真正写出来的东西。源码全程不变。
>
}
/>
@@ -1875,8 +1819,7 @@ export default function TsconfigPage() {
class 字段搬进了构造函数,而只声明没赋值的{" "}
note 整个消失了。到 es2015,
?? 和 ?. 变成了{" "}
- _a !== null && _a !== void 0,
- 文件顶部还多出一个临时变量。到 es5,
+ _a !== null && _a !== void 0,文件顶部还多出一个临时变量。到 es5,
class 变成往 prototype 上挂方法的{" "}
IIFE,模板字符串变成{" "}
"".concat(…),for...of{" "}
@@ -1900,10 +1843,8 @@ export default function TsconfigPage() {
}
zh={
<>
- 这些改写,每一条都是你没写过、却要发布、要调试、
- 要在堆栈信息里读的代码。这就是低 target{" "}
- 的价钱,只有当外面真的有跑不了新语法的东西时才值得付。
- 另外注意:在这里换 target,默认的{" "}
+ 这些改写,每一条都是你没写过、却要发布、要调试、要在堆栈信息里读的代码。这就是低 target{" "}
+ 的价钱,只有当外面真的有跑不了新语法的东西时才值得付。另外注意:在这里换 target,默认的{" "}
lib 也跟着换了 ——
下一段讲的正是「为什么这是两个问题」。
>
@@ -1934,8 +1875,7 @@ export default function TsconfigPage() {
Promise、Map、
Array.prototype.at、document{" "}
这些名字到底认不认识。设了 target{" "}
- 就顺带定了一个默认 lib,
- 所以在你需要把它们分开之前,这两个选项感觉像是一个:
+ 就顺带定了一个默认 lib,所以在你需要把它们分开之前,这两个选项感觉像是一个:
>
}
/>
@@ -1966,12 +1906,8 @@ export default function TsconfigPage() {
zh={
<>
这就是让区别变明显的那种情况:你想让 Promise{" "}
- 有类型,同时仍然产出 ES5 语法,于是把两个选项分开设。
- 但要清楚你说出口的是什么。lib{" "}
- 是你对运行环境的声明,编译器会信你的话,
- 它不会加 polyfill。声明了 es2022,
- 却跑在没有 Promise 的引擎上,
- 就是运行时失败,而编译期一声不响。
+ 有类型,同时仍然产出 ES5 语法,于是把两个选项分开设。但要清楚你说出口的是什么。lib{" "}
+ 是你对运行环境的声明,编译器会信你的话,它不会加 polyfill。声明了 es2022,却跑在没有 Promise 的引擎上,就是运行时失败,而编译期一声不响。
>
}
/>
@@ -1992,12 +1928,10 @@ export default function TsconfigPage() {
}
zh={
<>
- module 和 moduleResolution 同样是两个选项,
- 不是一个。module 是编译器写出的模块格式,
+ module 和 moduleResolution 同样是两个选项,不是一个。module 是编译器写出的模块格式,
moduleResolution 是它用来找到{" "}
import "./util"{" "}
- 背后那个文件的算法。实际配置时你是成对选的,
- 而选哪一对取决于谁消费产物:
+ 背后那个文件的算法。实际配置时你是成对选的,而选哪一对取决于谁消费产物:
>
}
/>
@@ -2031,8 +1965,7 @@ export default function TsconfigPage() {
}
zh={
<>
- nodenext 按 Node 自己的规则来,
- 包括 package.json 里的 type 和{" "}
+ nodenext 按 Node 自己的规则来,包括 package.json 里的 type 和{" "}
exports 字段。它同时替你定下{" "}
moduleResolution —— 你写成别的会得到{" "}
@@ -2071,8 +2004,7 @@ export default function TsconfigPage() {
zh={
<>
bundler 对齐的是 Vite 和 esbuild
- 实际的解析行为,这样编译器和真正干活的那个工具说的是一致的。
- 它要求 module 是 esnext、
+ 实际的解析行为,这样编译器和真正干活的那个工具说的是一致的。它要求 module 是 esnext、
preserve,或 es2015 及以上 ——
否则报 TS5095。而既然 JavaScript
由打包器产出,这里通常就该开 noEmit。
@@ -2107,8 +2039,7 @@ export default function TsconfigPage() {
}
zh={
<>
- 产物去哪(dist)、源码在哪(src)。
- 不设 outDir,每个 .js{" "}
+ 产物去哪(dist)、源码在哪(src)。不设 outDir,每个 .js{" "}
就落在自己的 .ts 旁边 ——
源码树变得难读,也很容易误提交。
>
@@ -2130,8 +2061,7 @@ export default function TsconfigPage() {
}
zh={
<>
- 写出一份从产物回到源码的映射,
- 于是调试器和堆栈信息可以指向你的 .ts{" "}
+ 写出一份从产物回到源码的映射,于是调试器和堆栈信息可以指向你的 .ts{" "}
那一行,而不是产物那一行。开着。
>
}
@@ -2153,9 +2083,7 @@ export default function TsconfigPage() {
}
zh={
<>
- 只当类型用的导入必须写成 import type,
- 否则报 TS1484。这样编译器就不必猜
- 「这个 import 能不能删」—— 它只删标了 type{" "}
+ 只当类型用的导入必须写成 import type,否则报 TS1484。这样编译器就不必猜「这个 import 能不能删」—— 它只删标了 type{" "}
的那些。第 09 章讲这件事。
>
}
@@ -2177,8 +2105,7 @@ export default function TsconfigPage() {
}
zh={
<>
- 拒绝那些无法「一个文件一个文件单独编译」的写法。
- 比如不写 export type 就转发一个类型,会报{" "}
+ 拒绝那些无法「一个文件一个文件单独编译」的写法。比如不写 export type 就转发一个类型,会报{" "}
TS1205。当产物由 Babel、swc{" "}
或 esbuild 这类单文件转译器生成时,这一项是必要的 ——
它们看不到你的其他文件。
@@ -2207,14 +2134,12 @@ export default function TsconfigPage() {
zh={
<>
让你可以对一个用 export = 的 CommonJS 模块写{" "}
- import legacy from "legacy"。
- 不开会报{" "}
+ import legacy from "legacy"。不开会报{" "}
TS1259: Module can only be default-imported using the
'esModuleInterop' flag
- 。它同时会改变产物里的互操作代码,
- 所以现代配置里默认是开的。
+ 。它同时会改变产物里的互操作代码,所以现代配置里默认是开的。
>
}
/>
@@ -2237,8 +2162,7 @@ export default function TsconfigPage() {
<>
noEmit:只检查、不写文件 ——
JavaScript 由别的工具产出时,就该用它。
- noEmitOnError:只在没有报错时才写产物。
- 这是两个不同的问题,而两者默认都是关的。
+ noEmitOnError:只在没有报错时才写产物。这是两个不同的问题,而两者默认都是关的。
>
}
/>
@@ -2273,10 +2197,7 @@ export default function TsconfigPage() {
.d.ts 文件内部
- 的类型检查 —— 全部 .d.ts,
- 包括你自己写的那些。它不跳过你的 .ts 代码,
- 也不影响这些声明继续被用来检查你的代码。
- 它跳过的只是「声明文件自己有没有毛病」这一项。
+ 的类型检查 —— 全部 .d.ts,包括你自己写的那些。它不跳过你的 .ts 代码,也不影响这些声明继续被用来检查你的代码。它跳过的只是「声明文件自己有没有毛病」这一项。
>
}
/>
@@ -2295,10 +2216,7 @@ export default function TsconfigPage() {
}
zh={
<>
- 严格来说这会丢信息:库的声明文件确实可能有错。
- 但现实里那些错不归你修,而两个库的全局声明互相冲突时,
- 还会因为一个与你的代码无关的原因让构建失败。
- 常见的选择是打开它,把检查的力气花在自己的文件上。
+ 严格来说这会丢信息:库的声明文件确实可能有错。但现实里那些错不归你修,而两个库的全局声明互相冲突时,还会因为一个与你的代码无关的原因让构建失败。常见的选择是打开它,把检查的力气花在自己的文件上。
>
}
/>
@@ -2324,15 +2242,12 @@ export default function TsconfigPage() {
zh={
<>
有个相关的选项值得知道:erasableSyntaxOnly
- (TypeScript 5.8 起)会拒绝一切有运行时效果、
- 因此无法「只擦类型」就去掉的语法 —— enum、
- 带实现的 namespace、构造函数参数属性。报错是{" "}
+ (TypeScript 5.8 起)会拒绝一切有运行时效果、因此无法「只擦类型」就去掉的语法 —— enum、带实现的 namespace、构造函数参数属性。报错是{" "}
TS1294: This syntax is not allowed when
'erasableSyntaxOnly' is enabled.
{" "}
- 如果你希望自己的文件能被「只擦类型」的工具直接运行
- (比如较新版本的 Node),这一项就有意义。
+ 如果你希望自己的文件能被「只擦类型」的工具直接运行(比如较新版本的 Node),这一项就有意义。
>
}
/>
@@ -2367,10 +2282,7 @@ export default function TsconfigPage() {
}
zh={
<>
- 失败的做法是一次做完:所有文件改名,strict 开满,
- 然后面对四百个报错,又没法只上线一部分修复。
- 有效的做法是渐进:每一步系统都在跑,
- 每一步都比上一步严。一帧一帧走一遍:
+ 失败的做法是一次做完:所有文件改名,strict 开满,然后面对四百个报错,又没法只上线一部分修复。有效的做法是渐进:每一步系统都在跑,每一步都比上一步严。一帧一帧走一遍:
>
}
/>
@@ -2391,8 +2303,7 @@ export default function TsconfigPage() {
zh={
<>
第 2 天那一步其实不必动 tsconfig。在某个 .js{" "}
- 文件顶部写两行注释就够了:一行打开检查,
- 一行给它一个可以对照的类型。
+ 文件顶部写两行注释就够了:一行打开检查,一行给它一个可以对照的类型。
>
}
/>
@@ -2426,8 +2337,7 @@ export default function TsconfigPage() {
为这个文件打开检查。@type 注释给{" "}
menu 一个类型 —— 没有它,menu 是{" "}
any,而在 any 上读{" "}
- it.pirce 是合法的,拼写错误就不会被报出来。
- JSDoc 类型是迁移期的桥,不是终点:等文件改成{" "}
+ it.pirce 是合法的,拼写错误就不会被报出来。JSDoc 类型是迁移期的桥,不是终点:等文件改成{" "}
.ts,它们就变成普通的类型标注。
>
}
@@ -2446,8 +2356,7 @@ export default function TsconfigPage() {
}
zh={
<>
- 每次迁移都会有今天修不了的错。
- 压住一行有两种注释,而它们随时间的表现差得很远:
+ 每次迁移都会有今天修不了的错。压住一行有两种注释,而它们随时间的表现差得很远:
>
}
/>
@@ -2492,8 +2401,7 @@ export default function TsconfigPage() {
}
zh={
<>
- 它说的是「我预期这里有错」。错误消失时,
- 这句话变成假的,于是注释自己报错。
+ 它说的是「我预期这里有错」。错误消失时,这句话变成假的,于是注释自己报错。
迁移期用它,并在后面写上原因。
>
}
@@ -2537,12 +2445,8 @@ export default function TsconfigPage() {
}
zh={
<>
- 顺序反了。strict 的成本随代码量增长。
- 第一天它是零:每个错都在你写下那一行的那一分钟被纠正。
- 三个月后,它是散落在你已经记不清的代码里的几百个错,
- 外加一个「要不就这样吧」的念头。渐进地打开{" "}
- strict 是存量代码的合理策略;
- 对新代码来说,它只是一张延后的账单。
+ 顺序反了。strict 的成本随代码量增长。第一天它是零:每个错都在你写下那一行的那一分钟被纠正。三个月后,它是散落在你已经记不清的代码里的几百个错,外加一个「要不就这样吧」的念头。渐进地打开{" "}
+ strict 是存量代码的合理策略;对新代码来说,它只是一张延后的账单。
>
}
/>
@@ -2571,10 +2475,7 @@ export default function TsconfigPage() {
}
zh={
<>
- 每一个 @ts-ignore 都是检查器不再读的一行。
- 偶尔为之是合理的取舍;成为习惯,就是一行一行地拆掉类型系统,
- 而且没有任何地方记着拆在哪里:项目看起来是有检查的,
- 实际上到处是没人列得出来的豁免。真的修不了的时候,用{" "}
+ 每一个 @ts-ignore 都是检查器不再读的一行。偶尔为之是合理的取舍;成为习惯,就是一行一行地拆掉类型系统,而且没有任何地方记着拆在哪里:项目看起来是有检查的,实际上到处是没人列得出来的豁免。真的修不了的时候,用{" "}
@ts-expect-error 并在旁边写上原因 ——
问题消失后,这条记录会自己要求被删掉。
>
@@ -2605,13 +2506,8 @@ export default function TsconfigPage() {
}
zh={
<>
- target 不是用来比新的版本号,
- 它是你对「产物要跑在什么环境里」的陈述。
- 如果那个环境是某个公司内网里锁定在旧版本的浏览器,
- 把它定成 esnext,就是把一个{" "}
- SyntaxError 送到真实用户面前。
- 编译器没法提醒你,因为只有你知道产物跑在哪。
- 先回答「谁来跑」。
+ target 不是用来比新的版本号,它是你对「产物要跑在什么环境里」的陈述。如果那个环境是某个公司内网里锁定在旧版本的浏览器,把它定成 esnext,就是把一个{" "}
+ SyntaxError 送到真实用户面前。编译器没法提醒你,因为只有你知道产物跑在哪。先回答「谁来跑」。
>
}
/>
@@ -2631,8 +2527,7 @@ export default function TsconfigPage() {
zh={
<>
这个误区还有另一半:在一个由打包器产出 JavaScript
- 的项目里花时间调 target。那种配置下,
- 上线的语法由打包器自己的 target 决定,tsconfig 里的{" "}
+ 的项目里花时间调 target。那种配置下,上线的语法由打包器自己的 target 决定,tsconfig 里的{" "}
target 只影响类型检查,以及 tsc{" "}
自己写出的那些文件。
>
@@ -2663,13 +2558,9 @@ export default function TsconfigPage() {
}
zh={
<>
- 不存在唯一正确的配置。
- 从别的项目抄一份过来,通常会一起抄来一些描述
- 「那个项目的环境」而不是你的环境的设置。
- 值得抄的是判断依据。两个问题就决定了这个文件的大半:
+ 不存在唯一正确的配置。从别的项目抄一份过来,通常会一起抄来一些描述「那个项目的环境」而不是你的环境的设置。值得抄的是判断依据。两个问题就决定了这个文件的大半:
谁运行产物 —— 直接是 Node,还是打包器;以及
- 谁生成产物 —— tsc,
- 还是另一个工具加上 tsc --noEmit 做检查。
+ 谁生成产物 —— tsc,还是另一个工具加上 tsc --noEmit 做检查。
>
}
/>
@@ -2688,10 +2579,7 @@ export default function TsconfigPage() {
zh={
<>
有两件事可以一般性地说。开 strict ——
- 对新代码来说,没有反对的理由。以及,
- 把你手上这份文件读一遍,而不是继承它:
- 里面每一个选项都是关于你的项目的一句断言,
- 而其中有些可能已经不成立了。
+ 对新代码来说,没有反对的理由。以及,把你手上这份文件读一遍,而不是继承它:里面每一个选项都是关于你的项目的一句断言,而其中有些可能已经不成立了。
>
}
/>
@@ -2740,8 +2628,7 @@ export default function TsconfigPage() {
zh: (
<>
tsconfig.json 由 tsc{" "}
- 和编辑器的类型服务共同读取,所以同一份代码在两边得到同一个答案。
- 它的选项分三类:检查多严、产物长什么样、管哪些文件。
+ 和编辑器的类型服务共同读取,所以同一份代码在两边得到同一个答案。它的选项分三类:检查多严、产物长什么样、管哪些文件。
>
),
},
@@ -2790,8 +2677,7 @@ export default function TsconfigPage() {
strict 不等于全部检查。
noUncheckedIndexedAccess(下标取值变成{" "}
T | undefined)和{" "}
- exactOptionalPropertyTypes 都在它之外,
- 要单独开。两者都需要 strictNullChecks 才起作用。
+ exactOptionalPropertyTypes 都在它之外,要单独开。两者都需要 strictNullChecks 才起作用。
>
),
},
@@ -2810,11 +2696,8 @@ export default function TsconfigPage() {
zh: (
<>
target 定产出的语法代次,并顺带定一个默认{" "}
- lib;lib 决定有哪些类型声明存在,
- 不会加 polyfill。module 是写出的模块格式,
- moduleResolution 是怎么找到 import 的东西。
- 直接给 Node 跑:module: nodenext;
- 交给打包器:
+ lib;lib 决定有哪些类型声明存在,不会加 polyfill。module 是写出的模块格式,
+ moduleResolution 是怎么找到 import 的东西。直接给 Node 跑:module: nodenext;交给打包器:
esnext + moduleResolution: bundler。
>
),
@@ -2834,10 +2717,8 @@ export default function TsconfigPage() {
<>
tsc 会检查,并为每个输入文件写出一个{" "}
.js;它不打包,而且除非设了{" "}
- noEmitOnError,类型报错也不会阻止它写产物。
- 如果 JavaScript 是打包器或 swc 产出的、
- tsc --noEmit 只负责检查,
- 那么产物类选项不影响上线的东西。
+ noEmitOnError,类型报错也不会阻止它写产物。如果 JavaScript 是打包器或 swc 产出的、
+ tsc --noEmit 只负责检查,那么产物类选项不影响上线的东西。
>
),
},
@@ -2859,8 +2740,7 @@ export default function TsconfigPage() {
checkJs 或 @ts-check 加 JSDoc 类型 →
一次一个文件改成 .ts → strict{" "}
的成员一项一项开。压住某一行用{" "}
- @ts-expect-error,不用 @ts-ignore:
- 错误消失后它会报 TS2578,所以这笔账丢不了。
+ @ts-expect-error,不用 @ts-ignore:错误消失后它会报 TS2578,所以这笔账丢不了。
>
),
},
diff --git a/app/tsconfig/viz.tsx b/app/tsconfig/viz.tsx
index b96967e..68fd7eb 100644
--- a/app/tsconfig/viz.tsx
+++ b/app/tsconfig/viz.tsx
@@ -362,8 +362,7 @@ export function StrictPanel() {
specified without specifying option
'strictNullChecks'.
- 「没赋值」的意思就是「值是 undefined」,
- 而 strictNullChecks 关着时这句话根本不成立。
+ 「没赋值」的意思就是「值是 undefined」,而 strictNullChecks 关着时这句话根本不成立。
>
}
/>
@@ -383,8 +382,7 @@ export function StrictPanel() {
开着,但什么也没变。strictNullChecks 关着时,
string | undefined 会退回成{" "}
string,所以给下标取值加上{" "}
- | undefined 没有任何作用。
- 把 strictNullChecks 打开,这一行才开始报错。
+ | undefined 没有任何作用。把 strictNullChecks 打开,这一行才开始报错。
>
}
/>
@@ -417,8 +415,7 @@ export function StrictPanel() {
<>
🏆 五个 bug 现在全是编译错误。看最后一个开关:
noUncheckedIndexedAccess{" "}
- 不属于 strict: true。上面那个按钮只打开四个家族成员,
- 这一个不动 —— 它得你自己开。
+ 不属于 strict: true。上面那个按钮只打开四个家族成员,这一个不动 —— 它得你自己开。
>
}
/>
@@ -555,8 +552,7 @@ export function TargetSwitch() {
zh={
<>
源码里用到的写法在 ES2022 里都已经存在,所以
- tsc 只擦掉类型,其他一个字不动。
- 产物小,而且和源码对得上、还能读。
+ tsc 只擦掉类型,其他一个字不动。产物小,而且和源码对得上、还能读。
>
}
/>
@@ -575,11 +571,8 @@ export function TargetSwitch() {
}
zh={
<>
- 为了跑在只认 ES5 的引擎上,tsc 把每一种较新的写法都改写成
- 旧的等价形式。产物变大,也更难读。只有真的存在这种引擎时
- 才把 target 定这么低 —— 而且记住,
- target 不会替你补上缺失的运行时 API。
- 那件事由 lib 描述,而 lib{" "}
+ 为了跑在只认 ES5 的引擎上,tsc 把每一种较新的写法都改写成旧的等价形式。产物变大,也更难读。只有真的存在这种引擎时才把 target 定这么低 —— 而且记住,
+ target 不会替你补上缺失的运行时 API。那件事由 lib 描述,而 lib{" "}
只改变检查器相信什么。
>
}
@@ -625,8 +618,7 @@ const MIG_FRAMES: MigFrame[] = [
}
zh={
<>
- 三个 JavaScript 文件,九百行,注释只有一句「别动」。
- 报错 0 —— 不是没有 bug,是没有人在查。
+ 三个 JavaScript 文件,九百行,注释只有一句「别动」。报错 0 —— 不是没有 bug,是没有人在查。
>
}
/>
@@ -655,8 +647,7 @@ const MIG_FRAMES: MigFrame[] = [
zh={
<>
npx tsc --init 生成配置。先只开一个选项:
- allowJs。.js 文件从此进入编译,
- 但只是被收进来,还没有被检查。谁也没被打扰,系统照常跑。
+ allowJs。.js 文件从此进入编译,但只是被收进来,还没有被检查。谁也没被打扰,系统照常跑。
>
}
/>
@@ -687,8 +678,7 @@ const MIG_FRAMES: MigFrame[] = [
zh={
<>
加上 checkJs(或在某个文件顶部写{" "}
- {"// @ts-check"}),编译器开始读这些旧代码。
- 假设它报了 23 个错。这些 bug 一直都在,今天才被点名。
+ {"// @ts-check"}),编译器开始读这些旧代码。假设它报了 23 个错。这些 bug 一直都在,今天才被点名。
注意:完全没有类型标注的 JavaScript 会藏住大部分问题 ——
JSDoc 注释就是你给检查器提供对照物的方式。
>
@@ -719,10 +709,8 @@ const MIG_FRAMES: MigFrame[] = [
}
zh={
<>
- 一次只改一个文件:order.js → order.ts,
- 修完它的报错再动下一个。一时修不了的,写上{" "}
- @ts-expect-error 并注明原因。
- 这行注释会在错误消失时自己报错,所以这笔账丢不了。
+ 一次只改一个文件:order.js → order.ts,修完它的报错再动下一个。一时修不了的,写上{" "}
+ @ts-expect-error 并注明原因。这行注释会在错误消失时自己报错,所以这笔账丢不了。
>
}
/>
@@ -751,10 +739,7 @@ const MIG_FRAMES: MigFrame[] = [
}
zh={
<>
- 所有文件都是 .ts 了,于是把这一族一项一项打开:
- 先 noImplicitAny 清掉隐式 any 的参数,
- 再 strictNullChecks 处理没判的 null。
- 每开一项,报错都是一批做得完的量。
+ 所有文件都是 .ts 了,于是把这一族一项一项打开:先 noImplicitAny 清掉隐式 any 的参数,再 strictNullChecks 处理没判的 null。每开一项,报错都是一批做得完的量。
>
}
/>
diff --git a/app/type-magic/page.tsx b/app/type-magic/page.tsx
index 88ad27e..d795f60 100644
--- a/app/type-magic/page.tsx
+++ b/app/type-magic/page.tsx
@@ -428,8 +428,7 @@ export default function TypeMagicPage() {
),
zh: (
<>
- 上一章那些工具类型,拆开都是同一批零件:keyof、
- 条件类型、infer、映射类型。 这一章把零件一个个认全 ——
+ 上一章那些工具类型,拆开都是同一批零件:keyof、条件类型、infer、映射类型。这一章把零件一个个认全 ——
章末,你会亲手把那五个工具类型重新写一遍。
>
),
@@ -513,9 +512,7 @@ export default function TypeMagicPage() {
zh={
<>
上一章你用 Partial、Pick{" "}
- 用得挺顺,没问过它们怎么工作。拆开看,里面没有特别的东西:
- 一个读键的、一个读类型的、一个做判断的、一个做循环的。
- 把它们拼起来,类型系统就成了一门可以编程的语言:
+ 用得挺顺,没问过它们怎么工作。拆开看,里面没有特别的东西:一个读键的、一个读类型的、一个做判断的、一个做循环的。把它们拼起来,类型系统就成了一门可以编程的语言:
输入是类型,输出也是类型,而且只在编译时运行。
>
}
@@ -534,8 +531,7 @@ export default function TypeMagicPage() {
}
zh={
<>
- 你熟悉的 JavaScript 操作,大多在类型这一侧有个对应的写法。
- 下面这张表就是本章的地图:每一行对应一节。
+ 你熟悉的 JavaScript 操作,大多在类型这一侧有个对应的写法。下面这张表就是本章的地图:每一行对应一节。
>
}
/>
@@ -645,8 +641,7 @@ export default function TypeMagicPage() {
}
zh={
<>
- 这一章的所有东西都发生在 tsc 检查代码的时候。
- 检查完,它们全部被去掉。这就是类型擦除,和第 00
+ 这一章的所有东西都发生在 tsc 检查代码的时候。检查完,它们全部被去掉。这就是类型擦除,和第 00
章讲的是同一条规则。所以复杂的类型只会花掉编译时间和可读性,
打包出来的 JavaScript 一个字节都不会多。
>
@@ -696,8 +691,7 @@ export default function TypeMagicPage() {
<>
对比着记:Object.keys(order){" "}
在程序运行时给你一个字符串数组;keyof Order{" "}
- 在编译时给你一个字符串字面量联合。
- 这个联合的每个成员都是真实存在的键 —— 所以{" "}
+ 在编译时给你一个字符串字面量联合。这个联合的每个成员都是真实存在的键 —— 所以{" "}
"cup" 会被拒绝。
>
),
@@ -769,12 +763,10 @@ export default function TypeMagicPage() {
}
zh={
<>
- JavaScript 的 typeof 在运行时执行,
- 返回八种字符串之一,比如 "string"、
+ JavaScript 的 typeof 在运行时执行,返回八种字符串之一,比如 "string"、
"object"(第 03
章拿它做过收窄)。TypeScript 的 typeof 只出现在
- 类型位置 —— type 声明的等号右边、
- 注解的冒号后面 —— 编译时会被去掉。
+ 类型位置 —— type 声明的等号右边、注解的冒号后面 —— 编译时会被去掉。
>
}
/>
@@ -844,10 +836,9 @@ export default function TypeMagicPage() {
说明:联合里如果一个成员已经覆盖了另一个,后者就会被吸收 ——
Size 是字符串组成的联合,而联合里本来就有{" "}
string,所以 Size 消失了。第二,
- T[number] 是读取数组元素类型的标准写法。 配上{" "}
+ T[number] 是读取数组元素类型的标准写法。配上{" "}
as const
- (它把数组变成由字面量类型组成的只读元组),
- 一份数据同时成了一份可选值名单:改一处,两边一起变。
+ (它把数组变成由字面量类型组成的只读元组),一份数据同时成了一份可选值名单:改一处,两边一起变。
>
),
}}
@@ -865,8 +856,7 @@ export default function TypeMagicPage() {
zh={
<>
三个零件齐了:keyof 读键,typeof{" "}
- 读值的类型,T[K] 读属性的类型。
- 本章后面的东西都建在它们上面。
+ 读值的类型,T[K] 读属性的类型。本章后面的东西都建在它们上面。
>
}
/>
@@ -905,7 +895,7 @@ export default function TypeMagicPage() {
zh: (
<>
这里的 extends 是「能不能赋给」的意思 —— 和第 04
- 章的兼容判断是同一件事,现在被用作三元表达式的条件。 成立就取{" "}
+ 章的兼容判断是同一件事,现在被用作三元表达式的条件。成立就取{" "}
X,不成立就取 Y。
>
),
@@ -925,8 +915,7 @@ export default function TypeMagicPage() {
}
zh={
<>
- 单看没什么。重要的行为出现在被判断的类型是联合的时候。
- 上一章说过 Exclude 的定义只有一行 ——
+ 单看没什么。重要的行为出现在被判断的类型是联合的时候。上一章说过 Exclude 的定义只有一行 ——
就是下面这行。可是一个三元表达式,怎么能从联合里
删掉成员?
>
@@ -970,11 +959,8 @@ export default function TypeMagicPage() {
只有 extends 左边是裸类型参数时才会发生
{" "}
- —— 也就是光秃秃的 T,外面什么都没包。
- 发生分发时,如果 T 是 A | B,
- 编译器就把整个条件类型改写成{" "}
- (A extends U ? X : Y) | (B extends U ? X : Y)。
- 规则就这么多。不知道这条,谁都没法预测条件类型会返回什么。
+ —— 也就是光秃秃的 T,外面什么都没包。发生分发时,如果 T 是 A | B,编译器就把整个条件类型改写成{" "}
+ (A extends U ? X : Y) | (B extends U ? X : Y)。规则就这么多。不知道这条,谁都没法预测条件类型会返回什么。
>
}
/>
@@ -1011,10 +997,8 @@ export default function TypeMagicPage() {
zh={
<>
这是设计,不是缺陷。想让每个成员各自处理(
- Exclude 那种),就用裸 T;
- 想把联合当成一个类型来判断,就把两边
- 都包进单元素元组:[T] extends [U]。
- 只包一边会改变比较的对象,所以两边都要包。
+ Exclude 那种),就用裸 T;想把联合当成一个类型来判断,就把两边
+ 都包进单元素元组:[T] extends [U]。只包一边会改变比较的对象,所以两边都要包。
>
}
/>
@@ -1052,12 +1036,9 @@ export default function TypeMagicPage() {
}
zh={
<>
- never 是没有成员的联合。对它分发,
- 判断跑零次,所以结果是 never。
- 这不是编译器里的特例,只是根本没有东西可以分发。 而{" "}
+ never 是没有成员的联合。对它分发,判断跑零次,所以结果是 never。这不是编译器里的特例,只是根本没有东西可以分发。而{" "}
boolean 也不是单个类型,它是{" "}
- true | false,所以分布式条件类型会对它跑两次。
- 记住这两点,条件类型大部分「怪结果」就不怪了。
+ true | false,所以分布式条件类型会对它跑两次。记住这两点,条件类型大部分「怪结果」就不怪了。
>
}
/>
@@ -1104,8 +1085,7 @@ export default function TypeMagicPage() {
Promise<某个类型> 的样子, 就把那个类型记作{" "}
U 并返回 U。infer 在{" "}
extends{" "}
- 后面的模式里声明一个类型变量,由编译器在匹配时填上。
- 相当于对类型做解构。这个变量只能在真分支里使用。
+ 后面的模式里声明一个类型变量,由编译器在匹配时填上。相当于对类型做解构。这个变量只能在真分支里使用。
>
),
}}
@@ -1133,8 +1113,7 @@ export default function TypeMagicPage() {
<>
第 7 行就是上一章 ReturnType 的核心:如果{" "}
T 是函数,就把返回值类型抓出来。TypeScript
- 还允许给这个变量加约束(infer U extends …),
- 本章不用它。
+ 还允许给这个变量加约束(infer U extends …),本章不用它。
>
),
}}
@@ -1219,10 +1198,8 @@ export default function TypeMagicPage() {
zh={
<>
写成 {"{ [K in keyof T]: … }"}{" "}
- 这个形式时,映射类型做的不只是造一个新对象类型。
- 它会保留源类型的 ? 和 readonly
- (只要你没去改它们),而且数组进去还是数组,元组进去还是元组。
- 这样的映射类型叫同态(homomorphic)映射类型。 这也正是{" "}
+ 这个形式时,映射类型做的不只是造一个新对象类型。它会保留源类型的 ? 和 readonly
+ (只要你没去改它们),而且数组进去还是数组,元组进去还是元组。这样的映射类型叫同态(homomorphic)映射类型。这也正是{" "}
-? 和 -readonly{" "}
必须存在的原因:修饰符默认会被照抄,否则就没办法去掉它们。
>
@@ -1426,8 +1403,7 @@ export default function TypeMagicPage() {
这就是 Pick 拼错键会报错、Omit{" "}
不报的原因
- :Omit 的键约束是 keyof any。
- 上一章留下的问题,到这里有答案了。
+ :Omit 的键约束是 keyof any。上一章留下的问题,到这里有答案了。
>
}
/>
@@ -1545,16 +1521,11 @@ export default function TypeMagicPage() {
}
zh={
<>
- 读懂一个定义和写出一个定义是两回事。下面的窗口里放着三个占位定义和一个判分器,
- 判分器本身只有两行类型代码。
- {"Expect"} 是一个类型参数,
- 它的约束就是 true 这一个类型,所以塞给它{" "}
+ 读懂一个定义和写出一个定义是两回事。下面的窗口里放着三个占位定义和一个判分器,判分器本身只有两行类型代码。
+ {"Expect"} 是一个类型参数,它的约束就是 true 这一个类型,所以塞给它{" "}
false,和任何一次约束违反没有区别。
Equal<X, Y> 拿两个只有 X 和{" "}
- Y 不同的泛型函数类型作比较;
- 编译器只有在两个延迟求值的条件类型「出身相同」时才认为它们相关,
- 所以这个判定比 X extends Y 严格。
- 能用它,不需要能推导它 —— 它是类型练习里的通用判分器。
+ Y 不同的泛型函数类型作比较;编译器只有在两个延迟求值的条件类型「出身相同」时才认为它们相关,所以这个判定比 X extends Y 严格。能用它,不需要能推导它 —— 它是类型练习里的通用判分器。
>
}
/>
@@ -1601,14 +1572,11 @@ export default function TypeMagicPage() {
TS2344: Type 'false' does not satisfy the constraint
'true'.
{" "}
- 改对一个,对应那条报错自己消失。清到没有报错,
- 就意味着你从零写出了 Partial、Pick、
+ 改对一个,对应那条报错自己消失。清到没有报错,就意味着你从零写出了 Partial、Pick、
Exclude,而且每一个都和标准库的版本核对过 ——
这句话是编译器验的,不是我说的。那些差一点的写法也值得试:
- {"{ [K in keyof T]: T[K] }"} 少一个问号,
- 或者把 MyPick 写成{" "}
- {"{ [P in keyof T]: T[P] }"}。两个都像对的,
- 两个都不对,判分器当场就说。
+ {"{ [K in keyof T]: T[K] }"} 少一个问号,或者把 MyPick 写成{" "}
+ {"{ [P in keyof T]: T[P] }"}。两个都像对的,两个都不对,判分器当场就说。
>
}
/>
@@ -1641,10 +1609,8 @@ export default function TypeMagicPage() {
五个工具类型全部亲手写过,而且你的版本和标准库几乎一字不差。
零件只有这么几个,剩下的是组合。在 VS Code 里按住 Cmd 或
Ctrl 点一下 Partial,跳进{" "}
- lib.es5.d.ts:上一章还读不下去的地方,
- 现在就是普通代码。内置类型不够用的时候(深层{" "}
- Readonly、严格的 Omit),
- 你已经能自己写 —— 下面的动手任务里就有一个。
+ lib.es5.d.ts:上一章还读不下去的地方,现在就是普通代码。内置类型不够用的时候(深层{" "}
+ Readonly、严格的 Omit),你已经能自己写 —— 下面的动手任务里就有一个。
>
}
/>
@@ -1677,10 +1643,8 @@ export default function TypeMagicPage() {
zh={
<>
类型层面的代码和普通代码守同一条规矩:
- 一眼能读懂的,别写成三层嵌套的条件类型。 一个{" "}
- interface 说得清的事,不用上条件类型。
- 同事悬停十秒还看不懂的类型是成本,不是成绩。
- 这条线画在哪,终章「类型思维」会再谈。
+ 一眼能读懂的,别写成三层嵌套的条件类型。一个{" "}
+ interface 说得清的事,不用上条件类型。同事悬停十秒还看不懂的类型是成本,不是成绩。这条线画在哪,终章「类型思维」会再谈。
>
}
/>
@@ -1726,8 +1690,7 @@ export default function TypeMagicPage() {
),
zh: (
<>
- 类型自己是一门小语言:输入类型、输出类型,只在编译时运行,
- 之后被去掉。值层面的操作大多在类型层面有个对应写法。
+ 类型自己是一门小语言:输入类型、输出类型,只在编译时运行,之后被去掉。值层面的操作大多在类型层面有个对应写法。
>
),
},
@@ -1769,8 +1732,7 @@ export default function TypeMagicPage() {
T extends U ? X : Y 是类型世界的三元表达式。
只有被判断的类型是裸类型参数时才会对联合分发:
A | B 会变成{" "}
- (A extends U ? X : Y) | (B extends U ? X : Y)。
- 变成 never 的成员会消失,因为 never{" "}
+ (A extends U ? X : Y) | (B extends U ? X : Y)。变成 never 的成员会消失,因为 never{" "}
就是空联合。Exclude 全部内容就是这些。
>
),
@@ -1787,8 +1749,7 @@ export default function TypeMagicPage() {
),
zh: (
<>
- 两边都包起来 —— [T] extends [U] —— 就关掉分发。
- 两个推论:对 never 分发没有成员可判,结果是{" "}
+ 两边都包起来 —— [T] extends [U] —— 就关掉分发。两个推论:对 never 分发没有成员可判,结果是{" "}
never;boolean 是{" "}
true | false,所以会分发两次。
>
@@ -1809,8 +1770,7 @@ export default function TypeMagicPage() {
zh: (
<>
infer 从匹配到的形状里抓出类型。映射类型{" "}
- {"{ [K in keyof T]: … }"} 逐键重建类型,
- 照抄它没改的修饰符,并支持 ?、-?、
+ {"{ [K in keyof T]: … }"} 逐键重建类型,照抄它没改的修饰符,并支持 ?、-?、
readonly、-readonly
。配合模板字面量类型 的
as
@@ -1830,8 +1790,7 @@ export default function TypeMagicPage() {
),
zh: (
<>
- 上一章那五个工具类型你已经全部亲手写过。
- 随之而来的是克制:可读性优先于聪明, 而递归类型仍然有深度上限(
+ 上一章那五个工具类型你已经全部亲手写过。随之而来的是克制:可读性优先于聪明, 而递归类型仍然有深度上限(
ts(2589))。
>
),
diff --git a/app/type-magic/viz.tsx b/app/type-magic/viz.tsx
index 4cdc080..b8f940e 100644
--- a/app/type-magic/viz.tsx
+++ b/app/type-magic/viz.tsx
@@ -85,8 +85,7 @@ export function TmDistribute() {
<>
规则是这样的:条件类型会逐个成员判断, 但前提是{" "}
extends 左边是一个裸类型参数 —— 也就是光秃秃的{" "}
- T,这里正是如此。
- 所以四个成员在左边排好队,一个一个过闸。
+ T,这里正是如此。所以四个成员在左边排好队,一个一个过闸。
>
}
/>
@@ -108,8 +107,7 @@ export function TmDistribute() {
<>
"{m}" extends{" "}
"cancelled" 成立,所以这个成员变成{" "}
- never,被丢弃。never 是空联合,
- 合并结果时它什么也不贡献。
+ never,被丢弃。never 是空联合,合并结果时它什么也不贡献。
>
}
/>
@@ -126,8 +124,7 @@ export function TmDistribute() {
zh={
<>
"{m}" 不能赋给{" "}
- "cancelled",判断为假,走假分支。
- 假分支写的是 T,所以这个成员原样保留。
+ "cancelled",判断为假,走假分支。假分支写的是 T,所以这个成员原样保留。
>
}
/>
@@ -172,8 +169,7 @@ export function TmDistribute() {
}
zh={
<>
- 分发逐帧看:MyExclude<OrderStatus, "cancelled">
- 是怎么算出来的
+ 分发逐帧看:MyExclude<OrderStatus, "cancelled">是怎么算出来的
>
}
/>
@@ -327,7 +323,7 @@ const TM_RULES: {
}
zh={
<>
- 每个键都照抄一遍,只在冒号前加上 ?。 这就是{" "}
+ 每个键都照抄一遍,只在冒号前加上 ?。这就是{" "}
Partial 的全部定义。
>
}
@@ -353,8 +349,8 @@ const TM_RULES: {
}
zh={
<>
- 修饰符前面加个减号就是去掉它,于是 ? 被摘掉。 这就是{" "}
- Required。注意这一次进来的键带着 ?。 在{" "}
+ 修饰符前面加个减号就是去掉它,于是 ? 被摘掉。这就是{" "}
+ Required。注意这一次进来的键带着 ?。在{" "}
strictNullChecks 下,-?{" "}
同时会把属性类型里的 undefined 去掉。
>
@@ -379,7 +375,7 @@ const TM_RULES: {
}
zh={
<>
- readonly 写在方括号前面。这就是 Readonly。 同理,
+ readonly 写在方括号前面。这就是 Readonly。同理,
-readonly 可以把它去掉。
>
}
@@ -405,8 +401,7 @@ const TM_RULES: {
}
zh={
<>
- as 把键名整个换掉:模板字面量类型拼出新名字,
- 属性类型改成回调。一个对象类型就变成了配套的事件处理器类型。
+ as 把键名整个换掉:模板字面量类型拼出新名字,属性类型改成回调。一个对象类型就变成了配套的事件处理器类型。
as 和模板字面量类型都是 TypeScript 4.1 加入的。
>
}
diff --git a/app/types/page.tsx b/app/types/page.tsx
index 797b5e7..e454ad9 100644
--- a/app/types/page.tsx
+++ b/app/types/page.tsx
@@ -370,9 +370,7 @@ export default function TypesPage() {
),
zh: (
<>
- string、number、boolean…… 类型没那么神秘,
- 就是每个值随身带的一张标签。更省事的是:
- 大多数标签不用你写 —— TypeScript
+ string、number、boolean…… 类型没那么神秘,就是每个值随身带的一张标签。更省事的是:大多数标签不用你写 —— TypeScript
看一眼值,自己就填好了。这手本事叫推断。
>
),
@@ -440,21 +438,18 @@ export default function TypesPage() {
zh={
<>
- 你是店长。进货间堆着几十个箱子,每个箱子上贴着标签:
- 「珍珠 · 袋装 · 5kg」「椰浆 · 罐装 · 易腐」。
+ 你是店长。进货间堆着几十个箱子,每个箱子上贴着标签:「珍珠 · 袋装 · 5kg」「椰浆 · 罐装 · 易腐」。
不用开箱,你就知道每箱能干什么、不能干什么。
TypeScript 给程序里的每个值也贴这么一张标签,学名叫
类型(type):name 这一栏是 string,price 这一栏是
- number。之后你每次用这个值,编译器都先看一眼标签:
- 拿 string 去做乘法?标签不允许,报错。
+ number。之后你每次用这个值,编译器都先看一眼标签:拿 string 去做乘法?标签不允许,报错。
大多数标签你从来不用写。写下 let price = 22,TS
看一眼右边的 22 就把标签填好了 —— 这叫
- 推断(inference)。只有少数地方需要你亲手写,
- 也就是注解(annotation),§04 讲清楚是哪几处。
+ 推断(inference)。只有少数地方需要你亲手写,也就是注解(annotation),§04 讲清楚是哪几处。
>
}
@@ -479,8 +474,7 @@ export default function TypesPage() {
}
zh={
<>
- 程序里真实存在的东西:22、"Oolong Tea"、
- 一个订单对象。运行时真正干活的是它们。
+ 程序里真实存在的东西:22、"Oolong Tea"、一个订单对象。运行时真正干活的是它们。
>
}
/>
@@ -507,8 +501,7 @@ export default function TypesPage() {
}
zh={
<>
- 对值的描述:是什么、有哪些字段。类型只在编译期存在。
- 编译器会把所有注解、type、
+ 对值的描述:是什么、有哪些字段。类型只在编译期存在。编译器会把所有注解、type、
interface 全部删掉,产出普通 JavaScript ——
所以运行时没有任何类型检查,除非那段检查是你自己写的。
>
@@ -534,8 +527,7 @@ export default function TypesPage() {
}
zh={
<>
- 它负责填标签(推断)和读标签(检查)。
- 发现某个操作和标签对不上,就在代码运行之前报错。
+ 它负责填标签(推断)和读标签(检查)。发现某个操作和标签对不上,就在代码运行之前报错。
>
}
/>
@@ -589,17 +581,13 @@ export default function TypesPage() {
zh={
<>
- 日常写码 95% 的时间在跟 string、number、boolean、null、
- undefined 打交道。null 和 undefined 的区别值得记一句:
- undefined 是「从来没人赋过值」时你拿到的东西(没写初始值、
- 属性不存在、函数没有 return);null 是有人特意赋进去的、
- 表示「空」的值。
+ 日常写码 95% 的时间在跟 string、number、boolean、null、undefined 打交道。null 和 undefined 的区别值得记一句:
+ undefined 是「从来没人赋过值」时你拿到的东西(没写初始值、属性不存在、函数没有 return);null 是有人特意赋进去的、表示「空」的值。
它们之所以是两个独立的类型,是因为{" "}
strictNullChecks 开着。这个开关一关,
- null 和 undefined 可以赋给任何类型,这一整类错误编译器就不再管了。
- 开关本身第 10 章细讲。
+ null 和 undefined 可以赋给任何类型,这一整类错误编译器就不再管了。开关本身第 10 章细讲。
>
}
@@ -637,8 +625,7 @@ export default function TypesPage() {
zh={
<>
string[] 读作「string 的数组」。
- Array<string> 是完全相同的意思。
- 元素类型简单时,社区习惯用前者,因为短。
+ Array<string> 是完全相同的意思。元素类型简单时,社区习惯用前者,因为短。
>
}
/>
@@ -672,14 +659,11 @@ export default function TypesPage() {
zh={
<>
number[] 这样的数组类型完全不说明元素有几个;
- [string, number] 这样的元组则明确说「就两个」,
- 并且规定每个位置是什么。有一个反直觉的点:
+ [string, number] 这样的元组则明确说「就两个」,并且规定每个位置是什么。有一个反直觉的点:
元组上的 push 依然合法
- ,因为元组在运行时仍然是个普通数组。
- 固定长度只在构造这个值和用下标取值时检查,后续修改不查。
- 想连修改一起禁掉,给它加 readonly。
+ ,因为元组在运行时仍然是个普通数组。固定长度只在构造这个值和用下标取值时检查,后续修改不查。想连修改一起禁掉,给它加 readonly。
>
}
/>
@@ -708,8 +692,7 @@ export default function TypesPage() {
zh={
<>
{"{ name: string; price: number }"}{" "}
- 本身就是一个类型,直接写在原地,叫对象类型字面量,
- 属性之间用分号隔开。形状能嵌套、能进数组 ——
+ 本身就是一个类型,直接写在原地,叫对象类型字面量,属性之间用分号隔开。形状能嵌套、能进数组 ——
真实项目的数据几乎都是这样描述出来的。可选属性{" "}
?、interface 和 type{" "}
别名,是下一章的全部内容。
@@ -758,13 +741,8 @@ export default function TypesPage() {
}
zh={
<>
- 上面那台放大镜里的每个类型,都是为了讲解手写进去的。
- 下面这个窗口里跑的是真正的 TypeScript 编译器,
- 就在你的浏览器里。点 drink、price、
- menu、cheap,
- 给出答案的和你编辑器里那台是同一个程序。
- 然后看它画波浪线的那一行:totalFor 的参数。
- 推断读的是值,而参数没有值可读。
+ 上面那台放大镜里的每个类型,都是为了讲解手写进去的。下面这个窗口里跑的是真正的 TypeScript 编译器,就在你的浏览器里。点 drink、price、
+ menu、cheap,给出答案的和你编辑器里那台是同一个程序。然后看它画波浪线的那一行:totalFor 的参数。推断读的是值,而参数没有值可读。
>
}
/>
@@ -798,14 +776,8 @@ export default function TypesPage() {
}
zh={
<>
- 那个窗口的第二个页签值得打开:
- 它是编译器把这份文件里所有推断结果一次列全的清单。
- 读最后一行 ——
- declare function totalFor(qty: any): number。
- 参数推成了 any,这是编译器在说「这一处我放弃了」。
- 在文件末尾补一句 totalFor("three"),
- 不会多出任何报错。any 的代价是本章 §06 的内容;
- 第二个页签里那种文件是干什么用的,09 章讲。
+ 那个窗口的第二个页签值得打开:它是编译器把这份文件里所有推断结果一次列全的清单。读最后一行 ——
+ declare function totalFor(qty: any): number。参数推成了 any,这是编译器在说「这一处我放弃了」。在文件末尾补一句 totalFor("three"),不会多出任何报错。any 的代价是本章 §06 的内容;第二个页签里那种文件是干什么用的,09 章讲。
>
}
/>
@@ -824,8 +796,7 @@ export default function TypesPage() {
}
zh={
<>
- 规律就在这里。局部变量这种「声明的同时就赋值」的场合,
- 推断全包了。那注解该写在哪?写在边界上 ——
+ 规律就在这里。局部变量这种「声明的同时就赋值」的场合,推断全包了。那注解该写在哪?写在边界上 ——
凡是别的代码要依赖这个类型的地方,以及推断无从下手的地方。
>
}
@@ -856,8 +827,7 @@ export default function TypesPage() {
zh={
<>
参数是外面塞进来的,推断没有材料;开着{" "}
- noImplicitAny 时必须写。
- 返回值可以推断,但写出来等于把承诺讲明白 ——
+ noImplicitAny 时必须写。返回值可以推断,但写出来等于把承诺讲明白 ——
对外导出的函数建议写。
>
}
@@ -882,8 +852,7 @@ export default function TypesPage() {
}
zh={
<>
- 导出的常量、全店共用的菜单结构 —— 给它们一个具名类型。
- 这份类型就是契约,谁塞错东西,就在那一行报错。
+ 导出的常量、全店共用的菜单结构 —— 给它们一个具名类型。这份类型就是契约,谁塞错东西,就在那一行报错。
>
}
/>
@@ -937,8 +906,7 @@ export default function TypesPage() {
zh={
<>
注解是承诺,推断是观察。
- 观察已经对了的地方再加一个冒号,不会让代码更安全,
- 只会让真正的契约更难被看见。§08 还会说到它。
+ 观察已经对了的地方再加一个冒号,不会让代码更安全,只会让真正的契约更难被看见。§08 还会说到它。
>
}
/>
@@ -978,13 +946,10 @@ export default function TypesPage() {
zh={
<>
类型可以很宽(「是个字符串」),也可以很窄(「就是{" "}
- "small" 这个字符串」)。
- 窄的那种叫字面量类型(literal type)。
- 拿到哪一种,取决于变量是怎么声明的:let{" "}
+ "small" 这个字符串」)。窄的那种叫字面量类型(literal type)。拿到哪一种,取决于变量是怎么声明的:let{" "}
会把字面量放宽成 string,这个动作叫拓宽(widening);
const 则保留字面量。道理很直白:
- let 变量本来就是要重新赋值的,
- 类型只允许一个值就没法用了。
+ let 变量本来就是要重新赋值的,类型只允许一个值就没法用了。
>
}
/>
@@ -1016,9 +981,7 @@ export default function TypesPage() {
"small" | "medium" | "large"
{" "}
- 读作「三者之一」,这就是字面量联合:
- 把「只能取这几个值」写进类型本身,
- 错杯型在你保存文件那一刻就被拦下。竖线 |{" "}
+ 读作「三者之一」,这就是字面量联合:把「只能取这几个值」写进类型本身,错杯型在你保存文件那一刻就被拦下。竖线 |{" "}
是第 03 章的主角。
>
}
@@ -1037,9 +1000,7 @@ export default function TypesPage() {
}
zh={
<>
- 拓宽这件事,与其读结论,不如直接问编译器。
- 下面一共三份稿子:现在这份,加标题栏里的另外两份。
- 每换一份,都点一下 cupSize,看它回什么。
+ 拓宽这件事,与其读结论,不如直接问编译器。下面一共三份稿子:现在这份,加标题栏里的另外两份。每换一份,都点一下 cupSize,看它回什么。
>
}
/>
@@ -1088,8 +1049,7 @@ export default function TypesPage() {
Type '"mega"' is not assignable to type
'Size'.
{" "}
- 值一个字都没变,变的是类型收得多窄。
- 中间那份稿子里可点的名字有三个,而只有第一个保住了字面量类型 ——
+ 值一个字都没变,变的是类型收得多窄。中间那份稿子里可点的名字有三个,而只有第一个保住了字面量类型 ——
原因在下面这张卡片里。
>
}
@@ -1168,10 +1128,7 @@ export default function TypesPage() {
}
zh={
<>
- 用 string 存杯型,"mega"、"Large"、
- "LARGE" 全都合法,写错的代价照旧发生在运行时。
- 用字面量联合,名单之外的值编译期直接拒收。
- 合法值的范围收得越窄,编译器能替你挡的就越多 ——
+ 用 string 存杯型,"mega"、"Large"、"LARGE" 全都合法,写错的代价照旧发生在运行时。用字面量联合,名单之外的值编译期直接拒收。合法值的范围收得越窄,编译器能替你挡的就越多 ——
这是贯穿全书的思路,03 章开始大量使用。
>
}
@@ -1212,9 +1169,7 @@ export default function TypesPage() {
zh={
<>
两行高亮都藏着真错误:键名 prise{" "}
- 拼错了,没人报;然后 total 也成了 any,
- 于是拿一个数字调 toUpperCase() 同样被放行。
- 两个错都活到了运行时 —— 和裸写 JavaScript 一模一样。
+ 拼错了,没人报;然后 total 也成了 any,于是拿一个数字调 toUpperCase() 同样被放行。两个错都活到了运行时 —— 和裸写 JavaScript 一模一样。
>
}
/>
@@ -1255,22 +1210,16 @@ export default function TypesPage() {
zh={
<>
- 一,关掉检查。值一旦是 any,
- 对它做任何操作编译器都放行:属性拼错、方法不存在、参数传错,
- 全部沉默。
+ 一,关掉检查。值一旦是 any,对它做任何操作编译器都放行:属性拼错、方法不存在、参数传错,全部沉默。
二,会扩散。从 any
- 值上取的属性、由它算出的结果、把它传进去的回调参数,
- 统统变成 any。一个 any 能让一整条数据流失去检查。
+ 值上取的属性、由它算出的结果、把它传进去的回调参数,统统变成 any。一个 any 能让一整条数据流失去检查。
这不等于永远不能用。迁移老 JavaScript
项目时它有用,偶尔也可以当临时出口(终章专门讨论「何时 any
- 合理」)。原则是:有意识地用,并且清楚自己关掉了什么。
- 另外还有一个安全的替代品 unknown:
- 它接受任何值,但在你检查清楚它到底是什么之前,
- 几乎什么都不让你做。03 章细讲。
+ 合理」)。原则是:有意识地用,并且清楚自己关掉了什么。另外还有一个安全的替代品 unknown:它接受任何值,但在你检查清楚它到底是什么之前,几乎什么都不让你做。03 章细讲。
>
}
@@ -1294,9 +1243,7 @@ export default function TypesPage() {
还有一种你没写、自己长出来的 any ——
最常见的来源是没标类型的函数参数。strict 家族里的{" "}
noImplicitAny{" "}
- 专门管这个:凡是类型悄悄退化成 any 的地方一律报错。
- TypeScript Playground 默认开着 strict,
- 所以你在动手任务里会亲眼见到它工作。
+ 专门管这个:凡是类型悄悄退化成 any 的地方一律报错。TypeScript Playground 默认开着 strict,所以你在动手任务里会亲眼见到它工作。
>
}
/>
@@ -1368,8 +1315,7 @@ export default function TypesPage() {
zh={
<>
推断照单全收,prise{" "}
- 被当成一个真实字段记了下来。
- 在 filter 用到 price{" "}
+ 被当成一个真实字段记了下来。在 filter 用到 price{" "}
之前什么都不报,而报错时指向的是
并没有写错的那段代码。
>
@@ -1401,9 +1347,7 @@ export default function TypesPage() {
zh={
<>
立了 MenuItem 这份契约之后,错误
- 精确落在写错的那一行,还附带改法。
- 这就是「在边界上注解」的回报:
- 报错的位置就是犯错的位置。
+ 精确落在写错的那一行,还附带改法。这就是「在边界上注解」的回报:报错的位置就是犯错的位置。
>
}
/>
@@ -1474,8 +1418,7 @@ export default function TypesPage() {
<>
大写的 String 是 new String(){" "}
造出来的那个包装对象的类型,不是原始字符串。
- String 不能赋给 string,
- 写了它就会遇到一串莫名其妙的兼容错误。
+ String 不能赋给 string,写了它就会遇到一串莫名其妙的兼容错误。
注解一律用小写:string、number、boolean。大写的{" "}
String / Number /{" "}
Boolean,当它们不存在。
@@ -1504,9 +1447,7 @@ export default function TypesPage() {
zh={
<>
let count: number = 0{" "}
- 里的注解,只是把编译器从 0 那里已经读到的信息又说了一遍。
- 它占地方,还会把真正重要的注解淹没掉。
- 显而易见的地方交给推断,注解留给 §04 说的三种边界 ——
+ 里的注解,只是把编译器从 0 那里已经读到的信息又说了一遍。它占地方,还会把真正重要的注解淹没掉。显而易见的地方交给推断,注解留给 §04 说的三种边界 ——
这样读代码的人一眼就能看出契约在哪。
>
}
@@ -1536,9 +1477,7 @@ export default function TypesPage() {
<>
空数组没有元素,推断不出元素类型,TS 只能先记成{" "}
any[]。它接着会尝试从同一作用域后面的{" "}
- push 反推类型,这叫「演化数组」。
- 但只要这个值离开函数,或者在填进内容之前就被使用,
- 这套猜测就失效了,noImplicitAny 会当场报错。
+ push 反推类型,这叫「演化数组」。但只要这个值离开函数,或者在填进内容之前就被使用,这套猜测就失效了,noImplicitAny 会当场报错。
好习惯:空箱子出生就贴标签。
>
}
@@ -1587,8 +1526,7 @@ export default function TypesPage() {
Variable 'items' implicitly has an
'any[]' type
- 。注意顶层那行 toppings 完全不报错,
- 它的类型就是 any[]。一行注解{" "}
+ 。注意顶层那行 toppings 完全不报错,它的类型就是 any[]。一行注解{" "}
const safe: string[] = []{" "}
就能解决全部问题,之后每一次 push 都受检。
>
@@ -1637,9 +1575,7 @@ export default function TypesPage() {
),
zh: (
<>
- 类型就是值随身带的标签。推断 = 编译器替你读这张标签,
- 所以大多数局部变量一个冒号都不用写。
- 类型在编译时被删除:程序运行时没有任何东西再检查它们。
+ 类型就是值随身带的标签。推断 = 编译器替你读这张标签,所以大多数局部变量一个冒号都不用写。类型在编译时被删除:程序运行时没有任何东西再检查它们。
>
),
},
@@ -1694,9 +1630,7 @@ export default function TypesPage() {
<>
let 拓宽("small" →
string),const 保留字面量类型。但{" "}
- const 锁不住内容:
- 数组元素和对象属性照样拓宽,除非写 as const。
- 字面量联合("small" | "medium" |
+ const 锁不住内容:数组元素和对象属性照样拓宽,除非写 as const。字面量联合("small" | "medium" |
"large")是 03 章的入口。
>
),
@@ -1714,8 +1648,7 @@ export default function TypesPage() {
zh: (
<>
any 关掉检查,并顺着表达式扩散;空的{" "}
- [] 会先落成 any[]。
- 两者当临时手段可以,长期留着不行。noImplicitAny{" "}
+ [] 会先落成 any[]。两者当临时手段可以,长期留着不行。noImplicitAny{" "}
负责报出那些你没亲手写的 any。
>
),
diff --git a/app/types/viz.tsx b/app/types/viz.tsx
index ada9e81..733a610 100644
--- a/app/types/viz.tsx
+++ b/app/types/viz.tsx
@@ -96,8 +96,7 @@ const ID_ENTRIES: IdEntry[] = [
}
zh={
<>
- JS/TS 只有一种数字类型:22、19.5、-3 都是 number。
- 别的语言里的 int / float 之分,这里没有。
+ JS/TS 只有一种数字类型:22、19.5、-3 都是 number。别的语言里的 int / float 之分,这里没有。
>
}
/>
@@ -117,9 +116,7 @@ const ID_ENTRIES: IdEntry[] = [
}
zh={
<>
- NaN 的意思是「不是数」,可它的类型偏偏是 number。
- 所以类型系统拦不住 NaN —— 要拦,得去查产生它的那个值,
- 通常是某个 undefined。
+ NaN 的意思是「不是数」,可它的类型偏偏是 number。所以类型系统拦不住 NaN —— 要拦,得去查产生它的那个值,通常是某个 undefined。
>
}
/>
@@ -154,8 +151,7 @@ const ID_ENTRIES: IdEntry[] = [
}
zh={
<>
- 「这里空着」,而且是有意为之的空。
- null 不会自己出现,一定是某段代码把它赋了进去 ——
+ 「这里空着」,而且是有意为之的空。null 不会自己出现,一定是某段代码把它赋了进去 ——
通常表示「这个字段存在,但故意留空」。
>
}
@@ -195,8 +191,7 @@ const ID_ENTRIES: IdEntry[] = [
}
zh={
<>
- 「从来没人往这儿放过值」。声明了没赋值、读一个不存在的属性、
- 函数没有 return —— 拿到的都是它。
+ 「从来没人往这儿放过值」。声明了没赋值、读一个不存在的属性、函数没有 return —— 拿到的都是它。
>
}
/>
@@ -213,8 +208,7 @@ const ID_ENTRIES: IdEntry[] = [
}
zh={
<>
- null 和 undefined 是两个不同的值,含义也不同。
- 它们之所以是两个独立的类型,是因为 strictNullChecks{" "}
+ null 和 undefined 是两个不同的值,含义也不同。它们之所以是两个独立的类型,是因为 strictNullChecks{" "}
开着;这个开关一关,两者可以赋给任何类型,编译器也就不再提醒你了。
>
}
@@ -234,8 +228,7 @@ const ID_ENTRIES: IdEntry[] = [
}
zh={
<>
- 大到 number 存不准的整数,写法是在数字后面加一个 n。
- 日常业务代码几乎用不上,知道有这户人家就行。
+ 大到 number 存不准的整数,写法是在数字后面加一个 n。日常业务代码几乎用不上,知道有这户人家就行。
>
}
/>
@@ -255,8 +248,7 @@ const ID_ENTRIES: IdEntry[] = [
}
zh={
<>
- 保证和其他任何值都不相等的值,常用来做「绝不会撞名」的对象键。
- 日常业务代码里也少见,遇到再回来查。
+ 保证和其他任何值都不相等的值,常用来做「绝不会撞名」的对象键。日常业务代码里也少见,遇到再回来查。
>
}
/>
@@ -349,8 +341,7 @@ const LENS_LINES: LensLine[] = [
}
zh={
<>
- let 声明的变量以后还能改,所以 TS 往宽了记:
- 「反正是个字符串」。从字面量类型放宽到 string,这个动作叫
+ let 声明的变量以后还能改,所以 TS 往宽了记:「反正是个字符串」。从字面量类型放宽到 string,这个动作叫
拓宽(widening)。
>
}
@@ -368,8 +359,7 @@ const LENS_LINES: LensLine[] = [
}
zh={
<>
- const 变量不会再被赋值,TS 就敢把话说死:
- 类型就是字面量 "Grape Tea" 本身 ——
+ const 变量不会再被赋值,TS 就敢把话说死:类型就是字面量 "Grape Tea" 本身 ——
比 string 窄得多,这叫字面量类型。
>
}
@@ -392,8 +382,7 @@ const LENS_LINES: LensLine[] = [
}
zh={
<>
- 数字同理:初始值是 22,但以后可能改成 19.5,所以记成 number。
- 注意你一个冒号都没写。
+ 数字同理:初始值是 22,但以后可能改成 19.5,所以记成 number。注意你一个冒号都没写。
>
}
/>
@@ -494,9 +483,7 @@ const LENS_LINES: LensLine[] = [
zh={
<>
这一行常让人意外:const 版依旧是 string[]。
- const 只保证「变量名不能重新赋值」,
- 数组内容照样能 push、能改,所以元素类型必须留宽。
- 想连内容一起锁死,要用 as const(终章讲)。
+ const 只保证「变量名不能重新赋值」,数组内容照样能 push、能改,所以元素类型必须留宽。想连内容一起锁死,要用 as const(终章讲)。
>
}
/>
@@ -524,8 +511,7 @@ const LENS_LINES: LensLine[] = [
}
zh={
<>
- 对象:TS 把每个属性的类型都推出来,拼成一份「形状」。
- 后面的章节反复出现的就是这份形状。
+ 对象:TS 把每个属性的类型都推出来,拼成一份「形状」。后面的章节反复出现的就是这份形状。
>
}
/>
@@ -587,8 +573,7 @@ const LENS_LINES: LensLine[] = [
}
zh={
<>
- const 版在赋值这一步就被拦下了,还轮不到类型出场。
- 这是 JavaScript 自己的规矩,TS 只是把它提前到编译期告诉你。
+ const 版在赋值这一步就被拦下了,还轮不到类型出场。这是 JavaScript 自己的规矩,TS 只是把它提前到编译期告诉你。
>
}
/>
@@ -616,8 +601,7 @@ const LENS_LINES: LensLine[] = [
zh={
<>
这一行是整节的重点:你一个类型注解都没写,TS
- 照样逮住「拿字符串当数字用」。
- 推断出来的类型,和你手写的一样严格。
+ 照样逮住「拿字符串当数字用」。推断出来的类型,和你手写的一样严格。
>
}
/>
diff --git a/app/utility/page.tsx b/app/utility/page.tsx
index 1b1e80d..10e7bd9 100644
--- a/app/utility/page.tsx
+++ b/app/utility/page.tsx
@@ -494,9 +494,7 @@ export default function UtilityPage() {
),
zh: (
<>
- 工具类型接受一个你已经有的类型,返回一个与它相关的新类型。
- TypeScript 自带一套标准工具,新类型不必手写。
- 这一章讲怎么用它们,第 07 章讲它们是怎么造出来的。
+ 工具类型接受一个你已经有的类型,返回一个与它相关的新类型。TypeScript 自带一套标准工具,新类型不必手写。这一章讲怎么用它们,第 07 章讲它们是怎么造出来的。
>
),
}}
@@ -579,15 +577,11 @@ export default function UtilityPage() {
zh={
<>
- 草稿单:顾客还在挑,属性可以缺。锁定单:小票已经打出来,
- 一个字都不许改。列表行:只显示三个属性。对外接口:
- 内部备注绝不能出去。库存表:每个杯型对一个数字。
- 五个需求,五种类型。
+ 草稿单:顾客还在挑,属性可以缺。锁定单:小票已经打出来,一个字都不许改。列表行:只显示三个属性。对外接口:内部备注绝不能出去。库存表:每个杯型对一个数字。五个需求,五种类型。
你当然可以照着 Order 把五个都手写一遍 ——
- 六个属性抄五遍。然后 Order 加了一个属性,
- 五个地方都得改。漏掉一处,这些类型之间就悄悄不一致了。
+ 六个属性抄五遍。然后 Order 加了一个属性,五个地方都得改。漏掉一处,这些类型之间就悄悄不一致了。
>
}
@@ -614,8 +608,7 @@ export default function UtilityPage() {
zh={
<>
这就是第 01 章那个奶茶店类型;§05 会再把第 03
- 章的订单状态联合加进来。把这份定义贴进 Playground,
- 本章后面的例子都能接着它跑。
+ 章的订单状态联合加进来。把这份定义贴进 Playground,本章后面的例子都能接着它跑。
>
}
/>
@@ -639,11 +632,9 @@ export default function UtilityPage() {
zh={
<>
TypeScript 的解法是随语言自带一批工具类型(utility
- types)。每一个都接受一个已有的类型,产出一个新类型。
- 写法看起来像函数调用,只是圆括号换成了尖括号:
+ types)。每一个都接受一个已有的类型,产出一个新类型。写法看起来像函数调用,只是圆括号换成了尖括号:
Partial<Order>。这和第 05
- 章的泛型是同一套写法,原因也一样:尖括号里的类型是一个实参。
- 上面五个需求,一个工具类型对应一个。
+ 章的泛型是同一套写法,原因也一样:尖括号里的类型是一个实参。上面五个需求,一个工具类型对应一个。
>
}
/>
@@ -719,8 +710,7 @@ export default function UtilityPage() {
}
zh={
<>
- 一行只显示订单号、饮品和杯型。这是白名单:
- 点到名的留下,其余不带走。
+ 一行只显示订单号、饮品和杯型。这是白名单:点到名的留下,其余不带走。
>
}
/>
@@ -744,8 +734,7 @@ export default function UtilityPage() {
}
zh={
<>
- internalNote 只给店员看。这是黑名单:
- 点到名的删掉,其余原样保留。
+ internalNote 只给店员看。这是黑名单:点到名的删掉,其余原样保留。
>
}
/>
@@ -773,10 +762,8 @@ export default function UtilityPage() {
}
zh={
- 工具类型不发明新东西:它读一个已有的类型,返回一个新类型。
- 它也从不改动输入 —— Partial<Order>{" "}
- 造出草稿类型之后,Order 依然是那六个必填属性。
- 本章每一个工具类型都是这样,下面的流水线可以直接看到这一点。
+ 工具类型不发明新东西:它读一个已有的类型,返回一个新类型。它也从不改动输入 —— Partial<Order>{" "}
+ 造出草稿类型之后,Order 依然是那六个必填属性。本章每一个工具类型都是这样,下面的流水线可以直接看到这一点。
}
/>
@@ -811,11 +798,8 @@ export default function UtilityPage() {
}
zh={
<>
- 注意三件事。进去的是类型,出来的也是类型,没有任何代码在运行。
- 换工具只改变输出,从不改动输入。还有:被 Pick 或{" "}
- Omit 排除掉的属性,只是在新类型里没有。
- 它在 Order 里还好好地待着,
- 用到 Order 的代码不受任何影响。
+ 注意三件事。进去的是类型,出来的也是类型,没有任何代码在运行。换工具只改变输出,从不改动输入。还有:被 Pick 或{" "}
+ Omit 排除掉的属性,只是在新类型里没有。它在 Order 里还好好地待着,用到 Order 的代码不受任何影响。
>
}
/>
@@ -861,10 +845,8 @@ export default function UtilityPage() {
zh={
<>
第 02 章手动给属性加过 ?,Partial{" "}
- 就是对每个属性都做一次。留意最后一行:
- 因为这个属性可能不存在,读它拿到的是{" "}
- string | undefined,用之前要先收窄。
- 收窄是第 03 章的内容。
+ 就是对每个属性都做一次。留意最后一行:因为这个属性可能不存在,读它拿到的是{" "}
+ string | undefined,用之前要先收窄。收窄是第 03 章的内容。
>
}
/>
@@ -902,8 +884,7 @@ export default function UtilityPage() {
就可选标记而言,Partial 和 Required{" "}
是相反的一对,但它们并不是严格互逆的。Partial{" "}
加上 ?,连带把 undefined{" "}
- 加进属性类型;Required 把两者一起去掉。
- 所以上面的 Required<A> 是{" "}
+ 加进属性类型;Required 把两者一起去掉。所以上面的 Required<A> 是{" "}
{"{ note: string }"},而不是{" "}
{"{ note: string | undefined }"}。
>
@@ -937,8 +918,7 @@ export default function UtilityPage() {
zh={
<>
最后两行是本节的重点。readonly 拒绝的是
- 给属性赋值。它对属性指向的那个对象或数组不作任何限制,
- 所以 push 依然合法。
+ 给属性赋值。它对属性指向的那个对象或数组不作任何限制,所以 push 依然合法。
>
}
/>
@@ -983,16 +963,13 @@ export default function UtilityPage() {
<>
Partial、Required、
- Readonly 只处理最外层对象的属性,
- 不会进到属性类型的内部。在{" "}
+ Readonly 只处理最外层对象的属性,不会进到属性类型的内部。在{" "}
Partial<{"{ meta: { note: string } }"}> 里,
meta 变成了可选,meta.note{" "}
仍然必填。
- Readonly 这一点值得再说一遍,因为它的名字听起来
- 比它做的事更强。它阻止的是给属性赋值,
- 并不会冻结属性指向的那个对象,而且它在运行时完全不存在 ——
+ Readonly 这一点值得再说一遍,因为它的名字听起来比它做的事更强。它阻止的是给属性赋值,并不会冻结属性指向的那个对象,而且它在运行时完全不存在 ——
Object.freeze 是另一件事,发生在运行时。
@@ -1114,17 +1091,11 @@ export default function UtilityPage() {
T 上真实存在的键。Omit<T, K>{" "}
声明的是 K extends keyof any,任何{" "}
string、number、
- symbol 都收。这个更宽的约束是官方故意的:
- 它让你可以对一个「可能存在也可能不存在」的键写{" "}
+ symbol 都收。这个更宽的约束是官方故意的:它让你可以对一个「可能存在也可能不存在」的键写{" "}
Omit。代价就是拼错不报错。
- 而这恰好在最想用 Omit 的场合最危险:
- 如果你删掉一个属性是因为它不能发给客户端,
- 那么一个拼错的键会顺利编译,属性照样在里面。
- 这种场合用 Pick 白名单,让编译器帮你核对名字;
- 或者学完第 07 章,自己写一个严格版的 Omit。
- 本节末尾的实验室里可以亲眼看这个洞漏出来:拼错的{" "}
+ 而这恰好在最想用 Omit 的场合最危险:如果你删掉一个属性是因为它不能发给客户端,那么一个拼错的键会顺利编译,属性照样在里面。这种场合用 Pick 白名单,让编译器帮你核对名字;或者学完第 07 章,自己写一个严格版的 Omit。本节末尾的实验室里可以亲眼看这个洞漏出来:拼错的{" "}
Omit 算出来的形状里,internalNote{" "}
原封不动地还在,而没有任何地方报错。
@@ -1167,13 +1138,9 @@ export default function UtilityPage() {
zh={
<>
Record<Size, number> 比{" "}
- {"{ [k: string]: number }"} 严格,
- 因为 Size 是有限的联合,
- 编译器清楚知道必须有哪几个键。键类型换成{" "}
+ {"{ [k: string]: number }"} 严格,因为 Size 是有限的联合,编译器清楚知道必须有哪几个键。键类型换成{" "}
string 之后,Record{" "}
- 就等于一个索引签名,没有名单可以核对。
- 注意最后一行:索引签名承诺每个键都有值,
- 所以读一个不存在的键能通过类型检查,运行时却拿到{" "}
+ 就等于一个索引签名,没有名单可以核对。注意最后一行:索引签名承诺每个键都有值,所以读一个不存在的键能通过类型检查,运行时却拿到{" "}
undefined。想让编译器在这里补上{" "}
| undefined,要开{" "}
noUncheckedIndexedAccess(第 10 章)。
@@ -1207,8 +1174,7 @@ export default function UtilityPage() {
这一章里每一处写着「展开成什么样」的注释,都可以一键核对。在下面的窗口里点
等号左边的名字 —— DraftOrder、
ListRow、PublicOrder、
- CupStock —— 编译器就把算完的形状整个打出来,
- 每个属性都写全。Partial 展开是{" "}
+ CupStock —— 编译器就把算完的形状整个打出来,每个属性都写全。Partial 展开是{" "}
{"{ id?: string | undefined; drink?: string | undefined; … }"}
,一眼能看到它做的两件事:加上问号,同时把{" "}
undefined 并进每个属性的类型里。
@@ -1338,14 +1304,11 @@ export default function UtilityPage() {
Partial、Pick、Omit{" "}
接受的是对象类型,处理它的属性;Exclude、
Extract、NonNullable 接受的是
- 联合类型,处理它的成员。不确定该用哪个时问自己:
- 我手上这个类型,是一组属性,还是一份候选名单?
+ 联合类型,处理它的成员。不确定该用哪个时问自己:我手上这个类型,是一组属性,还是一份候选名单?
- 这一组三个都是对联合的每个成员分别求值的,
- 所以 Exclude<A | B | C, B> 得到的是{" "}
- A | C,而不是把整个联合当成一个整体去比较。
- 第 07 章会给出产生这种行为的那一行定义。
+ 这一组三个都是对联合的每个成员分别求值的,所以 Exclude<A | B | C, B> 得到的是{" "}
+ A | C,而不是把整个联合当成一个整体去比较。第 07 章会给出产生这种行为的那一行定义。
>
}
@@ -1393,9 +1356,7 @@ export default function UtilityPage() {
注意 typeof makeOrder。makeOrder{" "}
是一个值,而 ReturnType 需要一个
类型。出现在类型位置上的 typeof{" "}
- 会取出一个值的类型,这就是两者之间的桥。
- 忘了写它是这里最常见的错误,最后三行就是那条报错。
- 这个 typeof 和 JavaScript
+ 会取出一个值的类型,这就是两者之间的桥。忘了写它是这里最常见的错误,最后三行就是那条报错。这个 typeof 和 JavaScript
的同名运算符做的不是一件事,第 07 章会讲。
>
}
@@ -1418,10 +1379,8 @@ export default function UtilityPage() {
}
zh={
<>
- 什么时候用得上?库导出了函数、却没导出结果类型的时候;
- 以及你不想专门给一个内部函数的结果起名字的时候。注意{" "}
- Parameters<T> 返回的是一个元组,
- 不是联合也不是对象,所以 MakeOrderArgs[1] 就是{" "}
+ 什么时候用得上?库导出了函数、却没导出结果类型的时候;以及你不想专门给一个内部函数的结果起名字的时候。注意{" "}
+ Parameters<T> 返回的是一个元组,不是联合也不是对象,所以 MakeOrderArgs[1] 就是{" "}
Size,这个元组也可以直接展开传进一次调用。
>
}
@@ -1497,13 +1456,11 @@ export default function UtilityPage() {
"LARGE",Lowercase{" "}
方向相反;Capitalize<"size">{" "}
得到 "Size",Uncapitalize{" "}
- 方向相反。四个都只作用在字符串字面量类型上,
- 配上第 07 章的模板字面量类型才真正有用。现在先认个脸。
+ 方向相反。四个都只作用在字符串字面量类型上,配上第 07 章的模板字面量类型才真正有用。现在先认个脸。
另外还有一个 NoInfer<T>,TypeScript 5.4
- 加进来的。它标记某个参数位置,让编译器在推断类型实参时不使用它。
- 很冷门,知道它存在就够了。
+ 加进来的。它标记某个参数位置,让编译器在推断类型实参时不使用它。很冷门,知道它存在就够了。
>
}
@@ -1544,9 +1501,7 @@ export default function UtilityPage() {
}
zh={
<>
- 从里往外读。第 2 行先挑出两个属性,再把两者都变成可选。
- 第 9 行先从状态联合里去掉两个成员,
- 再把剩下的当成 Record 的键集合。
+ 从里往外读。第 2 行先挑出两个属性,再把两者都变成可选。第 9 行先从状态联合里去掉两个成员,再把剩下的当成 Record 的键集合。
>
}
/>
@@ -1565,9 +1520,7 @@ export default function UtilityPage() {
}
zh={
<>
- 顺序有影响吗?有时有。作用在同一层级上的两个工具,
- 换顺序往往结果相同;作用在不同层级上的两个,通常就不同。
- 与其背规则,不如悬停在类型别名上直接看结果:
+ 顺序有影响吗?有时有。作用在同一层级上的两个工具,换顺序往往结果相同;作用在不同层级上的两个,通常就不同。与其背规则,不如悬停在类型别名上直接看结果:
>
}
/>
@@ -1661,8 +1614,7 @@ export default function UtilityPage() {
三,Omit 的键拼错不报错。
- 为了安全去掉一个属性时,把拼写再核对一遍;
- 或者用 Pick 白名单,让编译器替你核对。
+ 为了安全去掉一个属性时,把拼写再核对一遍;或者用 Pick 白名单,让编译器替你核对。
>
}
@@ -1692,8 +1644,7 @@ export default function UtilityPage() {
这套工具很好用,但它们一点都不特殊。Partial{" "}
的定义只有一行 TypeScript,Exclude 更短。第 07
章会把它们拆开:映射类型、条件类型、keyof、
- infer。零件就那么几个,拆完之后,
- 这一章用过的每一个工具,你都能自己写出来。
+ infer。零件就那么几个,拆完之后,这一章用过的每一个工具,你都能自己写出来。
}
/>
@@ -1738,8 +1689,7 @@ export default function UtilityPage() {
),
zh: (
<>
- 工具类型接受一个类型,返回一个新类型。写法像用尖括号的函数调用,
- 输入的类型永远不会被改动。
+ 工具类型接受一个类型,返回一个新类型。写法像用尖括号的函数调用,输入的类型永远不会被改动。
>
),
},
@@ -1776,8 +1726,7 @@ export default function UtilityPage() {
<>
挑属性:Pick 是白名单,Omit 是黑名单,
Record<K, V> 现造一个对象类型 —— 当{" "}
- K 是有限联合时,它要求每个键都在。
- 只有 Pick 会检查键。
+ K 是有限联合时,它要求每个键都在。只有 Pick 会检查键。
>
),
},
@@ -1794,8 +1743,7 @@ export default function UtilityPage() {
<>
筛联合:Exclude 去掉匹配的成员,
Extract 只留下它们,NonNullable 清掉{" "}
- null 和 undefined。
- 这一组作用在成员上,不是属性上。
+ null 和 undefined。这一组作用在成员上,不是属性上。
>
),
},
@@ -1829,8 +1777,7 @@ export default function UtilityPage() {
zh: (
<>
它们可以组合,结果从里往外读。这里缺的东西(深{" "}
- Readonly、严格版 Omit),
- 第 07 章你自己就能造出来。
+ Readonly、严格版 Omit),第 07 章你自己就能造出来。
>
),
},
diff --git a/app/utility/viz.tsx b/app/utility/viz.tsx
index fdf1f0e..5724cf6 100644
--- a/app/utility/viz.tsx
+++ b/app/utility/viz.tsx
@@ -164,8 +164,7 @@ function utFieldMsg(tool: UtToolId, f: UtField): ReactNode {
}
zh={
<>
- {f.key} 过机:冒号前加上一个 ?。
- 这个属性现在可以不写,所以读它拿到的是{" "}
+ {f.key} 过机:冒号前加上一个 ?。这个属性现在可以不写,所以读它拿到的是{" "}
{f.type} | undefined
@@ -208,8 +207,7 @@ function utFieldMsg(tool: UtToolId, f: UtField): ReactNode {
}
zh={
<>
- {f.key} 过机:前面加上 readonly。
- 对象建好之后,再给这个属性赋值就会被编译器拒绝。
+ {f.key} 过机:前面加上 readonly。对象建好之后,再给这个属性赋值就会被编译器拒绝。
>
}
/>
@@ -225,8 +223,7 @@ function utFieldMsg(tool: UtToolId, f: UtField): ReactNode {
}
zh={
<>
- {f.key} 在白名单上,原样通过。名字、类型、
- 可选与只读标记全都不变。
+ {f.key} 在白名单上,原样通过。名字、类型、可选与只读标记全都不变。
>
}
/>
@@ -256,8 +253,7 @@ function utFieldMsg(tool: UtToolId, f: UtField): ReactNode {
}
zh={
<>
- {f.key} 不在白名单上,被丢掉。新类型里没有这个属性,
- 读它就是一个编译错误。
+ {f.key} 不在白名单上,被丢掉。新类型里没有这个属性,读它就是一个编译错误。
>
}
/>
@@ -271,8 +267,7 @@ function utFieldMsg(tool: UtToolId, f: UtField): ReactNode {
}
zh={
<>
- {f.key} 被点了名,Omit 把它丢掉。
- 对外的类型里再也没有这个属性。
+ {f.key} 被点了名,Omit 把它丢掉。对外的类型里再也没有这个属性。
>
}
/>
@@ -308,8 +303,7 @@ export function UtPipeline() {
}
zh={
<>
- 六个属性在左边排好队,机器上装的是 {tool}。
- 点「下一步」,让它们一个一个过机。
+ 六个属性在左边排好队,机器上装的是 {tool}。点「下一步」,让它们一个一个过机。
>
}
/>
@@ -328,8 +322,7 @@ export function UtPipeline() {
zh={
<>
出料完毕,这就是 {meta.out}。回头看入料区:Order{" "}
- 和原来一模一样。工具类型接受一个类型、返回一个新类型,
- 从不改动你交给它的那个类型。
+ 和原来一模一样。工具类型接受一个类型、返回一个新类型,从不改动你交给它的那个类型。
>
}
/>
@@ -527,8 +520,7 @@ export function UtUnionSieve() {
<>
Exclude 去掉与第二个参数匹配的成员:
"done" 和{" "}
- "cancelled" 被去掉,其余留下。
- 这正好是「还在进行中」的订单状态,适合用在取餐大屏上。
+ "cancelled" 被去掉,其余留下。这正好是「还在进行中」的订单状态,适合用在取餐大屏上。
>
}
/>
@@ -543,8 +535,7 @@ export function UtUnionSieve() {
}
zh={
<>
- Extract 只留下匹配的成员,其余全部去掉。
- 这里得到的是两个终结状态,归档表要的就是这个类型。
+ Extract 只留下匹配的成员,其余全部去掉。这里得到的是两个终结状态,归档表要的就是这个类型。
>
}
/>
diff --git a/lib/classes-data.tsx b/lib/classes-data.tsx
index 6b4675d..f46b29f 100644
--- a/lib/classes-data.tsx
+++ b/lib/classes-data.tsx
@@ -208,8 +208,7 @@ export const LABS: Lab[] = [
public / protected / private{" "}
三种成员的 MilkTeaShop,再写一个子类{" "}
FranchiseShop。然后从子类里读 private 成员、
- 从类外读 protected 成员,把两条报错完整读一遍。
- 接着再加一行 shop["vaultCode"] ——
+ 从类外读 protected 成员,把两条报错完整读一遍。接着再加一行 shop["vaultCode"] ——
编译器会说什么吗?
}
@@ -227,10 +226,7 @@ export const LABS: Lab[] = [
}
zh={
<>
- 把鼠标停在红波浪线上就能看到报错。对比两条的措辞:
- 一条以 only accessible within class 结尾,
- 另一条多了 and its subclasses。
- 方括号那一行才是最出人意料的。
+ 把鼠标停在红波浪线上就能看到报错。对比两条的措辞:一条以 only accessible within class 结尾,另一条多了 and its subclasses。方括号那一行才是最出人意料的。
>
}
/>
@@ -258,8 +254,7 @@ export const LABS: Lab[] = [
两条报错的差别,就是两级权限的差别:protected{" "}
那条以 and its subclasses 结尾,
private 那条停在{" "}
- within class 'MilkTeaShop'。
- 最关键的是最后一行:shop["vaultCode"]{" "}
+ within class 'MilkTeaShop'。最关键的是最后一行:shop["vaultCode"]{" "}
一点错都不报。用方括号访问私有成员是被有意允许的 ——
这提醒我们,private{" "}
是给改代码的人看的一道检查,不是一把锁。
@@ -296,8 +291,7 @@ export const LABS: Lab[] = [
在 Playground 里定义本章的 PaymentProvider 接口(
pay + refund),写一个{" "}
class CashPay implements PaymentProvider,但
- 故意漏掉 refund,读一读编译器说了什么。
- 然后把它补上,并让同一个类再 implements 第二个接口{" "}
+ 故意漏掉 refund,读一读编译器说了什么。然后把它补上,并让同一个类再 implements 第二个接口{" "}
Named {"{ name: string }"}。
}
@@ -314,8 +308,7 @@ export const LABS: Lab[] = [
}
zh={
<>
- 失败时报的是 ts(2420) incorrectly implements,
- 第二行会点名缺了哪个成员。多个接口用逗号隔开:
+ 失败时报的是 ts(2420) incorrectly implements,第二行会点名缺了哪个成员。多个接口用逗号隔开:
implements A, B。
>
}
@@ -338,10 +331,8 @@ export const LABS: Lab[] = [
}
zh={
<>
- 注意报错出现的位置:在类名上,而不是散落在使用它的地方。
- 这正是 implements 的用处 ——
- 让类明确写出自己要符合什么,少了成员就在定义处报出来,
- 而不是等到别人调用时才暴露。
+ 注意报错出现的位置:在类名上,而不是散落在使用它的地方。这正是 implements 的用处 ——
+ 让类明确写出自己要符合什么,少了成员就在定义处报出来,而不是等到别人调用时才暴露。
>
}
/>
@@ -372,10 +363,8 @@ export const LABS: Lab[] = [
zh={
在 Playground 里写一个带{" "}
- private vaultCode = "8848" 的类,
- 点开右侧的 .JS 标签页看编译产物;然后把{" "}
- private vaultCode 改成 #vaultCode,
- 再看一次。最后分别从类外读一次这个字段,并对两个实例各做一次{" "}
+ private vaultCode = "8848" 的类,点开右侧的 .JS 标签页看编译产物;然后把{" "}
+ private vaultCode 改成 #vaultCode,再看一次。最后分别从类外读一次这个字段,并对两个实例各做一次{" "}
JSON.stringify。
}
@@ -394,10 +383,8 @@ export const LABS: Lab[] = [
}
zh={
<>
- 要对比的只有一点:编译产物里,那个字段前面还剩下什么。
- 一个版本里 private 消失了,另一个版本里{" "}
- # 原样还在。把 Playground 的 target 设为 ES2022,
- 第二个版本就不会被改写成 WeakMap。
+ 要对比的只有一点:编译产物里,那个字段前面还剩下什么。一个版本里 private 消失了,另一个版本里{" "}
+ # 原样还在。把 Playground 的 target 设为 ES2022,第二个版本就不会被改写成 WeakMap。
>
}
/>
@@ -422,8 +409,7 @@ export const LABS: Lab[] = [
.JS 面板里,ShopA 只剩下一个普通的{" "}
vaultCode = "8848" ——{" "}
private 这个词不见了;ShopB 的{" "}
- #vaultCode 则原样保留。
- 差别就在这里:private 在编译时检查,
+ #vaultCode 则原样保留。差别就在这里:private 在编译时检查,
#field 在运行时生效。
>
}
@@ -457,8 +443,7 @@ export const LABS: Lab[] = [
写一个 abstract 类 Staff:抽象方法{" "}
greet(): string,加一个普通方法{" "}
- clockIn()(内部调用 this.greet())。
- 依次制造三种情况:① 直接 new Staff();② 子类{" "}
+ clockIn()(内部调用 this.greet())。依次制造三种情况:① 直接 new Staff();② 子类{" "}
Barista 不实现 greet;③ 实现之后调用{" "}
clockIn()。三种情况各是什么结果?
@@ -502,10 +487,7 @@ export const LABS: Lab[] = [
}
zh={
<>
- 这三个结果就是「没写完的类」的完整生命周期:
- 类本身不能实例化(①);子类在实现完每一个抽象成员之前不能用(②);
- 实现之后,它就继承到了那些已经写好的方法(③)。
- 父类固定流程的步骤,子类补上每一步的细节。
+ 这三个结果就是「没写完的类」的完整生命周期:类本身不能实例化(①);子类在实现完每一个抽象成员之前不能用(②);实现之后,它就继承到了那些已经写好的方法(③)。父类固定流程的步骤,子类补上每一步的细节。
>
}
/>
@@ -632,10 +614,8 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- private 是编译期检查:它挡住的是改代码的人,
- 挡不住 s["vaultCode"]、
- JSON.stringify、DevTools 和普通 JavaScript。
- 要一个运行时也拿不到的字段,用 JavaScript 的 #field。
+ private 是编译期检查:它挡住的是改代码的人,挡不住 s["vaultCode"]、
+ JSON.stringify、DevTools 和普通 JavaScript。要一个运行时也拿不到的字段,用 JavaScript 的 #field。
>
}
/>
@@ -653,8 +633,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 关于 protected 和 private 的差别,
- 哪句话说对了?
+ 关于 protected 和 private 的差别,哪句话说对了?
>
}
/>
@@ -740,8 +719,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 字段、方法、访问器、参数属性,两个修饰符都能用。
- 它们管的是访问范围,不挑成员种类。
+ 字段、方法、访问器、参数属性,两个修饰符都能用。它们管的是访问范围,不挑成员种类。
>
}
/>,
@@ -775,8 +753,7 @@ export const QUIZ: QuizItem[] = [
zh={
<>
public:任何代码。protected
- :本类,以及任何 extends 它的类。private:只有类体自己。
- 三者都在编译期检查。
+ :本类,以及任何 extends 它的类。private:只有类体自己。三者都在编译期检查。
>
}
/>
@@ -795,8 +772,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- strict 下 name: string; 报了 ts(2564):
- 声明了却从未赋值。下面哪种改法不能消掉这个错误?
+ strict 下 name: string; 报了 ts(2564):声明了却从未赋值。下面哪种改法不能消掉这个错误?
>
}
/>
@@ -896,9 +872,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- ! 确实能消掉报错。
- 代价是这份保证从编译器转到了你身上,那是另一回事;
- 但就「检查能不能通过」而言,它是有效的。
+ ! 确实能消掉报错。代价是这份保证从编译器转到了你身上,那是另一回事;但就「检查能不能通过」而言,它是有效的。
>
}
/>,
@@ -917,8 +891,7 @@ export const QUIZ: QuizItem[] = [
zh={
<>
编译器不跨方法追踪赋值:它无法确定 init(){" "}
- 一定会被调用,也无法确定它在每条路径上都赋了值。
- 就地初始化、构造器里赋值,或者用 ! ——
+ 一定会被调用,也无法确定它在每条路径上都赋了值。就地初始化、构造器里赋值,或者用 ! ——
绕道 init 是唯一无效的那条路。
>
}
@@ -971,9 +944,7 @@ export const QUIZ: QuizItem[] = [
zh={
<>
abstract 类是一个没写完的类:标了{" "}
- abstract 的成员只有签名没有方法体,
- 子类必须把它们全部实现。直接对它 new 会报 ts(2511)。
- 那些已经实现好的方法,子类照常继承。
+ abstract 的成员只有签名没有方法体,子类必须把它们全部实现。直接对它 new 会报 ts(2511)。那些已经实现好的方法,子类照常继承。
>
}
/>
@@ -1008,8 +979,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 编译期核对这个类有没有接口列出的成员。它不提供任何东西,
- 也不留下运行时痕迹
+ 编译期核对这个类有没有接口列出的成员。它不提供任何东西,也不留下运行时痕迹
>
}
/>,
@@ -1052,8 +1022,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 没有东西可复制。interface 只描述形状,里面没有任何方法体,
- 每一个都得类自己写。
+ 没有东西可复制。interface 只描述形状,里面没有任何方法体,每一个都得类自己写。
>
}
/>,
@@ -1103,10 +1072,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- implements 不会以任何方式改变类的类型,
- 它只是让编译器确认这个类符合接口。
- 正因为它只是一次检查,一个类可以同时接受多个接口的检查,
- 而 extends 只能有一个基类。
+ implements 不会以任何方式改变类的类型,它只是让编译器确认这个类符合接口。正因为它只是一次检查,一个类可以同时接受多个接口的检查,而 extends 只能有一个基类。
>
}
/>
@@ -1206,8 +1172,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 这是最常见的错误预期。implements 只做检查:
- 它拿你写好的类去和接口比对,绝不会把接口的类型倒推回你的参数上。
+ 这是最常见的错误预期。implements 只做检查:它拿你写好的类去和接口比对,绝不会把接口的类型倒推回你的参数上。
>
}
/>,
@@ -1260,8 +1225,7 @@ export const QUIZ: QuizItem[] = [
zh={
<>
这个检查只有一个方向:从类到接口。
- pay(amount: number) 还是要自己写,
- 编译器再拿你写的去和 PaymentProvider 比对。
+ pay(amount: number) 还是要自己写,编译器再拿你写的去和 PaymentProvider 比对。
>
}
/>
@@ -1346,8 +1310,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 漏了一条。想一想 readonly 在这里算不算修饰符,
- 以及一个只做类型剥离的运行时拿这种语法能怎么办。
+ 漏了一条。想一想 readonly 在这里算不算修饰符,以及一个只做类型剥离的运行时拿这种语法能怎么办。
>
}
/>
@@ -1363,8 +1326,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 有一项选错了。参数属性会让编译器生成一条赋值语句,
- 而生成的语句正是留在产物里的痕迹。
+ 有一项选错了。参数属性会让编译器生成一条赋值语句,而生成的语句正是留在产物里的痕迹。
>
}
/>
@@ -1385,8 +1347,7 @@ export const QUIZ: QuizItem[] = [
zh={
<>
参数属性一行顶四行,但它会生成代码:编译器替你写下{" "}
- this.db = db。
- 这就是它不能被单纯擦除、以及 erasableSyntaxOnly{" "}
+ this.db = db。这就是它不能被单纯擦除、以及 erasableSyntaxOnly{" "}
会报它的原因。构造器参数前写上 public、
private、protected、readonly{" "}
中任意一个,都会形成参数属性。
@@ -1409,8 +1370,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- PaperCup 和 PlasticCup 成员完全相同,
- 且各自声明了一个 private stock = 0。
+ PaperCup 和 PlasticCup 成员完全相同,且各自声明了一个 private stock = 0。
const cup: PaperCup = new PlasticCup() 的结果是?
>
}
@@ -1427,8 +1387,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 报错:private 成员必须来自同一处声明,
- 所以这两个类是按「出身」比较的
+ 报错:private 成员必须来自同一处声明,所以这两个类是按「出身」比较的
>
}
/>,
@@ -1488,8 +1447,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- TypeScript 的类型检查没有「警告」这一档,
- 赋值要么通过要么不通过 —— 这里是结结实实的 ts(2322)。
+ TypeScript 的类型检查没有「警告」这一档,赋值要么通过要么不通过 —— 这里是结结实实的 ts(2322)。
>
}
/>,
@@ -1504,9 +1462,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 两个同形状且没有私有成员的类是可以互相赋值的,
- 第 04 章讲过。这里出问题的原因是 private,
- 而不是「它们是 class」。
+ 两个同形状且没有私有成员的类是可以互相赋值的,第 04 章讲过。这里出问题的原因是 private,而不是「它们是 class」。
>
}
/>,
@@ -1526,8 +1482,7 @@ export const QUIZ: QuizItem[] = [
<>
完整规则:类型按形状比较,但 private 和{" "}
protected 成员必须来自同一处声明
- (从共同基类继承下来的算)。想让它们重新兼容,
- 就让两个类 extends 同一个声明了 stock 的基类。
+ (从共同基类继承下来的算)。想让它们重新兼容,就让两个类 extends 同一个声明了 stock 的基类。
>
}
/>
@@ -1619,8 +1574,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- private 会被擦除,运行时就是个普通属性。
- 连在 TypeScript 里,s["vaultCode"]{" "}
+ private 会被擦除,运行时就是个普通属性。连在 TypeScript 里,s["vaultCode"]{" "}
都能读到而且不报错。
>
}
@@ -1653,8 +1607,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- readonly 管的是这个属性能不能被赋值,
- 而不是谁能读它 —— 而且它同样只在编译期检查。
+ readonly 管的是这个属性能不能被赋值,而不是谁能读它 —— 而且它同样只在编译期检查。
>
}
/>,
@@ -1672,8 +1625,7 @@ export const QUIZ: QuizItem[] = [
zh={
<>
「程序运行时读不到」是一个运行时要求,只有 JavaScript 的{" "}
- #field 做得到。
- 它不属于类型系统,所以擦除类型不会把它擦掉。
+ #field 做得到。它不属于类型系统,所以擦除类型不会把它擦掉。
>
}
/>
diff --git a/lib/functions-data.tsx b/lib/functions-data.tsx
index 3b52301..ec6bc14 100644
--- a/lib/functions-data.tsx
+++ b/lib/functions-data.tsx
@@ -39,11 +39,9 @@ export const LABS: Lab[] = [
zh={
<>
- 打开 TypeScript Playground(typescriptlang.org/play),
- 把下面这段 JS 风格的函数贴进去:
+ 打开 TypeScript Playground(typescriptlang.org/play),把下面这段 JS 风格的函数贴进去:
{`function makeTea(base, sweet) { return base + "(" + sweet + "% sugar)"; }`}
- 。给它补上注解:base 是 string,sweet 是 number,返回值 string。
- 然后故意把参数反着传,调用{" "}
+ 。给它补上注解:base 是 string,sweet 是 number,返回值 string。然后故意把参数反着传,调用{" "}
makeTea(50, "Oolong"),读一读编译器报了什么。
>
@@ -61,9 +59,8 @@ export const LABS: Lab[] = [
}
zh={
<>
- 每个参数写成 名字: 类型,返回值写在关门括号后面:
- ): string。
- 报错会说清是哪个参数、期望什么、实际给了什么。
+ 每个参数写成名字: 类型,返回值写在关门括号后面:
+ ): string。报错会说清是哪个参数、期望什么、实际给了什么。
>
}
/>
@@ -107,10 +104,7 @@ makeTea(50, "Oolong");
// Argument of type 'number' is not assignable to parameter of type 'string'.`}
/>
- 只会出现一条报错,不是两条。
- 对一个只有单条签名的函数,TypeScript 报出第一个对不上的实参就停了;
- 把它改对,第二个实参的报错才会冒出来。
- 而没有注解的 JS 版本根本不报错:程序照跑,打印出
+ 只会出现一条报错,不是两条。对一个只有单条签名的函数,TypeScript 报出第一个对不上的实参就停了;把它改对,第二个实参的报错才会冒出来。而没有注解的 JS 版本根本不报错:程序照跑,打印出
"50(Oolong% sugar)"。
>
@@ -151,9 +145,7 @@ makeTea(50, "Oolong");
zh={
<>
- 在 Playground 里写一个 orderLine 函数:
- 第一个参数 base(string,必选),
- 第二个参数 sugar(默认值 50),后面收任意多个配料{" "}
+ 在 Playground 里写一个 orderLine 函数:第一个参数 base(string,必选),第二个参数 sugar(默认值 50),后面收任意多个配料{" "}
...toppings: string[]。然后做三件事:
① 悬停 sugar,看推断出来的类型;
② 确认 orderLine("Milk Green") 和{" "}
@@ -179,8 +171,7 @@ makeTea(50, "Oolong");
}
zh={
<>
- 带默认值的参数不用写类型;rest 参数必须放在最后。
- 第 ③ 步不会报「顺序错误」—— 想想这时候函数到底要求几个实参。
+ 带默认值的参数不用写类型;rest 参数必须放在最后。第 ③ 步不会报「顺序错误」—— 想想这时候函数到底要求几个实参。
>
}
/>
@@ -236,15 +227,10 @@ bad("Milk Green");
// Expected 2 arguments, but got 1.`}
/>
- 悬停 sugar 显示 number,是从默认值 50 推断的。
- 第 ③ 步最有意思:把带默认值的参数放在前面并不是语法错误,
- 但这个默认值用不上了 ——
- 一个参数要在调用处变成可选,前提是它后面的参数也全都可选。
- 这里 base 是必选的,所以函数仍然要求两个实参,
- bad("Milk Green") 直接卡在个数上。
- 想用默认值就得写成{" "}
- bad(undefined, "Milk Green")。
- 所以实践中带默认值的参数照样放在最后。
+ 悬停 sugar 显示 number,是从默认值 50 推断的。第 ③ 步最有意思:把带默认值的参数放在前面并不是语法错误,但这个默认值用不上了 ——
+ 一个参数要在调用处变成可选,前提是它后面的参数也全都可选。这里 base 是必选的,所以函数仍然要求两个实参,
+ bad("Milk Green") 直接卡在个数上。想用默认值就得写成{" "}
+ bad(undefined, "Milk Green")。所以实践中带默认值的参数照样放在最后。
>
}
@@ -301,8 +287,7 @@ bad("Milk Green");
}
zh={
<>
- 索引签名的写法是 {"[sku: string]: number"}。
- 两条报错的说法完全不同:一条说属性只读,一条说类型不可赋值。
+ 索引签名的写法是 {"[sku: string]: number"}。两条报错的说法完全不同:一条说属性只读,一条说类型不可赋值。
>
}
/>
@@ -368,9 +353,7 @@ stock["tea-002"] = "many";
// Type 'string' is not assignable to type 'number'.`}
/>
- 两条规则各管一头:readonly 管的是这个属性能不能写,
- 索引签名管的是写进去的值长什么样。
- 顺手再做个实验:点开右侧的 .JS 标签 —— readonly{" "}
+ 两条规则各管一头:readonly 管的是这个属性能不能写,索引签名管的是写进去的值长什么样。顺手再做个实验:点开右侧的 .JS 标签 —— readonly{" "}
和所有类型都不见了。这就是「类型只活在编译期」的实际含义。
>
@@ -435,8 +418,7 @@ stock["tea-002"] = "many";
}
zh={
<>
- 第 ① 步的重点是「什么都不报」:两个同名 interface 会被合并成一个。
- 造对象时少写任何一个字段,编译器都会告诉你。
+ 第 ① 步的重点是「什么都不报」:两个同名 interface 会被合并成一个。造对象时少写任何一个字段,编译器都会告诉你。
>
}
/>
@@ -496,10 +478,7 @@ type Shop2 = { city: string }; // Duplicate identifier 'Shop2'.
type Size = "small" | "large";`}
/>
- 合并看着像 bug,其实是一道特意留的门:
- 用来给不属于你的类型做补充,比如给 window 加自定义字段,
- 或者给第三方包的类型打补丁(09 章细讲)。
- 也正因为它在读代码时容易被忽略,团队通常会约定统一用法 ——
+ 合并看着像 bug,其实是一道特意留的门:用来给不属于你的类型做补充,比如给 window 加自定义字段,或者给第三方包的类型打补丁(09 章细讲)。也正因为它在读代码时容易被忽略,团队通常会约定统一用法 ——
「随便选一个,但要一致」说的就是这件事。
>
@@ -569,8 +548,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- topping?: string 本身是完全合法的可选参数写法。
- 问题不在这个参数身上,在它站的位置。
+ topping?: string 本身是完全合法的可选参数写法。问题不在这个参数身上,在它站的位置。
>
),
},
@@ -584,8 +562,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 参数个数没有上限,rest 参数甚至能收任意多个。
- 报错说的是顺序,不是数量。
+ 参数个数没有上限,rest 参数甚至能收任意多个。报错说的是顺序,不是数量。
>
),
},
@@ -598,8 +575,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 必选参数本来就不需要默认值。
- 真正的规矩是:可选的、带默认值的,都排在必选参数后面。
+ 必选参数本来就不需要默认值。真正的规矩是:可选的、带默认值的,都排在必选参数后面。
>
),
},
@@ -616,9 +592,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 实参按位置对号入座。调用 brew("Oolong") 时,
- 没法说清 "Oolong" 算 topping 还是 base,
- 所以 TypeScript 直接禁止这样声明。报错原文:A required parameter
+ 实参按位置对号入座。调用 brew("Oolong") 时,没法说清 "Oolong" 算 topping 还是 base,所以 TypeScript 直接禁止这样声明。报错原文:A required parameter
cannot follow an optional parameter.
>
),
@@ -693,8 +667,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- () => 123 是一个返回 123 的箭头函数。
- 这道题问的是类型检查,不是这个值本身。
+ () => 123 是一个返回 123 的箭头函数。这道题问的是类型检查,不是这个值本身。
>
),
},
@@ -707,8 +680,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 运行时什么事都没有。类型全部被擦除,
- 这个函数老老实实返回 123,只是没人去读它。
+ 运行时什么事都没有。类型全部被擦除,这个函数老老实实返回 123,只是没人去读它。
>
),
},
@@ -725,9 +697,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- () => void 的宽容是有意设计的。
- 否则 forEach((n) => arr.push(n)) 就通不过检查,
- 因为 push 返回一个 number。记住这个分工:
+ () => void 的宽容是有意设计的。否则 forEach((n) => arr.push(n)) 就通不过检查,因为 push 返回一个 number。记住这个分工:
类型里的 void 表示调用方不看结果,
声明上的 void 表示函数不返回值。
>
@@ -790,8 +760,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 确实可能抛,但 TypeScript 不强制 try/catch,这段代码编译毫无问题。
- 编译器检查的是类型,不是运行时风险。
+ 确实可能抛,但 TypeScript 不强制 try/catch,这段代码编译毫无问题。编译器检查的是类型,不是运行时风险。
>
),
},
@@ -805,8 +774,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- JSON.parse 吃的就是字符串,json: string 写得完全正确。
- 问题出在「出去」那一头,不在「进来」这一头。
+ JSON.parse 吃的就是字符串,json: string 写得完全正确。问题出在「出去」那一头,不在「进来」这一头。
>
),
},
@@ -819,8 +787,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 推断确实在工作,但它读的是 JSON.parse 的返回值类型,
- 而那个类型在标准库里就是 any。
+ 推断确实在工作,但它读的是 JSON.parse 的返回值类型,而那个类型在标准库里就是 any。
>
),
},
@@ -836,10 +803,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- JSON.parse 返回 any,推断原样放行,
- 于是 load 的返回值也是 any。
- 调用方拼错字段、乱调方法,编译器全程沉默。
- 修法是写出返回值类型,把 any 关在函数内部。
+ JSON.parse 返回 any,推断原样放行,于是 load 的返回值也是 any。调用方拼错字段、乱调方法,编译器全程沉默。修法是写出返回值类型,把 any 关在函数内部。
>
),
},
@@ -918,8 +882,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 勾多了。其中一项是两边都会的基本功,
- 还有一项恰恰是 interface 独有的能力 —— 两个同名 type 只会报
+ 勾多了。其中一项是两边都会的基本功,还有一项恰恰是 interface 独有的能力 —— 两个同名 type 只会报
Duplicate identifier。
>
),
@@ -935,8 +898,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 描述对象形状两边都行;合并是 interface 独有;
- 联合类型和映射类型是 type 独有 ——
+ 描述对象形状两边都行;合并是 interface 独有;联合类型和映射类型是 type 独有 ——
这也是 06、07 两章的类型编程全部写在 type 一侧的原因。
>
),
@@ -997,8 +959,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- Object.freeze 是要你自己调用的运行时函数。
- TypeScript 从不往产物里添加行为,类型擦除是铁律。
+ Object.freeze 是要你自己调用的运行时函数。TypeScript 从不往产物里添加行为,类型擦除是铁律。
>
),
},
@@ -1011,8 +972,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- getter 是真实的 JavaScript 语法,得你自己写。
- readonly 一个字节的代码都不产生。
+ getter 是真实的 JavaScript 语法,得你自己写。readonly 一个字节的代码都不产生。
>
),
},
@@ -1026,8 +986,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 连注释都不会留下。类型信息对运行时完全不可见,
- 这正是「类型擦除」的含义。
+ 连注释都不会留下。类型信息对运行时完全不可见,这正是「类型擦除」的含义。
>
),
},
@@ -1043,10 +1002,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- readonly 是编译期的检查:
- 你写代码时编译器拦你,运行时没有任何防护。
- 想要真冻结就用 Object.freeze。
- 不过在团队协作里,编译期这一拦已经挡掉了绝大多数误改。
+ readonly 是编译期的检查:你写代码时编译器拦你,运行时没有任何防护。想要真冻结就用 Object.freeze。不过在团队协作里,编译期这一拦已经挡掉了绝大多数误改。
>
),
},
@@ -1095,8 +1051,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- void。写在类型位置上,它表示调用方不会读结果,
- 所以返回任何值的函数都能匹配。这就是 forEach 能接收{" "}
+ void。写在类型位置上,它表示调用方不会读结果,所以返回任何值的函数都能匹配。这就是 forEach 能接收{" "}
push 这种有返回值的回调的原因。
>
),
@@ -1146,8 +1101,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 试试就知道:g() 会报 Expected 1 arguments, but got 0。
- 两者的区别正是「能不能空手调用」。
+ 试试就知道:g() 会报 Expected 1 arguments, but got 0。两者的区别正是「能不能空手调用」。
>
),
},
@@ -1213,8 +1167,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- [1, 2, 3].map((n) => n * 2) 能编译通过,
- 可是 map 调用回调时会传三个实参。为什么?
+ [1, 2, 3].map((n) => n * 2) 能编译通过,可是 map 调用回调时会传三个实参。为什么?
>
),
},
@@ -1261,8 +1214,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- map 只有一条签名。这条规则是通用的,
- 对所有函数类型都成立,不是 map 的特例。
+ map 只有一条签名。这条规则是通用的,对所有函数类型都成立,不是 map 的特例。
>
),
},
@@ -1277,8 +1229,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 没有任何补齐。运行时 map 照样传三个实参,
- 只是这个回调没有给后两个起名字。
+ 没有任何补齐。运行时 map 照样传三个实参,只是这个回调没有给后两个起名字。
>
),
},
@@ -1310,10 +1261,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 在 JavaScript 里,传的实参比声明的参数多是正常且无害的。
- TypeScript 允许的正是这一点,
- 所以单参数的回调放得进三参数的回调类型里。
- 反过来会被拒绝:第四个参数会去读一个根本没人传的实参。
+ 在 JavaScript 里,传的实参比声明的参数多是正常且无害的。TypeScript 允许的正是这一点,所以单参数的回调放得进三参数的回调类型里。反过来会被拒绝:第四个参数会去读一个根本没人传的实参。
>
),
},
@@ -1332,8 +1280,7 @@ export const QUIZ: QuizItem[] = [
<>
即使开着 strict,
interface Feeder { feed(a: Animal): void }{" "}
- 也接受一个 feed 只收 Dog 的对象。
- 这说明什么?
+ 也接受一个 feed 只收 Dog 的对象。这说明什么?
>
),
},
@@ -1364,8 +1311,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- strictFunctionTypes 默认关着,
- 必须在 strict 之外单独打开。
+ strictFunctionTypes 默认关着,必须在 strict 之外单独打开。
>
),
},
@@ -1402,8 +1348,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- strict 确实会打开 strictFunctionTypes。
- 只是这个开关对用方法语法声明的成员不生效。
+ strict 确实会打开 strictFunctionTypes。只是这个开关对用方法语法声明的成员不生效。
>
),
},
@@ -1417,9 +1362,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 在函数类型里,参数按逆变检查:
- 一个收 Dog 的函数不能赋给「要求收 Animal」的位置。
- 方法语法才是例外。
+ 在函数类型里,参数按逆变检查:一个收 Dog 的函数不能赋给「要求收 Animal」的位置。方法语法才是例外。
>
),
},
@@ -1438,12 +1381,8 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 在 strictFunctionTypes 下,函数类型位置上的参数
- 按逆变检查,而用方法语法声明的成员沿用旧的双变规则。
- 这是类型系统里一个已知且刻意保留的漏洞:
- 堵上它,Array<Dog> 就不能当作{" "}
- Array<Animal> 用了。
- 想要严格检查,就用属性语法写这个成员。
+ 在 strictFunctionTypes 下,函数类型位置上的参数按逆变检查,而用方法语法声明的成员沿用旧的双变规则。这是类型系统里一个已知且刻意保留的漏洞:堵上它,Array<Dog> 就不能当作{" "}
+ Array<Animal> 用了。想要严格检查,就用属性语法写这个成员。
>
),
},
@@ -1523,8 +1462,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 重载解析不会去找「最合适的那条」。
- 它在第一条能接受这些实参的签名上就停了,而 unknown{" "}
+ 重载解析不会去找「最合适的那条」。它在第一条能接受这些实参的签名上就停了,而 unknown{" "}
什么都接受。
>
),
@@ -1569,10 +1507,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 重载按书写顺序尝试,第一条匹配的胜出。
- 把两条签名调换,fmt(1) 就变成 number 了。
- 顺序是 API 的一部分:越具体的签名越要写在前面。
- 另外别忘了,实现签名不能从外部调用。
+ 重载按书写顺序尝试,第一条匹配的胜出。把两条签名调换,fmt(1) 就变成 number 了。顺序是 API 的一部分:越具体的签名越要写在前面。另外别忘了,实现签名不能从外部调用。
>
),
},
@@ -1603,8 +1538,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 随便选一个并保持一致;需要 union 或映射类型时用 type,
- 需要 merging 时用 interface。
+ 随便选一个并保持一致;需要 union 或映射类型时用 type,需要 merging 时用 interface。
>
),
},
@@ -1630,8 +1564,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 「一定更快」是流传很广的说法,并没有这样的绝对结论,
- 官方也从没建议以性能为由禁用 type。
+ 「一定更快」是流传很广的说法,并没有这样的绝对结论,官方也从没建议以性能为由禁用 type。
>
),
},
@@ -1659,8 +1592,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- handbook 恰恰没有强制规定,
- 现行说法是按个人与团队偏好选,保持一致即可。
+ handbook 恰恰没有强制规定,现行说法是按个人与团队偏好选,保持一致即可。
>
),
},
@@ -1676,10 +1608,8 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 这场争论被夸大了。大部分代码里两者可以互换,
- 真正的选型标准只有两条:你需要的能力
- (union 或映射类型 → type;merging → interface),
- 以及代码库内部的一致性。
+ 这场争论被夸大了。大部分代码里两者可以互换,真正的选型标准只有两条:你需要的能力
+ (union 或映射类型 → type;merging → interface),以及代码库内部的一致性。
>
),
},
diff --git a/lib/generics-data.tsx b/lib/generics-data.tsx
index fdb4664..46cbdbd 100644
--- a/lib/generics-data.tsx
+++ b/lib/generics-data.tsx
@@ -205,8 +205,7 @@ export const LABS: Lab[] = [
<>
在函数名后面加 <T> 声明占位符, 再把签名里写死的{" "}
string / number / boolean{" "}
- 都换成 T。 返回值里的 | undefined 要留着:
- 空数组取第 0 个,拿到的是 undefined。
+ 都换成 T。返回值里的 | undefined 要留着:空数组取第 0 个,拿到的是 undefined。
>
}
/>
@@ -225,8 +224,7 @@ export const LABS: Lab[] = [
zh={
这一题的重点就是悬停:你能亲眼看到 T{" "}
- 在每次调用被解成不同的类型,返回值类型也跟着变。
- 一份定义,一个类型都没丢。
+ 在每次调用被解成不同的类型,返回值类型也跟着变。一份定义,一个类型都没丢。
}
/>
@@ -292,8 +290,7 @@ export const LABS: Lab[] = [
}
zh={
<>
- 切片用 all.slice((page - 1) * size, page * size)。
- 返回值类型写成 {"Paginated"} ——
+ 切片用 all.slice((page - 1) * size, page * size)。返回值类型写成 {"Paginated"} ——
函数的占位符和类型的占位符是同一个 T。
>
}
@@ -353,11 +350,8 @@ export const LABS: Lab[] = [
{"function longest(a: T, b: T): T"}
- ,返回 length 更大的那个。然后调用四次:
- 两个字符串、两个数组、longest(10, 100)、
- {"longest({ length: 3 }, { length: 7 })"}。
- 每次先自己预测结果,再看编译器怎么说。最后把约束删掉,
- 看函数体里哪一行先坏掉。
+ ,返回 length 更大的那个。然后调用四次:两个字符串、两个数组、longest(10, 100)、
+ {"longest({ length: 3 }, { length: 7 })"}。每次先自己预测结果,再看编译器怎么说。最后把约束删掉,看函数体里哪一行先坏掉。
}
/>
@@ -375,8 +369,7 @@ export const LABS: Lab[] = [
zh={
<>
判断一个类型能不能通过,只问一句:它可以赋值给{" "}
- {"{ length: number }"} 吗?这就是上一章那套结构化检查。
- 和 class、继承没有任何关系。
+ {"{ length: number }"} 吗?这就是上一章那套结构化检查。和 class、继承没有任何关系。
>
}
/>
@@ -396,8 +389,7 @@ export const LABS: Lab[] = [
zh={
最后那个实验才是这题的题眼:约束同时朝两个方向起作用 ——
- 对调用方它是一个条件,对函数体它是一个保证。
- 删掉它,先垮的是函数体。
+ 对调用方它是一个条件,对函数体它是一个保证。删掉它,先垮的是函数体。
}
/>
@@ -434,11 +426,9 @@ export const LABS: Lab[] = [
一个签名可以声明不止一个占位符。请在 Playground 里实现两个函数:①{" "}
{"swap(pair: [A, B]): [B, A]"}, 交换一个二元组;②{" "}
- {"zip(as: A[], bs: B[]): [A, B][]"},
- 把两个数组按位配对,以短的那个为准。验收标准:
+ {"zip(as: A[], bs: B[]): [A, B][]"},把两个数组按位配对,以短的那个为准。验收标准:
{'swap(["Boba milk tea", 18])'} 悬停显示{" "}
- [number, string],而且配出来的每一对,
- 两个位置各自的类型都不糊。
+ [number, string],而且配出来的每一对,两个位置各自的类型都不糊。
}
/>
@@ -481,8 +471,7 @@ export const LABS: Lab[] = [
}
zh={
- A 和 B 各自独立推断,
- 各自在出现的每个位置保持一致。swap{" "}
+ A 和 B 各自独立推断,各自在出现的每个位置保持一致。swap{" "}
换的是值的位置,不是类型;zip{" "}
配出的每一对都是货真价实的 [A, B]。多个占位符正是{" "}
{"Record"} 、{"Map"} {" "}
@@ -547,8 +536,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 「什么都收 + 不报错」any 就做得到,而且做得更彻底。
- 泛型的价值恰恰在于还会报错:类型对不上的时候它拦得住。
+ 「什么都收 + 不报错」any 就做得到,而且做得更彻底。泛型的价值恰恰在于还会报错:类型对不上的时候它拦得住。
>
),
},
@@ -563,8 +551,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 类型参数只存在于编译期,擦除之后运行时没有 T。
- 按值的形状分支那是收窄,第 03 章的内容。
+ 类型参数只存在于编译期,擦除之后运行时没有 T。按值的形状分支那是收窄,第 03 章的内容。
>
),
},
@@ -577,8 +564,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 字符数往往还变多了。泛型省掉的是重复的定义和丢失的类型信息,
- 不是键盘敲击数。
+ 字符数往往还变多了。泛型省掉的是重复的定义和丢失的类型信息,不是键盘敲击数。
>
),
},
@@ -593,8 +579,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 类型参数是一个在调用处才被填上的占位符,
- 同一个签名里的每次使用指的都是同一个类型 ——
+ 类型参数是一个在调用处才被填上的占位符,同一个签名里的每次使用指的都是同一个类型 ——
返回值的类型就是这样和实参的类型绑在一起的。
>
),
@@ -689,8 +674,7 @@ export const QUIZ: QuizItem[] = [
zh: (
<>
unknown{" "}
- 是在完全没有推断原料时才会出现的结果,比如类型参数只出现在返回值里。
- 这里原料充足:[9.9, 19.9] 就是 number[]。
+ 是在完全没有推断原料时才会出现的结果,比如类型参数只出现在返回值里。这里原料充足:[9.9, 19.9] 就是 number[]。
>
),
},
@@ -796,8 +780,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 「写得长」不是论点。区别在行为:一个把类型丢在门口,
- 一个把类型带到结果。
+ 「写得长」不是论点。区别在行为:一个把类型丢在门口,一个把类型带到结果。
>
),
},
@@ -827,8 +810,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- any 没有被废弃,它有合法的用途。
- 它和泛型是两个不同的工具,不是新旧版本的关系。
+ any 没有被废弃,它有合法的用途。它和泛型是两个不同的工具,不是新旧版本的关系。
>
),
},
@@ -917,8 +899,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 数组也有 .length,通过,T = number[]。
- 约束检查的是形状,不是「你是不是数组」。
+ 数组也有 .length,通过,T = number[]。约束检查的是形状,不是「你是不是数组」。
>
),
},
@@ -932,8 +913,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 这个匿名对象字面量正好有 length: number,
- 所以满足约束。约束问的是形状,不问这个类型是哪来的。
+ 这个匿名对象字面量正好有 length: number,所以满足约束。约束问的是形状,不问这个类型是哪来的。
>
),
},
@@ -1027,8 +1007,7 @@ export const QUIZ: QuizItem[] = [
zh: (
<>
还有对的没选全。编译出的 JavaScript 里还有 T 吗?
- {""} 是合法语法吗?
- 同一次调用会把同一个占位符填成两种类型吗?
+ {""} 是合法语法吗?同一次调用会把同一个占位符填成两种类型吗?
>
),
},
@@ -1046,8 +1025,7 @@ export const QUIZ: QuizItem[] = [
<>
选进了错误说法。类型参数列表里的 extends 是约束,不是继承
—— 它的意思是「可以赋值给」,
- string 或一个普通对象类型都满足它。
- 而绝大多数泛型调用靠推断,尖括号是例外。
+ string 或一个普通对象类型都满足它。而绝大多数泛型调用靠推断,尖括号是例外。
>
),
},
@@ -1104,7 +1082,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- T 只出现一次,没联系任何东西,也就什么都没承诺。 改成{" "}
+ T 只出现一次,没联系任何东西,也就什么都没承诺。改成{" "}
x: unknown 更诚实
>
),
@@ -1146,9 +1124,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 这种灵活是假的。x: unknown 一样什么值都收。
- 类型参数在这里没提供 unknown 给不了的任何东西,
- 只多了阅读成本。
+ 这种灵活是假的。x: unknown 一样什么值都收。类型参数在这里没提供 unknown 给不了的任何东西,只多了阅读成本。
>
),
},
@@ -1164,8 +1140,7 @@ export const QUIZ: QuizItem[] = [
zh: (
<>
加约束解决不了根本问题:T{" "}
- 依然只出现一次,依然没联系任何东西。
- 问题不是「洞太松」,而是「这个洞根本没在做事」。
+ 依然只出现一次,依然没联系任何东西。问题不是「洞太松」,而是「这个洞根本没在做事」。
>
),
},
@@ -1196,8 +1171,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 类型参数的价值在于把东西联系起来,所以它至少要出现两次:
- 在两个参数之间,或者在参数和返回值之间。 只出现一次的 T{" "}
+ 类型参数的价值在于把东西联系起来,所以它至少要出现两次:在两个参数之间,或者在参数和返回值之间。只出现一次的 T{" "}
什么都没承诺,直接写 unknown 更清楚。
>
),
@@ -1232,8 +1206,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 和 class 继承用的是同一个单词,但在类型参数列表里它的意思是
- 「可以赋值给」。本章 §04 那道门上就写着它。
+ 和 class 继承用的是同一个单词,但在类型参数列表里它的意思是「可以赋值给」。本章 §04 那道门上就写着它。
>
),
},
@@ -1248,8 +1221,7 @@ export const QUIZ: QuizItem[] = [
zh: (
<>
{""} 的意思是 T
- 必须可以赋值给 {"{ length: number }"}。它是约束,不是继承,
- 判定用的是第 04 章那套结构化检查。
+ 必须可以赋值给 {"{ length: number }"}。它是约束,不是继承,判定用的是第 04 章那套结构化检查。
>
),
},
@@ -1348,8 +1320,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 它连运行的机会都没有。错误在编译期就报出来了,
- 这正是把约束写进签名的意义。
+ 它连运行的机会都没有。错误在编译期就报出来了,这正是把约束写进签名的意义。
>
),
},
diff --git a/lib/home-data.tsx b/lib/home-data.tsx
index 733d8ab..c074d74 100644
--- a/lib/home-data.tsx
+++ b/lib/home-data.tsx
@@ -49,8 +49,7 @@ export const LABS: Lab[] = [
typescriptlang.org/play
(免注册),清空左侧编辑器,把下面这段纯 JS
- 贴进去 —— 一个类型都别写。然后把鼠标依次悬停在 shopName、price、
- menu 这几个变量名上,看弹出来的类型提示。
+ 贴进去 —— 一个类型都别写。然后把鼠标依次悬停在 shopName、price、menu 这几个变量名上,看弹出来的类型提示。
>
}
/>
@@ -224,8 +223,7 @@ const menu = [
zh={
<>
把下面这段贴进 Playground,然后看右侧面板的 .JS 标签
- (默认就开着)—— 那就是编译产物。对照找一找:type 那行去哪了?
- 两个冒号注解去哪了?
+ (默认就开着)—— 那就是编译产物。对照找一找:type 那行去哪了?两个冒号注解去哪了?
>
}
/>
@@ -335,8 +333,7 @@ function priceOf(base, size) {
zh={
<>
三步:npm i -D typescript →{" "}
- npx tsc --init → npx tsc。
- 编译产物默认生成在源文件旁边。
+ npx tsc --init → npx tsc。编译产物默认生成在源文件旁边。
>
}
/>
@@ -375,8 +372,7 @@ diff order.ts order.js`}
}
zh={
<>
- diff 会列出所有差异:type 行没了、两个注解没了,其余原样。
- 再跑 node order.js,输出 large 22 ——
+ diff 会列出所有差异:type 行没了、两个注解没了,其余原样。再跑 node order.js,输出 large 22 ——
和写 JS 没有任何区别。顺手把 size 改成{" "}
"mega" 再 npx tsc
,感受一下编译器把关的样子:报错、拒绝放行。
@@ -467,8 +463,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 浏览器根本不认识 TS,它只跑 JS。TS 必须先编译(擦掉类型)
- 才能进浏览器 —— 这也是「三条路」都绕不开的一步。
+ 浏览器根本不认识 TS,它只跑 JS。TS 必须先编译(擦掉类型)才能进浏览器 —— 这也是「三条路」都绕不开的一步。
>
}
/>,
@@ -500,8 +495,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 超集意味着你会的 JS 一行都没白学。TS 加的那层「类型」是描述,
- 不是逻辑 —— 描述你的数据长什么样,好让编译器替你把关。
+ 超集意味着你会的 JS 一行都没白学。TS 加的那层「类型」是描述,不是逻辑 —— 描述你的数据长什么样,好让编译器替你把关。
>
}
/>
@@ -555,8 +549,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 运行时已经太晚了 —— 而且类型在编译后全部擦除,运行时根本没有
- 「类型」这个东西可查。
+ 运行时已经太晚了 —— 而且类型在编译后全部擦除,运行时根本没有「类型」这个东西可查。
>
}
/>,
@@ -571,8 +564,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 部署只是把编译产物搬过去。检查早在你编辑器里保存那一刻
- (以及 CI 里跑 tsc 时)就完成了。
+ 部署只是把编译产物搬过去。检查早在你编辑器里保存那一刻(以及 CI 里跑 tsc 时)就完成了。
>
}
/>,
@@ -697,8 +689,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 反了 —— 被保证擦干净的恰恰是类型;注释保不保留倒要看编译配置。
- 类型从不进产物。
+ 反了 —— 被保证擦干净的恰恰是类型;注释保不保留倒要看编译配置。类型从不进产物。
>
}
/>,
@@ -715,8 +706,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 类型是「编译期的安检」,下了飞机(运行时)就不存在了。想通这一点,
- 你就同时想通了两件事:TS 不拖慢程序,TS 也不在运行时保护你。
+ 类型是「编译期的安检」,下了飞机(运行时)就不存在了。想通这一点,你就同时想通了两件事:TS 不拖慢程序,TS 也不在运行时保护你。
>
}
/>
@@ -768,8 +758,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 三条正路你漏了至少一条 —— 提示:一条靠「编译」,一条靠「转译」,
- 还有一条是运行时「边擦边跑」。
+ 三条正路你漏了至少一条 —— 提示:一条靠「编译」,一条靠「转译」,还有一条是运行时「边擦边跑」。
>
}
/>
@@ -784,8 +773,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 有一项到今天也办不到:浏览器只认 JS,你给它 .ts,
- 它只会当成语法错误的脚本。
+ 有一项到今天也办不到:浏览器只认 JS,你给它 .ts,它只会当成语法错误的脚本。
>
}
/>
@@ -921,8 +909,7 @@ export const QUIZ: QuizItem[] = [
zh={
<>
undefined 不吭声,乘个数变 NaN,NaN 再一路传染 ——
- 等它在页面上露面,案发现场早离出错那行十万八千里了。
- 这正是「编译期安检」要拦的头号危险品。
+ 等它在页面上露面,案发现场早离出错那行十万八千里了。这正是「编译期安检」要拦的头号危险品。
>
}
/>
@@ -969,8 +956,7 @@ export const QUIZ: QuizItem[] = [
<>
tsc 干两件事:检查(对照类型说明书报错)+ 翻译
(把 .ts 擦成 .js)。npx tsc --init 生成配置,
- npx tsc --noEmit 只检查不出产物 —— 这几个命令后面章节
- 会一直用。
+ npx tsc --noEmit 只检查不出产物 —— 这几个命令后面章节会一直用。
>
}
/>
@@ -1039,8 +1025,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 不存在这回事:产物就是 JS,引擎该怎么执行还怎么执行,
- 一步都没省。
+ 不存在这回事:产物就是 JS,引擎该怎么执行还怎么执行,一步都没省。
>
}
/>,
@@ -1055,8 +1040,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 也不慢 —— 擦除几乎不改动代码本身,产物性能和手写 JS 相同。
- 「快慢」这个维度上,TS 是中性的。
+ 也不慢 —— 擦除几乎不改动代码本身,产物性能和手写 JS 相同。「快慢」这个维度上,TS 是中性的。
>
}
/>,
diff --git a/lib/mindset-data.tsx b/lib/mindset-data.tsx
index 2fac2d4..e5e8c3a 100644
--- a/lib/mindset-data.tsx
+++ b/lib/mindset-data.tsx
@@ -203,11 +203,8 @@ export const LABS: Lab[] = [
打开 typescriptlang.org/play,把 §01 的 Config{" "}
和配置对象粘进去,然后做三轮:① 用 : Config 注解 ② 用{" "}
- as Config ③ 用 satisfies Config。
- 每一轮都把对象弄坏两次:把 theme 拼成{" "}
- thema、删掉 maxSugar,
- 记下哪种写法报错、哪种放行。最后恢复正确的对象,
- 悬停 config.theme,看三种写法各推断出什么。
+ as Config ③ 用 satisfies Config。每一轮都把对象弄坏两次:把 theme 拼成{" "}
+ thema、删掉 maxSugar,记下哪种写法报错、哪种放行。最后恢复正确的对象,悬停 config.theme,看三种写法各推断出什么。
>
),
@@ -223,9 +220,7 @@ export const LABS: Lab[] = [
),
zh: (
<>
- 你在填一张九格表:三种写法 × 三个问题(拼错的键、缺失的键、
- 推断出的类型)。填完应该和 §01 的三盏灯完全一致。
- 有一格对不上,先弄清楚再往下走。
+ 你在填一张九格表:三种写法 × 三个问题(拼错的键、缺失的键、推断出的类型)。填完应该和 §01 的三盏灯完全一致。有一格对不上,先弄清楚再往下走。
>
),
},
@@ -252,9 +247,7 @@ export const LABS: Lab[] = [
zh: (
<>
再做一个实验:把最后一行改成{" "}
- {"} as const satisfies Config;"} 再悬停。
- 这时每个属性都是 readonly,每个字面量都保留精确
- 类型,形状仍然按 Config 校验 ——
+ {"} as const satisfies Config;"} 再悬停。这时每个属性都是 readonly,每个字面量都保留精确类型,形状仍然按 Config 校验 ——
配置对象通常就这么写。
>
),
@@ -307,9 +300,7 @@ export const LABS: Lab[] = [
"interface Order { id: string; total: number; toppings: string[] }"
}
- ,然后手写 isOrder(x: unknown): x is Order,
- 逐个字段检查,包括 toppings 是不是「字符串数组」。
- 光用 Array.isArray 不够,它不说明元素是什么。
+ ,然后手写 isOrder(x: unknown): x is Order,逐个字段检查,包括 toppings 是不是「字符串数组」。光用 Array.isArray 不够,它不说明元素是什么。
用两份数据自测:
@@ -334,10 +325,8 @@ export const LABS: Lab[] = [
),
zh: (
<>
- 数组的套路:先 Array.isArray(o.toppings) 确认是数组,
- 再 {'o.toppings.every((t) => typeof t === "string")'}{" "}
- 检查元素。TypeScript 5.5 起,编译器能自己给这个箭头函数推断出类型谓词,
- 所以 every 之后数组会被收窄成 string[]。
+ 数组的套路:先 Array.isArray(o.toppings) 确认是数组,再 {'o.toppings.every((t) => typeof t === "string")'}{" "}
+ 检查元素。TypeScript 5.5 起,编译器能自己给这个箭头函数推断出类型谓词,所以 every 之后数组会被收窄成 string[]。
>
),
},
@@ -360,8 +349,7 @@ export const LABS: Lab[] = [
zh: (
<>
第二份数据的 total 是字符串{" "}
- {'"30"'},不是数字 30。
- 线上最常见的坏数据就是这种「只差一点点」的。
+ {'"30"'},不是数字 30。线上最常见的坏数据就是这种「只差一点点」的。
as 会放它进来,校验函数不会。
>
),
@@ -419,10 +407,8 @@ export const LABS: Lab[] = [
),
zh: (
<>
- 两条路:① 键重映射 [P in keyof T as …],
- 把黑名单上的键映射成 never;② 先用{" "}
- Exclude 过滤 keyof T,再映射剩下的键。
- 两种都写出来,才算真懂。
+ 两条路:① 键重映射 [P in keyof T as …],把黑名单上的键映射成 never;② 先用{" "}
+ Exclude 过滤 keyof T,再映射剩下的键。两种都写出来,才算真懂。
>
),
},
@@ -445,9 +431,7 @@ export const LABS: Lab[] = [
zh: (
<>
两种写法行为相同。第二种更接近标准库的真实定义:
- {"Pick>"} 。
- 去 type-challenges 的第 3 题(Omit)提交你的版本,
- 看看测试用例能不能找出你漏掉的边角。
+ {"Pick>"} 。去 type-challenges 的第 3 题(Omit)提交你的版本,看看测试用例能不能找出你漏掉的边角。
>
),
}}
@@ -485,13 +469,10 @@ export const LABS: Lab[] = [
zh: (
<>
- 在 Playground 实现 DeepReadonly<T>:
- 对象每一层的每个属性都变成 readonly。
- 验收方法:拿一个嵌套两层的配置对象,给最里层的字段赋值 ——
+ 在 Playground 实现 DeepReadonly<T>:对象每一层的每个属性都变成 readonly。验收方法:拿一个嵌套两层的配置对象,给最里层的字段赋值 ——
这一行必须报错。做完这道,去{" "}
github.com/type-challenges/type-challenges 的 easy
- 区做三道,推荐 Pick、Readonly、Tuple to Object。
- 每道题自带测试用例,当场就有结果。
+ 区做三道,推荐 Pick、Readonly、Tuple to Object。每道题自带测试用例,当场就有结果。
>
),
@@ -509,10 +490,8 @@ export const LABS: Lab[] = [
),
zh: (
<>
- 骨架:一个带 readonly 修饰符的映射类型,
- 属性类型走条件类型 —— 是对象就递归{" "}
- DeepReadonly<T[K]>,不是就原样保留。
- 小心函数:函数也是对象,想放过它,就再加一层{" "}
+ 骨架:一个带 readonly 修饰符的映射类型,属性类型走条件类型 —— 是对象就递归{" "}
+ DeepReadonly<T[K]>,不是就原样保留。小心函数:函数也是对象,想放过它,就再加一层{" "}
extends 判断。
>
),
@@ -538,9 +517,7 @@ export const LABS: Lab[] = [
),
zh: (
<>
- type-challenges 的官方判题要求更细 —— 数组和元组还有各自的
- 规则。提交你的版本,读失败的测试用例,再改。
- 这个循环比现成答案更有用。
+ type-challenges 的官方判题要求更细 —— 数组和元组还有各自的规则。提交你的版本,读失败的测试用例,再改。这个循环比现成答案更有用。
>
),
}}
@@ -633,8 +610,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 再想想 b。const 绑定不会被重新赋值,
- 编译器没有理由拓宽它,推断停在字面量类型{" "}
+ 再想想 b。const 绑定不会被重新赋值,编译器没有理由拓宽它,推断停在字面量类型{" "}
"Oolong Tea"。
>
),
@@ -650,8 +626,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- a 用 let 声明,以后可能被赋成别的字符串。
- 编译器把它拓宽成 string,不然你连改都改不了。
+ a 用 let 声明,以后可能被赋成别的字符串。编译器把它拓宽成 string,不然你连改都改不了。
>
),
},
@@ -665,8 +640,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 正好说反了:let 拓宽,const 保留字面量。
- 「会不会被改」决定「推得多宽」。
+ 正好说反了:let 拓宽,const 保留字面量。「会不会被改」决定「推得多宽」。
>
),
},
@@ -751,9 +725,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 那是没有穷尽检查时的情况:新状态被静默吞掉,
- 页面显示成一片空白,没人知道为什么。
- 那行 never 赋值就是为了打破这种沉默。
+ 那是没有穷尽检查时的情况:新状态被静默吞掉,页面显示成一片空白,没人知道为什么。那行 never 赋值就是为了打破这种沉默。
>
),
},
@@ -768,8 +740,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- never 只存在于编译期。类型被擦除,
- 运行时找不到它的任何痕迹。它的全部作用就是把错误提前到编译期。
+ never 只存在于编译期。类型被擦除,运行时找不到它的任何痕迹。它的全部作用就是把错误提前到编译期。
>
),
},
@@ -802,10 +773,8 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 这是穷尽检查(exhaustiveness check):联合的成员全部处理完之后,
- 走到 default 的值被收窄成 never,赋给{" "}
- never 合法;漏了一个,那个成员还在,赋值就失败。
- 新增状态的那一刻,所有没处理它的 switch 一起报错。—— 回看第 03 章。
+ 这是穷尽检查(exhaustiveness check):联合的成员全部处理完之后,走到 default 的值被收窄成 never,赋给{" "}
+ never 合法;漏了一个,那个成员还在,赋值就失败。新增状态的那一刻,所有没处理它的 switch 一起报错。—— 回看第 03 章。
>
),
},
@@ -823,8 +792,7 @@ export const QUIZ: QuizItem[] = [
zh: (
<>
{'order({ size: "large", ice: "none" })'} 报错说{" "}
- ice 不存在,但先把同一个对象存进变量再传就通过了。
- 为什么?
+ ice 不存在,但先把同一个对象存进变量再传就通过了。为什么?
>
),
},
@@ -843,8 +811,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 直接传的对象字面量是「新鲜」的,要做多余属性检查;
- 存进变量之后只按普通的结构兼容判断,多出来的属性不追究
+ 直接传的对象字面量是「新鲜」的,要做多余属性检查;存进变量之后只按普通的结构兼容判断,多出来的属性不追究
>
),
},
@@ -877,8 +844,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 没有「优先级」这回事。两种情况走的检查不同:
- 新鲜的字面量在普通结构比较之外,多受一道检查。
+ 没有「优先级」这回事。两种情况走的检查不同:新鲜的字面量在普通结构比较之外,多受一道检查。
>
),
},
@@ -894,8 +860,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 这是刻意设计:在调用处现写的字面量,多出来的属性几乎都是拼错
- 或误解,值得报出来;变量可能在别处另有用途,多几个属性是合法的。
+ 这是刻意设计:在调用处现写的字面量,多出来的属性几乎都是拼错或误解,值得报出来;变量可能在别处另有用途,多几个属性是合法的。
>
),
},
@@ -909,8 +874,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- const 对象的类型检查一点不少,少的只是多余属性检查
- 这一项。结构兼容仍然生效:少了属性照样报错。
+ const 对象的类型检查一点不少,少的只是多余属性检查这一项。结构兼容仍然生效:少了属性照样报错。
>
),
},
@@ -1030,8 +994,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 数组也有 length,T 推断成 number[]。
- 这个调用合法。
+ 数组也有 length,T 推断成 number[]。这个调用合法。
>
),
},
@@ -1046,8 +1009,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 这个对象的形状里明写着 length: number。
- TypeScript 比较形状而不是名字,谁提供了 length{" "}
+ 这个对象的形状里明写着 length: number。TypeScript 比较形状而不是名字,谁提供了 length{" "}
谁就满足约束。
>
),
@@ -1064,10 +1026,8 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- extends {"{ length: number }"} 是类型参数的约束:
- 传进来的类型必须有一个 number 类型的{" "}
- length 属性。number 没有 length,
- 调用被拦下。—— 回看第 05 章。
+ extends {"{ length: number }"} 是类型参数的约束:传进来的类型必须有一个 number 类型的{" "}
+ length 属性。number 没有 length,调用被拦下。—— 回看第 05 章。
>
),
},
@@ -1084,8 +1044,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 要做草稿功能:没填完的 Order 也要能保存,每个字段都允许先空着。
- 哪个工具类型是为这件事准备的?
+ 要做草稿功能:没填完的 Order 也要能保存,每个字段都允许先空着。哪个工具类型是为这件事准备的?
>
),
},
@@ -1152,8 +1111,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- Required 方向相反:它把所有可选字段变成必填。
- 那是提交前做最后校验用的,不是保存草稿用的。
+ Required 方向相反:它把所有可选字段变成必填。那是提交前做最后校验用的,不是保存草稿用的。
>
),
},
@@ -1167,8 +1125,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- Readonly 管的是「能不能改」,不是「能不能空」。
- 草稿恰恰是要反复改的。
+ Readonly 管的是「能不能改」,不是「能不能空」。草稿恰恰是要反复改的。
>
),
},
@@ -1182,8 +1139,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- Pick 是挑几个字段组成新类型。草稿不是「只剩 id」,
- 而是「每个字段都可以先没有」。
+ Pick 是挑几个字段组成新类型。草稿不是「只剩 id」,而是「每个字段都可以先没有」。
>
),
},
@@ -1264,8 +1220,7 @@ export const QUIZ: QuizItem[] = [
<>
typeof(用在类型位置)从值里取出类型,
keyof 再把键收成联合:
- {'"oolong" | "mango"'}。菜单只写一遍,
- 名字的类型从菜单里长出来;菜单改了,类型跟着改。—— 回看第 07 章。
+ {'"oolong" | "mango"'}。菜单只写一遍,名字的类型从菜单里长出来;菜单改了,类型跟着改。—— 回看第 07 章。
>
),
},
@@ -1403,8 +1358,7 @@ export const QUIZ: QuizItem[] = [
zh: (
<>
useUnknownInCatchVariables(strict 家族成员)让 catch
- 变量是 unknown:一种类型安全的「不知道」。
- 用之前必须收窄,通常是 e instanceof Error。
+ 变量是 unknown:一种类型安全的「不知道」。用之前必须收窄,通常是 e instanceof Error。
—— 回看第 10 章。
>
),
@@ -1424,8 +1378,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 写一个主题配置对象:既要编译器校验它符合 Config,
- 又要 config.theme 保持字面量类型{" "}
+ 写一个主题配置对象:既要编译器校验它符合 Config,又要 config.theme 保持字面量类型{" "}
"dark",方便后续做精确判断。用哪种写法?
>
),
@@ -1526,8 +1479,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 什么都不加,推断确实精确,但没有任何校验:字段拼错、漏写,
- 要到使用它的地方才暴露,离你写下它的位置很远。
+ 什么都不加,推断确实精确,但没有任何校验:字段拼错、漏写,要到使用它的地方才暴露,离你写下它的位置很远。
>
),
},
@@ -1544,8 +1496,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- satisfies(TypeScript 4.9)校验形状,同时保留推断出的
- 字面量类型 —— 只有它两样都给。一句话:注解检查但拓宽,
+ satisfies(TypeScript 4.9)校验形状,同时保留推断出的字面量类型 —— 只有它两样都给。一句话:注解检查但拓宽,
as 既不检查也不保留,satisfies 两样都做。
—— 回看本章 §01。
>
@@ -1618,8 +1569,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 迁移期在局部用 any 可以接受,
- 只要它不出现在导出的函数签名上
+ 迁移期在局部用 any 可以接受,只要它不出现在导出的函数签名上
>
),
},
@@ -1649,8 +1599,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 还漏了一条。想想 any 最麻烦的性质:
- 它会顺着赋值和属性访问一路传下去。另外「别进导出签名」那条也别漏。
+ 还漏了一条。想想 any 最麻烦的性质:它会顺着赋值和属性访问一路传下去。另外「别进导出签名」那条也别漏。
>
),
},
@@ -1666,8 +1615,7 @@ export const QUIZ: QuizItem[] = [
zh: (
<>
有一项选多了。要么是把 unknown 和 any{" "}
- 当成了一回事(它们对「怎么用这个值」的规定完全不同),
- 要么是高估了编译器对运行时数据的了解(类型被擦除了,它管不到)。
+ 当成了一回事(它们对「怎么用这个值」的规定完全不同),要么是高估了编译器对运行时数据的了解(类型被擦除了,它管不到)。
>
),
},
@@ -1688,11 +1636,8 @@ export const QUIZ: QuizItem[] = [
zh: (
<>
unknown 接收任何值,但用之前必须收窄;any{" "}
- 接收任何值且放行任何用法,还会扩散。原则:先用 unknown,
- 只有当它让代码写不下去时才降级到 any,
- 并且锁在最小的作用域里。E 是错的:res.json() 在 DOM
- 库里的类型是 Promise<any>,
- 真实形状只有在运行时被检查过才知道。—— 回看本章 §03、§04。
+ 接收任何值且放行任何用法,还会扩散。原则:先用 unknown,只有当它让代码写不下去时才降级到 any,并且锁在最小的作用域里。E 是错的:res.json() 在 DOM
+ 库里的类型是 Promise<any>,真实形状只有在运行时被检查过才知道。—— 回看本章 §03、§04。
>
),
},
@@ -1799,8 +1744,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- require 是 CommonJS 的运行时函数,导入的是值,
- 擦不掉,和类型导入完全是两回事。
+ require 是 CommonJS 的运行时函数,导入的是值,擦不掉,和类型导入完全是两回事。
>
),
},
@@ -1814,8 +1758,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 命名空间导入拿进来的是整个模块的值。你只想要一个类型,
- 却把全部实现搬了进来 —— 这正是 import type 要避免的。
+ 命名空间导入拿进来的是整个模块的值。你只想要一个类型,却把全部实现搬了进来 —— 这正是 import type 要避免的。
>
),
},
@@ -1831,8 +1774,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- import type 明确表示这一行只有类型,
- 编译时整行删除,打包器也不用猜。
+ import type 明确表示这一行只有类型,编译时整行删除,打包器也不用猜。
verbatimModuleSyntax(TypeScript 5.0)
把这个习惯变成了硬规定。—— 回看第 09 章、第 10 章。
>
@@ -1867,8 +1809,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- as const 的作用就是「一点都不拓宽」。
- 推断停在你写下的那个字面量上,同时每个属性都变成{" "}
+ as const 的作用就是「一点都不拓宽」。推断停在你写下的那个字面量上,同时每个属性都变成{" "}
readonly。
>
),
@@ -1885,10 +1826,8 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- as const 把推断钉在字面量 {'"dark"'} 上,
- 并加上 readonly。没有它,对象属性会被拓宽成{" "}
- string。配合 satisfies,
- 就是配置对象最常见的写法。—— 回看本章 §01。
+ as const 把推断钉在字面量 {'"dark"'} 上,并加上 readonly。没有它,对象属性会被拓宽成{" "}
+ string。配合 satisfies,就是配置对象最常见的写法。—— 回看本章 §01。
>
),
},
@@ -1934,8 +1873,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 不能 —— interface 在编译时被擦除,
- 运行时的判断只能检查值本身(类型谓词或校验函数)
+ 不能 —— interface 在编译时被擦除,运行时的判断只能检查值本身(类型谓词或校验函数)
>
),
},
@@ -1977,8 +1915,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 恰好相反:interface 不产生任何 JavaScript,
- 在编译产物里一个字节都找不到。
+ 恰好相反:interface 不产生任何 JavaScript,在编译产物里一个字节都找不到。
>
),
},
@@ -1993,8 +1930,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- strict 管的是编译期检查有多严,变不出运行时的类。
- 擦除对所有配置一视同仁。
+ strict 管的是编译期检查有多严,变不出运行时的类。擦除对所有配置一视同仁。
>
),
},
@@ -2008,8 +1944,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 这件事上 type 和 interface 完全一样:
- 都只存在于编译期,都会被擦除。运行时只能检查值本身的形状。
+ 这件事上 type 和 interface 完全一样:都只存在于编译期,都会被擦除。运行时只能检查值本身的形状。
>
),
},
@@ -2028,10 +1963,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 类型擦除既是这门课的第一课,也是最后一课:类型存在于你写代码和
- 编译的阶段;真正运行的是普通 JavaScript,里面只有值。
- 所以边界上需要你手写校验(本章 §03 的 isOrder);
- 也所以可辨识联合靠的是 status 这个
+ 类型擦除既是这门课的第一课,也是最后一课:类型存在于你写代码和编译的阶段;真正运行的是普通 JavaScript,里面只有值。所以边界上需要你手写校验(本章 §03 的 isOrder);也所以可辨识联合靠的是 status 这个
真实存在的属性,而不是类型的名字。—— 回看序章、本章 §03。
>
),
diff --git a/lib/modules-data.tsx b/lib/modules-data.tsx
index e24a46f..0ac6b53 100644
--- a/lib/modules-data.tsx
+++ b/lib/modules-data.tsx
@@ -181,8 +181,7 @@ export const LABS: Lab[] = [
在一个空目录里建两个文件:a.ts 里写{" "}
const TAX = 0.06;,b.ts 里写{" "}
- const TAX = 0.08;,两个文件都不写任何 import / export。
- 先跑 npx tsc --noEmit a.ts b.ts;再往 b.ts{" "}
+ const TAX = 0.08;,两个文件都不写任何 import / export。先跑 npx tsc --noEmit a.ts b.ts;再往 b.ts{" "}
顶上加一行 export {"{}"},重跑一次。
TAX 这个名字的归属发生了什么变化?
@@ -198,8 +197,7 @@ export const LABS: Lab[] = [
),
zh: (
<>
- 第一次会在两个文件里各报一次同样的错。这就是线索:
- 两个声明在同一个作用域里,所以它们互相看得见。
+ 第一次会在两个文件里各报一次同样的错。这就是线索:两个声明在同一个作用域里,所以它们互相看得见。
>
),
},
@@ -228,11 +226,8 @@ export const LABS: Lab[] = [
一个顶层既没有 import 也没有 export{" "}
- 的文件是脚本。它的顶层声明会进入全局作用域,
- 而程序里所有脚本共用这一个作用域,
- 所以两个脚本声明同名变量就会撞车。加上 export {"{}"}{" "}
- 之后,文件变成模块:顶层的名字只属于这个文件,
- 别的文件只有在你导出、它导入之后才看得见。
+ 的文件是脚本。它的顶层声明会进入全局作用域,而程序里所有脚本共用这一个作用域,所以两个脚本声明同名变量就会撞车。加上 export {"{}"}{" "}
+ 之后,文件变成模块:顶层的名字只属于这个文件,别的文件只有在你导出、它导入之后才看得见。
本章后面的内容都建立在这个区分之上 ——
@@ -315,8 +310,7 @@ export const LABS: Lab[] = [
export declare function cheapest(items: MenuItem[]): MenuItem;
{" "}
- —— 只有签名,没有函数体;interface 则被原样抄了过去。
- .JS 面板正相反:函数体在,interface 不见了。
+ —— 只有签名,没有函数体;interface 则被原样抄了过去。.JS 面板正相反:函数体在,interface 不见了。
同一份源码,两样产物:.js 是真正运行的代码,
@@ -348,9 +342,7 @@ export const LABS: Lab[] = [
),
zh: (
- 找个空目录,写一个 order.ts,导出一个函数和一个 interface。
- 然后跑 npx tsc order.ts --declaration,
- 打开生成的两个文件,和源码对照着看。
+ 找个空目录,写一个 order.ts,导出一个函数和一个 interface。然后跑 npx tsc order.ts --declaration,打开生成的两个文件,和源码对照着看。
),
},
@@ -364,8 +356,7 @@ export const LABS: Lab[] = [
),
zh: (
<>
- 产物应该有两个:order.js 和 order.d.ts。
- 只想要声明文件?再加 --emitDeclarationOnly。
+ 产物应该有两个:order.js 和 order.d.ts。只想要声明文件?再加 --emitDeclarationOnly。
>
),
},
@@ -404,8 +395,7 @@ export const LABS: Lab[] = [
TS 写的库「出厂自带类型」就是这么做的:编译时打开{" "}
declaration,再用 package.json 的{" "}
- types 字段指到这份 .d.ts。
- 这就是类型三个来源里的第一个。
+ types 字段指到这份 .d.ts。这就是类型三个来源里的第一个。
>
),
@@ -438,8 +428,7 @@ export const LABS: Lab[] = [
新建目录,跑 npm init -y,再 npm i lodash{" "}
—— 注意先别装 @types/lodash。写一个{" "}
index.ts 导入 debounce,跑{" "}
- npx tsc --noEmit --strict index.ts,把报错读全。
- 然后 npm i -D @types/lodash,重跑一次。
+ npx tsc --noEmit --strict index.ts,把报错读全。然后 npm i -D @types/lodash,重跑一次。
),
},
@@ -477,8 +466,7 @@ export const LABS: Lab[] = [
@types/lodash 装好的那一刻,编辑器里{" "}
- debounce 的补全和参数提示同时出现。
- 库本体没变,变的只是类型声明到货了。打开{" "}
+ debounce 的补全和参数提示同时出现。库本体没变,变的只是类型声明到货了。打开{" "}
node_modules/@types/lodash 看一眼:全是{" "}
.d.ts,一行实现都没有。
@@ -536,10 +524,8 @@ export const LABS: Lab[] = [
),
zh: (
<>
- 把 .d.ts 和源文件一起交给 tsc。
- 另外确认任何上级目录的 node_modules 里都没有真的{" "}
- boba-sdk,否则 Node 会把它找出来。
- 类型检查会通过,Node 会失败 —— 想清楚这两个结果为什么不矛盾。
+ 把 .d.ts 和源文件一起交给 tsc。另外确认任何上级目录的 node_modules 里都没有真的{" "}
+ boba-sdk,否则 Node 会把它找出来。类型检查会通过,Node 会失败 —— 想清楚这两个结果为什么不矛盾。
>
),
},
@@ -574,12 +560,10 @@ export const LABS: Lab[] = [
/>
- 声明是对「运行时有什么」的一个断言。编译器只能采信它,无从核实。
- Node 从不读 .d.ts —— 它去找真正的模块,找不到就报错。
+ 声明是对「运行时有什么」的一个断言。编译器只能采信它,无从核实。Node 从不读 .d.ts —— 它去找真正的模块,找不到就报错。
- 所以 declare module 面对的是装了但没有类型的包,
- 而不是一种「不装也能导入」的办法。
+ 所以 declare module 面对的是装了但没有类型的包,而不是一种「不装也能导入」的办法。
>
),
@@ -650,9 +634,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 那是加上 export {"{}"} 之后的情况。
- 顶层没有任何 import / export 时,这个文件根本不算模块,
- 它的名字是共享的,不是私有的。
+ 那是加上 export {"{}"} 之后的情况。顶层没有任何 import / export 时,这个文件根本不算模块,它的名字是共享的,不是私有的。
>
),
},
@@ -682,8 +664,7 @@ export const QUIZ: QuizItem[] = [
zh: (
<>
module 决定的是输出格式(esnext、
- commonjs 等)。某个文件到底是不是模块,由文件自己决定:
- 顶层有没有 import / export?
+ commonjs 等)。某个文件到底是不是模块,由文件自己决定:顶层有没有 import / export?
>
),
},
@@ -707,8 +688,7 @@ export const QUIZ: QuizItem[] = [
error TS2451: Cannot redeclare block-scoped variable 'TAX'.
{" "}
- 加一行 export {"{}"} 让文件获得模块作用域,报错就消失。
- 本章所有关于声明文件的内容都建立在这个区分之上。
+ 加一行 export {"{}"} 让文件获得模块作用域,报错就消失。本章所有关于声明文件的内容都建立在这个区分之上。
>
),
},
@@ -762,8 +742,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 一行实现都不允许。declare 的含义正是「实现在别处」。
- 带实现的那份文件叫 .ts。
+ 一行实现都不允许。declare 的含义正是「实现在别处」。带实现的那份文件叫 .ts。
>
),
},
@@ -795,9 +774,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- .d.ts 描述每个导出长什么样,自己不产出任何代码;
- 真正运行的代码在旁边那份 .js 里。
- 因为描述形状不需要改动代码,.d.ts{" "}
+ .d.ts 描述每个导出长什么样,自己不产出任何代码;真正运行的代码在旁边那份 .js 里。因为描述形状不需要改动代码,.d.ts{" "}
可以描述任何 JavaScript 库 —— 包括从来不是用 TypeScript 写的库。
>
),
@@ -869,8 +846,7 @@ export const QUIZ: QuizItem[] = [
zh: (
<>
那就成了值导入,运行时会真的去加载 ./order。
- type 的意义正是声明「这一行只有类型」,
- 所以整行都可以删掉。
+ type 的意义正是声明「这一行只有类型」,所以整行都可以删掉。
>
),
},
@@ -884,8 +860,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- require 是输出格式为 CommonJS 时才出现的东西,
- 那是另一个问题。纯类型导入在轮到输出格式之前就已经被删掉了。
+ require 是输出格式为 CommonJS 时才出现的东西,那是另一个问题。纯类型导入在轮到输出格式之前就已经被删掉了。
>
),
},
@@ -899,8 +874,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 浏览器不认识 TypeScript 的 type 关键字,
- 所以这一行必须在编译期处理掉,没有任何东西留给运行时决定。
+ 浏览器不认识 TypeScript 的 type 关键字,所以这一行必须在编译期处理掉,没有任何东西留给运行时决定。
>
),
},
@@ -916,10 +890,8 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 类型会被擦除,所以纯类型导入没有任何东西可导入。
- 删掉这一行同时也删掉了那次模块加载 —— 这一点很重要:
- .js 模块被加载时可以执行代码,
- 而你不希望仅仅因为需要一个类型名字就触发它。
+ 类型会被擦除,所以纯类型导入没有任何东西可导入。删掉这一行同时也删掉了那次模块加载 —— 这一点很重要:
+ .js 模块被加载时可以执行代码,而你不希望仅仅因为需要一个类型名字就触发它。
>
),
},
@@ -949,8 +921,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- esbuild、SWC 这类单文件转译器分不清一个名字是值还是类型,
- 显式标注让它们不必猜
+ esbuild、SWC 这类单文件转译器分不清一个名字是值还是类型,显式标注让它们不必猜
>
),
},
@@ -985,8 +956,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 这一行编译后就没了,谈不上加载。
- 收益在于构建工具不用猜,而不是运行时速度。
+ 这一行编译后就没了,谈不上加载。收益在于构建工具不用猜,而不是运行时速度。
>
),
},
@@ -1000,8 +970,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 类型永远到不了运行时,擦除是无条件的。真正的风险方向相反:
- 工具猜错,把某个值需要的导入删掉了,程序在运行时才失败。
+ 类型永远到不了运行时,擦除是无条件的。真正的风险方向相反:工具猜错,把某个值需要的导入删掉了,程序在运行时才失败。
>
),
},
@@ -1016,8 +985,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 只用 tsc 编译的项目,差别确实不大。
- 一旦接入单文件转译器,它就成了正确性问题 ——
+ 只用 tsc 编译的项目,差别确实不大。一旦接入单文件转译器,它就成了正确性问题 ——
这也是 TypeScript 5.0 加入 verbatimModuleSyntax{" "}
把规则明确下来的原因。
>
@@ -1134,8 +1102,7 @@ export const QUIZ: QuizItem[] = [
zh: (
<>
包已经装好了 —— 没装的话,报错会是{" "}
- ts(2307): Cannot find module 'lodash'。
- 缺的是类型声明,重装并不会带来它。
+ ts(2307): Cannot find module 'lodash'。缺的是类型声明,重装并不会带来它。
>
),
},
@@ -1149,9 +1116,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 能编译通过,但 lodash 的每个函数从此都是 any:
- 没有补全、不检查参数、没有返回类型。
- 为了消掉一行报错,代价太大。
+ 能编译通过,但 lodash 的每个函数从此都是 any:没有补全、不检查参数、没有返回类型。为了消掉一行报错,代价太大。
>
),
},
@@ -1164,8 +1129,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 为了一个库,把整个项目的检查都削弱了。
- 而正确的解法,报错原文里已经写着。
+ 为了一个库,把整个项目的检查都削弱了。而正确的解法,报错原文里已经写着。
>
),
},
@@ -1181,9 +1145,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 按顺序检查三个来源:包自带 → @types → 自己写声明。
- lodash 是老牌库,@types/lodash 一定有。
- 一条命令,类型、补全和文档提示就都回来了。
+ 按顺序检查三个来源:包自带 → @types → 自己写声明。lodash 是老牌库,@types/lodash 一定有。一条命令,类型、补全和文档提示就都回来了。
>
),
},
@@ -1215,8 +1177,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 两个英文单词连写,在 GitHub 上。
- 这个名字是在讲它收录的那些库:现在它们「确实有类型了」。
+ 两个英文单词连写,在 GitHub 上。这个名字是在讲它收录的那些库:现在它们「确实有类型了」。
>
),
},
@@ -1232,8 +1193,7 @@ export const QUIZ: QuizItem[] = [
zh: (
<>
DefinitelyTyped 收录了几千个 JavaScript 库的社区声明,并发布成{" "}
- @types/*。你也可以给它提 PR,
- 让下一个人不必再写一遍同样的声明。
+ @types/*。你也可以给它提 PR,让下一个人不必再写一遍同样的声明。
>
),
},
@@ -1302,9 +1262,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- @types 包里只有类型,不改变任何运行时能力。
- Node 能直接跑 TypeScript 是 Node 自己的功能
- (类型剥离,Node 22.6 加入),和这个包无关。
+ @types 包里只有类型,不改变任何运行时能力。Node 能直接跑 TypeScript 是 Node 自己的功能(类型剥离,Node 22.6 加入),和这个包无关。
>
),
},
@@ -1318,8 +1276,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- DOM 的类型随 TypeScript 一起发布,在 lib.dom.d.ts 里,
- 由 lib 选项控制 —— 完全是另一个来源。
+ DOM 的类型随 TypeScript 一起发布,在 lib.dom.d.ts 里,由 lib 选项控制 —— 完全是另一个来源。
>
),
},
@@ -1352,8 +1309,7 @@ export const QUIZ: QuizItem[] = [
zh: (
<>
fs、path、process{" "}
- 不属于 JavaScript 语言本身,它们由 Node 提供,
- 所以声明不在 TypeScript 自带的那几本 lib 里,而来自{" "}
+ 不属于 JavaScript 语言本身,它们由 Node 提供,所以声明不在 TypeScript 自带的那几本 lib 里,而来自{" "}
@types/node。这也是几乎每个 Node 项目都要装它的原因。
>
),
@@ -1466,10 +1422,8 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- declare 只陈述类型、不产出代码,
- 所以断言错了,失败会发生在运行时、发生在你的程序里。
- declare global 是从模块内部触及全局作用域的正确写法,
- 写在别处会报 ts(2669)。至于模块标识,
+ declare 只陈述类型、不产出代码,所以断言错了,失败会发生在运行时、发生在你的程序里。
+ declare global 是从模块内部触及全局作用域的正确写法,写在别处会报 ts(2669)。至于模块标识,
declare module 接受任何能用来导入模块的字符串 ——
包名、路径,或者像 "*.css" 这样的通配符。
>
@@ -1616,12 +1570,8 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- paths 是编译期的映射,
- 它只告诉编译器某个标识指向哪个文件。真正运行代码的那一方 ——
- Node、打包器、测试运行器 —— 需要一份自己的对应配置,否则导入就会失败。
- Next.js、Vite、webpack 会读 tsconfig.json 的 paths,
- 或者提供等价的别名选项;这就是为什么别名在应用里通常好用,
- 却在你第一次用纯 node 跑产物时失败。
+ paths 是编译期的映射,它只告诉编译器某个标识指向哪个文件。真正运行代码的那一方 ——
+ Node、打包器、测试运行器 —— 需要一份自己的对应配置,否则导入就会失败。Next.js、Vite、webpack 会读 tsconfig.json 的 paths,或者提供等价的别名选项;这就是为什么别名在应用里通常好用,却在你第一次用纯 node 跑产物时失败。
>
),
},
@@ -1697,8 +1647,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 方向反了。参数传错、方法名拼错,照样报错。
- 跳过的是声明之间的互相检查,不是「你用得对不对」。
+ 方向反了。参数传错、方法名拼错,照样报错。跳过的是声明之间的互相检查,不是「你用得对不对」。
>
),
},
@@ -1712,8 +1661,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- node_modules 里的 JavaScript 本来就不参与类型检查,
- 除非你打开 allowJs 并把它 include 进来。
+ node_modules 里的 JavaScript 本来就不参与类型检查,除非你打开 allowJs 并把它 include 进来。
skipLibCheck 管的是 .d.ts。
>
),
@@ -1749,11 +1697,8 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 声明文件之间会互相冲突:两个 @types 包可能用不同的类型
- 声明了同一个全局变量,而这两份你都改不了。
- skipLibCheck 跳过这类冲突,同时让编译更快。
- 代价真实存在但不大:声明文件内部真有错也不会报出来,
- 你可能只能在调用处发现某个类型不对劲。
+ 声明文件之间会互相冲突:两个 @types 包可能用不同的类型声明了同一个全局变量,而这两份你都改不了。
+ skipLibCheck 跳过这类冲突,同时让编译更快。代价真实存在但不大:声明文件内部真有错也不会报出来,你可能只能在调用处发现某个类型不对劲。
>
),
},
@@ -1773,8 +1718,7 @@ export const QUIZ: QuizItem[] = [
<>
项目里只有 helpers.d.ts,没有对应的{" "}
helpers.js。你写{" "}
- import {"{ helper }"} from "./helpers",
- 然后用 Node 运行产物,结果是?
+ import {"{ helper }"} from "./helpers",然后用 Node 运行产物,结果是?
>
),
},
@@ -1835,8 +1779,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 编译器不会拦。声明文件在,形状也对得上,导入就解析成功了。
- 这正是这个坑容易漏过去的地方:一路绿灯,直到运行才出问题。
+ 编译器不会拦。声明文件在,形状也对得上,导入就解析成功了。这正是这个坑容易漏过去的地方:一路绿灯,直到运行才出问题。
>
),
},
@@ -1850,8 +1793,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- .d.ts 不产出任何东西,没有实现可用。
- 声明写得再详细,也变不出代码。
+ .d.ts 不产出任何东西,没有实现可用。声明写得再详细,也变不出代码。
>
),
},
@@ -1864,8 +1806,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 根本走不到那一步。模块本身不存在,
- 导入这一步就失败了,轮不到读取任何绑定。
+ 根本走不到那一步。模块本身不存在,导入这一步就失败了,轮不到读取任何绑定。
>
),
},
@@ -1892,8 +1833,7 @@ export const QUIZ: QuizItem[] = [
'.../helpers.js'
,在 CommonJS 下报{" "}
- Error: Cannot find module './helpers'。
- 无论哪种,声明只能让编译器满意。
+ Error: Cannot find module './helpers'。无论哪种,声明只能让编译器满意。
>
),
},
diff --git a/lib/narrowing-data.tsx b/lib/narrowing-data.tsx
index 373ebe5..0784f7e 100644
--- a/lib/narrowing-data.tsx
+++ b/lib/narrowing-data.tsx
@@ -234,11 +234,8 @@ export const LABS: Lab[] = [
打开 TypeScript Playground(typescriptlang.org/play),写一个{" "}
{"function shout(id: string | number)"}
- ,函数体里直接 return id.toUpperCase()。
- 读一读报错,注意它有两行:第二行点名了到底是谁缺这个成员。
- 然后用 typeof 把它修好。修好后,把光标分别悬停在{" "}
- if 里面和 if 之后的 id 上,
- 对比这两个类型。
+ ,函数体里直接 return id.toUpperCase()。读一读报错,注意它有两行:第二行点名了到底是谁缺这个成员。然后用 typeof 把它修好。修好后,把光标分别悬停在{" "}
+ if 里面和 if 之后的 id 上,对比这两个类型。
toUpperCase。
- 修的办法是:写一道检查,让这条路径上只剩一种可能。
+ 报错的第二行会告诉你联合里哪个成员缺 toUpperCase。修的办法是:写一道检查,让这条路径上只剩一种可能。
>
}
/>
@@ -314,14 +310,8 @@ export const LABS: Lab[] = [
zh={
在 Playground 里先声明{" "}
- {"declare function later(fn: () => void): void;"}。
- 写一个接收 x: string | number 的函数,把它复制进{" "}
- let value = x,用 typeof 收窄,
- 然后调用两次 value.toUpperCase():
- 一次直接调用,一次放在传给 later 的回调里面。
- 最后在函数末尾加一行 value = 42。
- 这时只有一次调用会报错。再把 value = 42 删掉,
- 看那个报错消失。
+ {"declare function later(fn: () => void): void;"}。写一个接收 x: string | number 的函数,把它复制进{" "}
+ let value = x,用 typeof 收窄,然后调用两次 value.toUpperCase():一次直接调用,一次放在传给 later 的回调里面。最后在函数末尾加一行 value = 42。这时只有一次调用会报错。再把 value = 42 删掉,看那个报错消失。
{"{ note?: string }"} 类型,写{" "}
- if (d.note),再在回调里用 d.note。
- 这一次一定会报{" "}
+ if (d.note),再在回调里用 d.note。这一次一定会报{" "}
'd.note' is possibly 'undefined'
,哪怕代码里没有任何重新赋值 ——
因为属性随时可能被函数外面的代码改掉。
@@ -405,9 +392,7 @@ export const LABS: Lab[] = [
zh={
定义一个三状态的 Order 类型:pending(只有{" "}
- createdAt)、paid(多一个 paidAt)、
- delivered(再多一个 deliveredAt)。
- 每个成员的 status 都写成字面量类型。然后写{" "}
+ createdAt)、paid(多一个 paidAt)、delivered(再多一个 deliveredAt)。每个成员的 status 都写成字面量类型。然后写{" "}
{"function report(order: Order): string"},对{" "}
status 做 switch。在每个 case 里悬停{" "}
order,确认形状不一样。最后故意在 pending 分支里读{" "}
@@ -452,8 +437,7 @@ export const LABS: Lab[] = [
zh={
<>
现在把 status 改成 string{" "}
- 再看:order 在所有 case 里都不再收窄,
- 每一处 paidAt 都会报错。宽泛的{" "}
+ 再看:order 在所有 case 里都不再收窄,每一处 paidAt 都会报错。宽泛的{" "}
string 区分不了成员,所以标签字段必须是字面量类型
—— 这正是三个条件里的第二条。
>
@@ -491,11 +475,9 @@ export const LABS: Lab[] = [
zh={
接着上一题。给 switch 补一个 default 分支,里面写{" "}
- const _exhaustive: never = order;,确认没有报错。
- 然后给 Order 加第四个成员{" "}
+ const _exhaustive: never = order;,确认没有报错。然后给 Order 加第四个成员{" "}
{'{ status: "refunded"; refundedAt: Date }'},并且
- 不要动 report。看编译器把错报在哪一行、说了什么。
- 最后补上缺的 case,看报错消失。
+ 不要动 report。看编译器把错报在哪一行、说了什么。最后补上缺的 case,看报错消失。
Order 类型:① 先写{" "}
{'const paid = orders.filter((o) => o.status === "paid")'}
,悬停 paid 看类型;② 写一个谓词函数{" "}
- {"function isPaid(o: Order): o is Paid"},
- 用它再 filter 一次,对比结果;③ 把 Playground 的 TypeScript
+ {"function isPaid(o: Order): o is Paid"},用它再 filter 一次,对比结果;③ 把 Playground 的 TypeScript
版本切到 5.5 以下,再看第 ① 步的类型。
}
@@ -586,8 +566,7 @@ export const LABS: Lab[] = [
}
zh={
<>
- 版本选择器在 Playground 顶部。第 ① 步的答案随版本变化,
- 亲眼看到这个差别正是这道题的重点。
+ 版本选择器在 Playground 顶部。第 ① 步的答案随版本变化,亲眼看到这个差别正是这道题的重点。
>
}
/>
@@ -610,8 +589,7 @@ export const LABS: Lab[] = [
<>
从 TS 5.5 起,「单参数、直接返回一个收窄表达式」的 filter
回调会自动获得谓词。更长的逻辑仍然需要显式的{" "}
- is。而一旦手写,编译器就无条件信你:
- 函数体写错了,没有任何东西会提醒你。
+ is。而一旦手写,编译器就无条件信你:函数体写错了,没有任何东西会提醒你。
>
}
/>
@@ -696,8 +674,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 当然允许。只要确定 id 是字符串,这个调用就没问题。
- 问题在于现在还不确定。
+ 当然允许。只要确定 id 是字符串,这个调用就没问题。问题在于现在还不确定。
>
}
/>,
@@ -713,9 +690,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 共有成员可以直接调用。toString 就没问题,
- 因为 string 和 number 都有它。
- 被挡住的只是「有成员没有」的那些。
+ 共有成员可以直接调用。toString 就没问题,因为 string 和 number 都有它。被挡住的只是「有成员没有」的那些。
>
}
/>,
@@ -846,9 +821,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- typeof null === "object"。
- 这来自 1995 年 JavaScript 的第一版实现,为了兼容性一直保留。
- 所以判断对象的完整写法是{" "}
+ typeof null === "object"。这来自 1995 年 JavaScript 的第一版实现,为了兼容性一直保留。所以判断对象的完整写法是{" "}
x !== null && typeof x === "object"
@@ -929,8 +902,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 0 确实是合法的 number,但真值检查看的不是类型,
- 而是这个值真不真 —— 0 不真。
+ 0 确实是合法的 number,但真值检查看的不是类型,而是这个值真不真 —— 0 不真。
>
}
/>,
@@ -945,8 +917,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 真值检查对任何类型都合法,这正是它危险的地方。
- 编译器一声不吭,坑就这么安静地埋下了。
+ 真值检查对任何类型都合法,这正是它危险的地方。编译器一声不吭,坑就这么安静地埋下了。
>
}
/>,
@@ -960,8 +931,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- if (0) 在运行时平静地得到 false,不会有任何错误。
- 没有异常,只有走错的分支。
+ if (0) 在运行时平静地得到 false,不会有任何错误。没有异常,只有走错的分支。
>
}
/>,
@@ -981,8 +951,7 @@ export const QUIZ: QuizItem[] = [
<>
真值检查会一次排除所有 falsy 值:undefined、
null、0、""、
- NaN。「0 件」是数据,不是没填。
- 如果你只想排除 undefined,就写{" "}
+ NaN。「0 件」是数据,不是没填。如果你只想排除 undefined,就写{" "}
count !== undefined。
>
}
@@ -1005,10 +974,8 @@ export const QUIZ: QuizItem[] = [
zh={
<>
x 的类型是 string | number。在{" "}
- if (typeof x === "string") 里,
- 你给 setTimeout 传了一个调用{" "}
- x.toUpperCase() 的回调;同一个函数里的别处,
- 你还写了 x = 42。会发生什么?
+ if (typeof x === "string") 里,你给 setTimeout 传了一个调用{" "}
+ x.toUpperCase() 的回调;同一个函数里的别处,你还写了 x = 42。会发生什么?
>
}
/>
@@ -1067,9 +1034,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 回调写在哪里并不重要,重要的是它什么时候执行,
- 而编译器无法知道。既然 x 还会被重新赋值,
- 到那时那道检查可能已经不成立了。
+ 回调写在哪里并不重要,重要的是它什么时候执行,而编译器无法知道。既然 x 还会被重新赋值,到那时那道检查可能已经不成立了。
>
}
/>,
@@ -1086,8 +1051,7 @@ export const QUIZ: QuizItem[] = [
zh={
<>
对一个 string | number 类型的变量来说,{" "}
- x = 42 是完全合法的赋值。
- 它是报错的原因,而不是报错的位置。
+ x = 42 是完全合法的赋值。它是报错的原因,而不是报错的位置。
>
}
/>,
@@ -1101,8 +1065,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 回调里是会收窄的,但仅限于从未被重新赋值的变量。
- 把 x = 42 删掉,报错就没了。
+ 回调里是会收窄的,但仅限于从未被重新赋值的变量。把 x = 42 删掉,报错就没了。
>
}
/>,
@@ -1121,10 +1084,8 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 只有当变量不可能改变时,编译器才会把收窄结果带进嵌套函数。
- 函数里任何一处重新赋值都足以让它失效。被收窄的对象属性
- 更严格:它永远不会被带进回调,因为外部代码随时可能改动它。
- 两种情况的解法一样:把检查过的值复制进一个 const。
+ 只有当变量不可能改变时,编译器才会把收窄结果带进嵌套函数。函数里任何一处重新赋值都足以让它失效。被收窄的对象属性
+ 更严格:它永远不会被带进回调,因为外部代码随时可能改动它。两种情况的解法一样:把检查过的值复制进一个 const。
>
}
/>
@@ -1197,8 +1158,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 叫 kind、type、tag 都行。
- 编译器看的是结构,不是名字。
+ 叫 kind、type、tag 都行。编译器看的是结构,不是名字。
>
}
/>,
@@ -1214,9 +1174,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 差一个词:它必须是字面量类型,而不是 string。
- 宽泛的 string 区分不了成员。
- 数字字面量和布尔字面量同样可以当标签。
+ 差一个词:它必须是字面量类型,而不是 string。宽泛的 string 区分不了成员。数字字面量和布尔字面量同样可以当标签。
>
}
/>,
@@ -1247,8 +1205,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 三个条件:公共字段、字面量类型、
- 各成员的字面量互不相同。三条都满足时,一句{" "}
+ 三个条件:公共字段、字面量类型、各成员的字面量互不相同。三条都满足时,一句{" "}
switch (order.status) 就能把整个对象收窄到对应的成员。
>
}
@@ -1292,8 +1249,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 还漏了一个。把字段和字面量比较,就是一次普通的运行时比较,
- 也是可辨识联合最日常的写法。
+ 还漏了一个。把字段和字面量比较,就是一次普通的运行时比较,也是可辨识联合最日常的写法。
>
}
/>
@@ -1309,8 +1265,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 勾多了。有两个选项编译后就消失了,运行时什么都不检查。
- 那不是收窄,那只是让编译器别再报错。
+ 勾多了。有两个选项编译后就消失了,运行时什么都不检查。那不是收窄,那只是让编译器别再报错。
>
}
/>
@@ -1328,11 +1283,8 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- typeof、in 和字面量比较,
- 编译成 JavaScript 后都真的会执行,
- 所以收窄背后有一次真实的检查。as 和 !{" "}
- 编译后什么都不剩。类型判断错了,程序照样出错,
- 只是这次连提醒都没有了。
+ typeof、in 和字面量比较,编译成 JavaScript 后都真的会执行,所以收窄背后有一次真实的检查。as 和 !{" "}
+ 编译后什么都不剩。类型判断错了,程序照样出错,只是这次连提醒都没有了。
>
}
/>
@@ -1353,8 +1305,7 @@ export const QUIZ: QuizItem[] = [
zh={
<>
穷尽检查:在 default 分支里写{" "}
- const _x: ____ = order;,
- 就能让「漏了一个 case」变成编译错误。空格里填哪个类型?
+ const _x: ____ = order;,就能让「漏了一个 case」变成编译错误。空格里填哪个类型?
>
}
/>
@@ -1371,8 +1322,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 它是空联合 —— 所有 case 都处理完之后剩下的那个类型。
- §02 那段动画的最后一帧就是它。
+ 它是空联合 —— 所有 case 都处理完之后剩下的那个类型。§02 那段动画的最后一帧就是它。
>
}
/>
@@ -1390,9 +1340,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- never 是空联合,所以任何值都不能赋给它。
- 当所有 case 都被处理时,编译器认同这一行不可能被执行到,
- 赋值成立。一旦联合里多出一个成员,它会流到{" "}
+ never 是空联合,所以任何值都不能赋给它。当所有 case 都被处理时,编译器认同这一行不可能被执行到,赋值成立。一旦联合里多出一个成员,它会流到{" "}
default,赋值失败 —— 编译器就替你指出了漏掉的分支。
>
}
@@ -1479,8 +1427,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 代码运行前所有类型都被擦除了,不会发生任何转换。
- 谓词只改变编译器记录的内容,值本身分毫不动。
+ 代码运行前所有类型都被擦除了,不会发生任何转换。谓词只改变编译器记录的内容,值本身分毫不动。
>
}
/>,
@@ -1495,8 +1442,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 恰恰相反,它不检查。这正是谓词最危险的地方:
- 逻辑写反了,编译器照样相信你。
+ 恰恰相反,它不检查。这正是谓词最危险的地方:逻辑写反了,编译器照样相信你。
>
}
/>,
@@ -1532,12 +1478,10 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- o is Paid 的意思是「只要我返回 true,o 就是 Paid」。
- 所以在 if (isPaid(order)) 里 order{" "}
+ o is Paid 的意思是「只要我返回 true,o 就是 Paid」。所以在 if (isPaid(order)) 里 order{" "}
变成 Paid,而 orders.filter(isPaid){" "}
的类型是 Paid[]。还有一种相近的写法{" "}
- asserts o is Paid,它收窄的不是某个分支,
- 而是调用之后的所有代码。
+ asserts o is Paid,它收窄的不是某个分支,而是调用之后的所有代码。
>
}
/>
@@ -1676,10 +1620,8 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- ! 是一句编译期的断言。它让报错消失,
- 但不增加任何保护。断言错了,运行时照样是{" "}
- TypeError,而且这次编译器完全没有提醒过你。
- 规矩:能用 ?. 或 ?? 表达的,就用它们。
+ ! 是一句编译期的断言。它让报错消失,但不增加任何保护。断言错了,运行时照样是{" "}
+ TypeError,而且这次编译器完全没有提醒过你。规矩:能用 ?. 或 ?? 表达的,就用它们。
>
}
/>
@@ -1763,8 +1705,7 @@ export const QUIZ: QuizItem[] = [
zh={
<>
这是你想要的结果,但 || 判断的是真值,而{" "}
- 0 是 falsy,所以它被替换掉了。
- 想要这个效果,用 ??。
+ 0 是 falsy,所以它被替换掉了。想要这个效果,用 ??。
>
}
/>,
@@ -1779,8 +1720,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- || 总会返回两个操作数之一。左边是 falsy,
- 所以返回右边,也就是 50。
+ || 总会返回两个操作数之一。左边是 falsy,所以返回右边,也就是 50。
>
}
/>,
@@ -1794,8 +1734,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 这是合法的 JavaScript,也是合法的 TypeScript。
- 这种逻辑问题,编译器捕捉不到。
+ 这是合法的 JavaScript,也是合法的 TypeScript。这种逻辑问题,编译器捕捉不到。
>
}
/>,
@@ -1814,8 +1753,7 @@ export const QUIZ: QuizItem[] = [
<>
|| 判断的是真值,所以 0 和{" "}
"" 也会被替换;?? 只认{" "}
- null 和 undefined。
- 写默认值,用 ??。
+ null 和 undefined。写默认值,用 ??。
>
}
/>
diff --git a/lib/structural-data.tsx b/lib/structural-data.tsx
index 6b5a3ac..fcc0994 100644
--- a/lib/structural-data.tsx
+++ b/lib/structural-data.tsx
@@ -198,8 +198,7 @@ export const LABS: Lab[] = [
和一个 makeOrder(o: Order) 函数。然后做两件事:①
把带错字属性 sweetnes 的对象字面量
在调用处直接传进去,读一遍报错;②
- 把同一个对象先存进变量,再把变量传进去,它会安静通过。
- 把 ① 的报错逐字读完 —— 编译器有没有猜出你想写什么?
+ 把同一个对象先存进变量,再把变量传进去,它会安静通过。把 ① 的报错逐字读完 —— 编译器有没有猜出你想写什么?
>
}
/>
@@ -216,8 +215,7 @@ export const LABS: Lab[] = [
),
zh: (
<>
- 两次传的对象一个字都没改,唯一的区别是字面量写没写在调用处。
- 规则就是这么一句:多余属性检查只看「新鲜」的对象字面量。
+ 两次传的对象一个字都没改,唯一的区别是字面量写没写在调用处。规则就是这么一句:多余属性检查只看「新鲜」的对象字面量。
>
),
},
@@ -240,11 +238,8 @@ export const LABS: Lab[] = [
zh={
<>
① 的报错结尾是 Did you mean to write 'sweetness'?{" "}
- —— 编译器不但拦下错字,还说出了改法。② 编译通过,
- 但运行时打印的是 full sugar:
- 错字属性跟着混了进来,真正的 sweetness{" "}
- 从头到尾没被赋值。一屏之内就是全部取舍:
- 写在调用处的字面量从严查,变量按普通规则查。
+ —— 编译器不但拦下错字,还说出了改法。② 编译通过,但运行时打印的是 full sugar:错字属性跟着混了进来,真正的 sweetness{" "}
+ 从头到尾没被赋值。一屏之内就是全部取舍:写在调用处的字面量从严查,变量按普通规则查。
>
}
/>
@@ -281,8 +276,7 @@ export const LABS: Lab[] = [
{"type HasName = { name: string }"} 和函数{" "}
greet(x: HasName)。然后造三个来路完全不同的值:①
一个 class 的实例;② 一个存进变量的对象字面量;③
- 一个函数的返回值 —— 让它们全部通过 greet,
- 全程不写 implements、不写类型注解、不写{" "}
+ 一个函数的返回值 —— 让它们全部通过 greet,全程不写 implements、不写类型注解、不写{" "}
as。
>
}
@@ -321,9 +315,7 @@ export const LABS: Lab[] = [
zh={
<>
三个值和 HasName{" "}
- 之间没有任何声明过的关系。编译器把每个形状和要求的成员比了一遍,
- 每次都找到了 name: string。
- 这里的兼容是算出来的,不是登记出来的。
+ 之间没有任何声明过的关系。编译器把每个形状和要求的成员比了一遍,每次都找到了 name: string。这里的兼容是算出来的,不是登记出来的。
>
}
/>
@@ -358,9 +350,7 @@ export const LABS: Lab[] = [
第一步:定义 type UserId = string 和{" "}
type PostId = string,写一个{" "}
getUser(id: UserId),然后故意传一个{" "}
- PostId 进去,确认编译器毫无反应。第二步:
- 把两个类型改成品牌类型(branded types),
- 让同样的误传变成编译错误。再试着从一个普通 string 造出{" "}
+ PostId 进去,确认编译器毫无反应。第二步:把两个类型改成品牌类型(branded types),让同样的误传变成编译错误。再试着从一个普通 string 造出{" "}
UserId,看看代价是什么。
>
}
@@ -377,8 +367,7 @@ export const LABS: Lab[] = [
),
zh: (
<>
- 编译器只比形状,那就让两个形状不一样:
- 各自和一个带标记成员的对象类型求交集,标记成员的类型互不相同。
+ 编译器只比形状,那就让两个形状不一样:各自和一个带标记成员的对象类型求交集,标记成员的类型互不相同。
>
),
},
@@ -407,12 +396,9 @@ export const LABS: Lab[] = [
zh={
<>
__brand 只存在于类型层面。运行时{" "}
- UserId 就是普通 string,标记不占任何开销。
- 注意诚实的那一面:普通 string 不能赋给{" "}
- UserId,所以造一个出来总得断言一次。
- 通常的做法是只写一个 toUserId(s: string){" "}
- 函数来做这次断言,只在数据进入系统的入口调用它,
- 别处一律不写 as UserId。
+ UserId 就是普通 string,标记不占任何开销。注意诚实的那一面:普通 string 不能赋给{" "}
+ UserId,所以造一个出来总得断言一次。通常的做法是只写一个 toUserId(s: string){" "}
+ 函数来做这次断言,只在数据进入系统的入口调用它,别处一律不写 as UserId。
>
}
/>
@@ -450,8 +436,7 @@ export const LABS: Lab[] = [
"tea"、true、
{"() => {}"}、[1, 2]、null、
undefined,记下哪些报错。再把 x{" "}
- 的类型换成 object 重测一轮,对比两张结果,
- 说清楚两个类型各自到底挡住了什么。
+ 的类型换成 object 重测一轮,对比两张结果,说清楚两个类型各自到底挡住了什么。
>
}
/>
@@ -504,9 +489,7 @@ export const LABS: Lab[] = [
结果:{"{}"} 只挡 null 和{" "}
undefined,原始值全部放行 ——
因为它的必需成员清单是空的,任何值都满足。object{" "}
- 挡掉全部原始值,只收对象、数组和函数。所以:
- 想表达「任意对象」写 object;
- 想表达「什么值都可能,用之前先收窄」写{" "}
+ 挡掉全部原始值,只收对象、数组和函数。所以:想表达「任意对象」写 object;想表达「什么值都可能,用之前先收窄」写{" "}
unknown;{"{}"} 尽量别用。
>
}
@@ -581,8 +564,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 声明关系不是必需的。一个从没提过目标类型的对象字面量,
- 只要形状覆盖了目标的要求,照样通过。
+ 声明关系不是必需的。一个从没提过目标类型的对象字面量,只要形状覆盖了目标的要求,照样通过。
>
),
},
@@ -595,8 +577,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 在哪里声明完全没有影响。两个毫无关系的文件里,
- 形状相同的两个类型可以互换。
+ 在哪里声明完全没有影响。两个毫无关系的文件里,形状相同的两个类型可以互换。
>
),
},
@@ -612,8 +593,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- TypeScript 用的是结构化类型(structural typing):忽略名字,比较成员。
- 源类型有目标要求的每个成员,且这些成员的类型也兼容,赋值就成立。
+ TypeScript 用的是结构化类型(structural typing):忽略名字,比较成员。源类型有目标要求的每个成员,且这些成员的类型也兼容,赋值就成立。
>
),
},
@@ -674,8 +654,7 @@ export const QUIZ: QuizItem[] = [
zh: (
<>
「不同 interface 就是不同类型」是名义类型系统的规则。TypeScript
- 里名字只是标签,x: number 对上 x: number,
- 就是兼容。
+ 里名字只是标签,x: number 对上 x: number,就是兼容。
>
),
},
@@ -691,8 +670,7 @@ export const QUIZ: QuizItem[] = [
zh: (
<>
extends{" "}
- 能让意图更好读,但它不是兼容的前提。一句声明都不写,
- 形状相同照样能赋值。
+ 能让意图更好读,但它不是兼容的前提。一句声明都不写,形状相同照样能赋值。
>
),
},
@@ -724,8 +702,7 @@ export const QUIZ: QuizItem[] = [
zh: (
<>
形状相同、名字不同的两个类型,可以互相赋值。interface 和{" "}
- type 在这一点上表现一致:
- 都不会仅仅因为名字不同就造出一个不兼容的新类型。
+ type 在这一点上表现一致:都不会仅仅因为名字不同就造出一个不兼容的新类型。
>
),
},
@@ -745,8 +722,7 @@ export const QUIZ: QuizItem[] = [
<>
{"type Staff = { name: string }"},变量{" "}
barista 的类型是{" "}
- {"{ name: string; makeTea: () => void }"}。
- 下面哪个赋值能通过?
+ {"{ name: string; makeTea: () => void }"}。下面哪个赋值能通过?
>
),
},
@@ -797,8 +773,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 方向反了。Staff 身上没有 makeTea,
- 缺了一个必需成员:
+ 方向反了。Staff 身上没有 makeTea,缺了一个必需成员:
Property 'makeTea' is missing in type ... ts(2741)
@@ -832,8 +807,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 第一个方向是通的。barista 有 name,
- 类型也对,Staff 的要求全部满足。
+ 第一个方向是通的。barista 有 name,类型也对,Staff 的要求全部满足。
>
),
},
@@ -849,8 +823,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 成员越多越具体,对应的值集合越小。小集合里的每个值同时也属于大集合,
- 所以更具体的类型能用在要求较宽松的位置,反过来不行。
+ 成员越多越具体,对应的值集合越小。小集合里的每个值同时也属于大集合,所以更具体的类型能用在要求较宽松的位置,反过来不行。
>
),
},
@@ -890,8 +863,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 多余属性检查只对「新鲜」的对象字面量生效。对象一旦存进变量,
- 就只按普通的兼容规则判断,多出来的成员是允许的
+ 多余属性检查只对「新鲜」的对象字面量生效。对象一旦存进变量,就只按普通的兼容规则判断,多出来的成员是允许的
>
),
},
@@ -940,9 +912,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 这是刻意设计,也是文档里写明的行为。写在调用处的字面量只用这一次,
- 多出来的成员几乎都是拼错;变量可能在别处另有正当用途,
- 所以按普通规则判断。
+ 这是刻意设计,也是文档里写明的行为。写在调用处的字面量只用这一次,多出来的成员几乎都是拼错;变量可能在别处另有正当用途,所以按普通规则判断。
>
),
},
@@ -976,9 +946,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 多余属性检查是叠加在可赋值性之上的一道额外检查,目标是抓拼写错误。
- 只有当一个新鲜的对象字面量被直接赋值、或直接传给已知类型的位置时,
- 它才会运行。它不属于可赋值性规则本身 ——
+ 多余属性检查是叠加在可赋值性之上的一道额外检查,目标是抓拼写错误。只有当一个新鲜的对象字面量被直接赋值、或直接传给已知类型的位置时,它才会运行。它不属于可赋值性规则本身 ——
这正是先存进变量它就消失的原因。
>
),
@@ -1053,8 +1021,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 选进了一个错误说法。多写的属性只在字面量「新鲜」时才被报出来,
- 而类型是谁写的,编译器根本不看。
+ 选进了一个错误说法。多写的属性只在字面量「新鲜」时才被报出来,而类型是谁写的,编译器根本不看。
>
),
},
@@ -1111,8 +1078,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 用品牌类型:{'string & { __brand: "user" }'},
- 让两个形状不同
+ 用品牌类型:{'string & { __brand: "user" }'},让两个形状不同
>
),
},
@@ -1145,8 +1111,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 运行时检查只是兜底,而且要等错误的值已经传进来才会响。
- 这里的目标是让这次调用根本编译不过。
+ 运行时检查只是兜底,而且要等错误的值已经传进来才会响。这里的目标是让这次调用根本编译不过。
>
),
},
@@ -1161,8 +1126,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- any 是往反方向走:它把检查整个关掉,
- 连本来能拦下的错误也放过了。问题是检查太粗,不是检查太多。
+ any 是往反方向走:它把检查整个关掉,连本来能拦下的错误也放过了。问题是检查太粗,不是检查太多。
>
),
},
@@ -1175,8 +1139,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 编译器不看名字。名字再长,形状还是 string,
- 误传照样通过。
+ 编译器不看名字。名字再长,形状还是 string,误传照样通过。
>
),
},
@@ -1194,10 +1157,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 既然编译器比的是形状,就把区别写进形状:
- 各自和一个类型不同的标记成员求交集,两者就不再能互换。
- 标记只存在于编译期,没有运行时开销。代价是造一个值需要断言,
- 通常把它包进唯一一个工厂函数里。
+ 既然编译器比的是形状,就把区别写进形状:各自和一个类型不同的标记成员求交集,两者就不再能互换。标记只存在于编译期,没有运行时开销。代价是造一个值需要断言,通常把它包进唯一一个工厂函数里。
>
),
},
@@ -1245,9 +1205,7 @@ export const QUIZ: QuizItem[] = [
zh: (
<>
structural typing,结构化类型。相对的一种做法是 Java、C# 用的
- nominal typing(名义类型):
- 编译器比较声明的名字和声明的继承关系,所以成员完全一样的两个类
- 依然毫无关系。
+ nominal typing(名义类型):编译器比较声明的名字和声明的继承关系,所以成员完全一样的两个类依然毫无关系。
>
),
},
@@ -1263,8 +1221,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 想给函数参数一个类型,表达「任意对象都行,但原始值不行」,
- 应该写哪个?
+ 想给函数参数一个类型,表达「任意对象都行,但原始值不行」,应该写哪个?
>
),
},
@@ -1347,8 +1304,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- any 把检查关掉了,什么都拦不住,
- 而且从它读出来的值也不再受检查。
+ any 把检查关掉了,什么都拦不住,而且从它读出来的值也不再受检查。
>
),
},
@@ -1361,8 +1317,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- unknown 的意思是「任何值都可能」,原始值也收。
- 它安全,但表达的不是「只要对象」。
+ unknown 的意思是「任何值都可能」,原始值也收。它安全,但表达的不是「只要对象」。
>
),
},
@@ -1382,8 +1337,7 @@ export const QUIZ: QuizItem[] = [
<>
object 的意思是「不是原始值」:对象、数组、函数进得来,
number、string、boolean{" "}
- 进不来。想要任意对象用 object;
- 想要任何值、用之前先收窄,用 unknown;{"{}"}{" "}
+ 进不来。想要任意对象用 object;想要任何值、用之前先收窄,用 unknown;{"{}"}{" "}
放着别用。
>
),
diff --git a/lib/tsconfig-data.tsx b/lib/tsconfig-data.tsx
index 34772cb..996b5f7 100644
--- a/lib/tsconfig-data.tsx
+++ b/lib/tsconfig-data.tsx
@@ -201,10 +201,7 @@ export const LABS: Lab[] = [
}
zh={
<>
- 新建一个空目录,跑 npx tsc --init。
- 打开生成的 tsconfig.json 读一遍,
- 记下哪些选项已经是 true,
- 以及模板给它们分的段落名。
+ 新建一个空目录,跑 npx tsc --init。打开生成的 tsconfig.json 读一遍,记下哪些选项已经是 true,以及模板给它们分的段落名。
>
}
/>
@@ -220,8 +217,7 @@ export const LABS: Lab[] = [
}
zh={
<>
- 然后把 §02 调节台里那段演示代码存成 order.ts。
- 文件顶部补两行,免得报「找不到名字」:
+ 然后把 §02 调节台里那段演示代码存成 order.ts。文件顶部补两行,免得报「找不到名字」:
>
}
/>
@@ -248,8 +244,7 @@ declare function submit(x: unknown): void;`}
跑 npx tsc --noEmit,数一数报错。然后把{" "}
strict、noUncheckedIndexedAccess、
exactOptionalPropertyTypes 都改成{" "}
- false 再跑一次。两个数字的差,
- 就是这份配置替你做的事。
+ false 再跑一次。两个数字的差,就是这份配置替你做的事。
>
}
/>
@@ -269,8 +264,7 @@ declare function submit(x: unknown): void;`}
zh: (
<>
--noEmit 的意思是只检查、不写文件。模板把{" "}
- strict 放在「Recommended Options」,
- 另外两个放在「Stricter Typechecking Options」——
+ strict 放在「Recommended Options」,另外两个放在「Stricter Typechecking Options」——
这个分段本身就回答了「noUncheckedIndexedAccess 属于 strict 吗」。
>
),
@@ -295,8 +289,7 @@ declare function submit(x: unknown): void;`}
}
zh={
<>
- 再把 strict 单独开回来,另外两个仍然关着:
- 报错是 4 个,不是 5 个。少掉的那个正是{" "}
+ 再把 strict 单独开回来,另外两个仍然关着:报错是 4 个,不是 5 个。少掉的那个正是{" "}
sizes[3] 那一行。一次实验就够了 ——{" "}
noUncheckedIndexedAccess 不属于{" "}
strict。
@@ -333,9 +326,7 @@ declare function submit(x: unknown): void;`}
}
zh={
<>
- 把下面这段存成 boss.js,
- 用带 TypeScript 支持的编辑器打开。
- 里面有两个写错的属性名:pirce 和{" "}
+ 把下面这段存成 boss.js,用带 TypeScript 支持的编辑器打开。里面有两个写错的属性名:pirce 和{" "}
nmae。在第一行加上{" "}
{"// @ts-check"},看编辑器报出几个。
>
@@ -353,8 +344,7 @@ declare function submit(x: unknown): void;`}
}
zh={
<>
- 答案是 0 个。先自己想清楚为什么,
- 再想办法让编辑器把两个都报出来。
+ 答案是 0 个。先自己想清楚为什么,再想办法让编辑器把两个都报出来。
>
}
/>
@@ -377,8 +367,7 @@ declare function submit(x: unknown): void;`}
zh: (
<>
cheapest 的参数没有类型,所以 list 是{" "}
- any。在 any 上取任何属性都是合法的,
- 所以此刻 it.pirce 还不算错。用 JSDoc 给参数补一个类型:
+ any。在 any 上取任何属性都是合法的,所以此刻 it.pirce 还不算错。用 JSDoc 给参数补一个类型:
{"/** @param {{ name: string, price: number }[]} list */"}
@@ -410,12 +399,8 @@ declare function submit(x: unknown): void;`}
}
zh={
<>
- 一个文件,两个教训。@ts-check 只用一行,
- 就为单个 .js 文件打开了检查。
- 但检查需要有类型可对照 —— 没有类型,就没有可矛盾的东西。
- checkJs: true 做的是同一件事,
- 只不过一次覆盖所有 .js 文件,
- 也有同样的局限。
+ 一个文件,两个教训。@ts-check 只用一行,就为单个 .js 文件打开了检查。但检查需要有类型可对照 —— 没有类型,就没有可矛盾的东西。
+ checkJs: true 做的是同一件事,只不过一次覆盖所有 .js 文件,也有同样的局限。
>
}
/>
@@ -470,11 +455,9 @@ declare function submit(x: unknown): void;`}
}
zh={
<>
- 会出现两个错。读一读:两个都建议你改 lib,
- 不是 target。现在把 lib 设成{" "}
+ 会出现两个错。读一读:两个都建议你改 lib,不是 target。现在把 lib 设成{" "}
ES2022,target 仍然留在{" "}
- ES5,报错就消失了。然后切到 .JS 标签,
- 看看产出的代码有没有变。
+ ES5,报错就消失了。然后切到 .JS 标签,看看产出的代码有没有变。
>
}
/>
@@ -527,10 +510,7 @@ declare function submit(x: unknown): void;`}
<>
改 lib 前后,产出的 JavaScript 一字不差 —— 因为{" "}
lib 不参与产物生成。这里的风险值得记住:
- lib 是你对运行环境的声明,
- 而编译器会信你。声明了 es2022,
- 却把代码跑在没有 Promise 的引擎上,
- 就是运行时失败,而编译期一声不响。lib{" "}
+ lib 是你对运行环境的声明,而编译器会信你。声明了 es2022,却把代码跑在没有 Promise 的引擎上,就是运行时失败,而编译期一声不响。lib{" "}
不会替你加 polyfill。
>
}
@@ -566,10 +546,8 @@ declare function submit(x: unknown): void;`}
}
zh={
<>
- 建一个小项目:三个互相 import 的 .js 文件。
- 故意埋两个写错的属性名,再埋一个「可能返回 null
- 而调用处没判」的函数。然后照 §05 的路线走一遍,
- 每一步都记下 npx tsc --noEmit 的输出。
+ 建一个小项目:三个互相 import 的 .js 文件。故意埋两个写错的属性名,再埋一个「可能返回 null
+ 而调用处没判」的函数。然后照 §05 的路线走一遍,每一步都记下 npx tsc --noEmit 的输出。
>
}
/>
@@ -585,8 +563,7 @@ declare function submit(x: unknown): void;`}
}
zh={
<>
- 重点不是最后那个数字。重点是每一步系统都在跑,
- 而且每一步都比上一步严。
+ 重点不是最后那个数字。重点是每一步系统都在跑,而且每一步都比上一步严。
>
}
/>
@@ -631,8 +608,7 @@ declare function submit(x: unknown): void;`}
}
zh={
<>
- 真实项目做完这件事要几个月,但节奏是一样的。
- 开工前先知道一件事:类型报错并不会阻止 tsc{" "}
+ 真实项目做完这件事要几个月,但节奏是一样的。开工前先知道一件事:类型报错并不会阻止 tsc{" "}
把 JavaScript 写出来。除非你同时开{" "}
noEmitOnError,否则报错期间构建照样出产物。
>
@@ -694,8 +670,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- noImplicitAny 在家族里。它的作用是:不许因为「推不出类型」
- 就悄悄按 any 处理。
+ noImplicitAny 在家族里。它的作用是:不许因为「推不出类型」就悄悄按 any 处理。
>
),
},
@@ -752,8 +727,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- findOrder 声明的返回类型是 Order | null,
- 你直接写 findOrder(id).total。关着{" "}
+ findOrder 声明的返回类型是 Order | null,你直接写 findOrder(id).total。关着{" "}
strictNullChecks 时会发生什么?
>
),
@@ -823,8 +797,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 编译器从不添加这类运行时行为。它只做两件事:擦掉类型、报告错误。
- 判空代码永远得你自己写。
+ 编译器从不添加这类运行时行为。它只做两件事:擦掉类型、报告错误。判空代码永远得你自己写。
>
),
},
@@ -857,8 +830,7 @@ export const QUIZ: QuizItem[] = [
zh: (
<>
关掉 strictNullChecks,null 和{" "}
- undefined 可以赋给任何类型。「这个值可能没有」
- 从类型里消失了,也就没什么可检查的。开着时,同一行会报{" "}
+ undefined 可以赋给任何类型。「这个值可能没有」从类型里消失了,也就没什么可检查的。开着时,同一行会报{" "}
TS18047: 'order' is possibly 'null'.{" "}
如果这一族里只能开一个,就开它。
>
@@ -972,8 +944,7 @@ export const QUIZ: QuizItem[] = [
es2022 则原样保留、只擦掉类型。它还会顺带定一个默认{" "}
lib,而那是另一件事:lib{" "}
决定有哪些类型声明可用。另外注意:如果你的 JavaScript
- 是打包器、swc 或 esbuild 产出的,
- 那么上线的语法代次由那个工具的 target 决定,tsconfig 里的{" "}
+ 是打包器、swc 或 esbuild 产出的,那么上线的语法代次由那个工具的 target 决定,tsconfig 里的{" "}
target 说了不算。
>
),
@@ -1060,8 +1031,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 两者能压住的错误是同一批,都作用于下一行。
- 差别在于错误消失之后会发生什么。
+ 两者能压住的错误是同一批,都作用于下一行。差别在于错误消失之后会发生什么。
>
),
},
@@ -1074,8 +1044,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- @ts-ignore 仍然是合法的 TypeScript。
- 问题在于它永远沉默 —— 债还清了也不吭声。
+ @ts-ignore 仍然是合法的 TypeScript。问题在于它永远沉默 —— 债还清了也不吭声。
>
),
},
@@ -1089,8 +1058,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 两者都是编译期指令,和环境无关。编译器读到就生效,
- 不区分开发和生产。
+ 两者都是编译期指令,和环境无关。编译器读到就生效,不区分开发和生产。
>
),
},
@@ -1108,8 +1076,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- @ts-expect-error 的意思是「我预期这里有错」。
- 一旦下一行不再报错,这行注释自己就变成了错误:
+ @ts-expect-error 的意思是「我预期这里有错」。一旦下一行不再报错,这行注释自己就变成了错误:
TS2578: Unused '@ts-expect-error' directive.{" "}
这就是「以后找得回来的债」和「找不回来的债」的区别。
>
@@ -1170,8 +1137,7 @@ export const QUIZ: QuizItem[] = [
的类型变成 string | undefined,想用先收窄。它要靠{" "}
strictNullChecks 才有效果 —— 后者关着时,
string | undefined 会退回成 string。它不在{" "}
- strict 里,因为它会对每一次下标读取发话,
- 存量项目一开工作量很大。而 tsc --init{" "}
+ strict 里,因为它会对每一次下标读取发话,存量项目一开工作量很大。而 tsc --init{" "}
给新项目是默认开的。
>
),
@@ -1231,8 +1197,7 @@ export const QUIZ: QuizItem[] = [
zh: (
<>
bundler 描述的是 Vite / esbuild
- 这类打包器的解析方式。产物不经打包直接给 Node 跑,
- 就该按 Node 自己的规矩来:nodenext。
+ 这类打包器的解析方式。产物不经打包直接给 Node 跑,就该按 Node 自己的规矩来:nodenext。
>
),
},
@@ -1262,8 +1227,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- amd 是 RequireJS 时代的浏览器模块格式,
- 和 Node 后端没有关系。
+ amd 是 RequireJS 时代的浏览器模块格式,和 Node 后端没有关系。
>
),
},
@@ -1290,15 +1254,13 @@ export const QUIZ: QuizItem[] = [
<>
两条路,按「谁消费产物」来分。产物直接给 Node 跑:
module: nodenext —— 它同时把{" "}
- moduleResolution 钉在 nodenext,
- 你写成别的,编译器会报{" "}
+ moduleResolution 钉在 nodenext,你写成别的,编译器会报{" "}
TS5109: Option 'moduleResolution' must be set to
'NodeNext'
。产物交给打包器:
- module: esnext + moduleResolution: bundler,
- 把模块解析整个交给打包器。这两个选项别混:module{" "}
+ module: esnext + moduleResolution: bundler,把模块解析整个交给打包器。这两个选项别混:module{" "}
是编译器写出的模块格式,moduleResolution 是它怎么找到你
import 的东西。
>
@@ -1410,8 +1372,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 有一项选多了。要么是把「渐进开 strict」
- 这个存量项目的策略错套在了新项目上,要么是把一件 tsc{" "}
+ 有一项选多了。要么是把「渐进开 strict」这个存量项目的策略错套在了新项目上,要么是把一件 tsc{" "}
不做的事算在了它头上。
>
),
@@ -1437,8 +1398,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- A:一份配置两个读者,所以编辑器的红线和 CI 的失败是一致的。B:
- 库的声明文件有错不归你修,而两个库的全局声明打架还会拖垮你的构建。D:
+ A:一份配置两个读者,所以编辑器的红线和 CI 的失败是一致的。B:库的声明文件有错不归你修,而两个库的全局声明打架还会拖垮你的构建。D:
useUnknownInCatchVariables 是家族成员。C 说反了 ——
第一天开 strict 成本为零,成本随代码量增长。E 不对:
tsc 是每个输入文件产出一个 .js,从不打包。
@@ -1463,8 +1423,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 你接手一套九百行的 JavaScript 点单系统,负责人要求「一天都不能停」。
- 第一步该做什么?
+ 你接手一套九百行的 JavaScript 点单系统,负责人要求「一天都不能停」。第一步该做什么?
>
),
},
@@ -1520,8 +1479,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 把 .js 后缀全改成 .ts,
- 内容不用动就已经是 TypeScript 了
+ 把 .js 后缀全改成 .ts,内容不用动就已经是 TypeScript 了
>
),
},
@@ -1538,8 +1496,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 这样会一次冒出几百个错,又没法只上线一部分修复,最后只能回滚。
- 渐进的做法能让系统一直跑着。
+ 这样会一次冒出几百个错,又没法只上线一部分修复,最后只能回滚。渐进的做法能让系统一直跑着。
>
),
},
@@ -1554,8 +1511,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- @ts-ignore 是把检查关掉,不是把问题修好。
- 整个文件都压住,等于装了 TypeScript 却什么都没得到。
+ @ts-ignore 是把检查关掉,不是把问题修好。整个文件都压住,等于装了 TypeScript 却什么都没得到。
>
),
},
@@ -1572,8 +1528,7 @@ export const QUIZ: QuizItem[] = [
zh: (
<>
改后缀不改变代码本身。隐式 any 的参数、没判的{" "}
- null 一个都没少,而且现在会一起报出来、
- 把所有人挡住。应该先用 allowJs{" "}
+ null 一个都没少,而且现在会一起报出来、把所有人挡住。应该先用 allowJs{" "}
把文件收进来,再一个个迁移。
>
),
@@ -1592,8 +1547,7 @@ export const QUIZ: QuizItem[] = [
zh: (
<>
路线是:allowJs 把 .js 文件收进编译,
- checkJs 或 @ts-check 开始检查它们,
- 然后一次改一个文件成 .ts,再把 strict{" "}
+ checkJs 或 @ts-check 开始检查它们,然后一次改一个文件成 .ts,再把 strict{" "}
的成员一项一项开。每一步系统都在跑,每一步都比上一步严。
>
),
diff --git a/lib/type-magic-data.tsx b/lib/type-magic-data.tsx
index 89670d1..f1d69c5 100644
--- a/lib/type-magic-data.tsx
+++ b/lib/type-magic-data.tsx
@@ -134,8 +134,7 @@ export const LABS: Lab[] = [
在 TypeScript Playground 里写两个条件类型:
Naked<T> = T extends string ? true : false 和{" "}
- Wrapped<T> = [T] extends [string] ? true : false。
- 都传同一个实参 "a" | 1,再悬停看两个结果 ——
+ Wrapped<T> = [T] extends [string] ? true : false。都传同一个实参 "a" | 1,再悬停看两个结果 ——
说清楚为什么一个是 boolean,另一个是 false。
T 会让条件类型对联合的每个成员各跑一次,
- 结果再合并成联合;用 [ ] 包起来则是整体判断一次。
- 再想一想:true | false 会被显示成什么。
+ 裸 T 会让条件类型对联合的每个成员各跑一次,结果再合并成联合;用 [ ] 包起来则是整体判断一次。再想一想:true | false 会被显示成什么。
>
),
},
@@ -191,11 +188,9 @@ export const LABS: Lab[] = [
hl={[4, 8]}
/>
- A 显示成 boolean 是最容易卡住的一步。
- 编译器没有偷懒:true | false 和 boolean{" "}
+ A 显示成 boolean 是最容易卡住的一步。编译器没有偷懒:true | false 和 boolean{" "}
本来就是同一个类型,boolean{" "}
- 只是它的打印形式。所以条件类型返回了一个你没预料到的联合时,
- 第一件要检查的事就是:它是不是发生了分发。
+ 只是它的打印形式。所以条件类型返回了一个你没预料到的联合时,第一件要检查的事就是:它是不是发生了分发。
- 四个零件挤在一行里:映射类型的循环、给键改名的 as、
- 模板字面量类型,以及 Capitalize。Vue 的{" "}
- onXxx 属性、测试库的 mockXxx,
- 类型层面都是这个形状。
+ 四个零件挤在一行里:映射类型的循环、给键改名的 as、模板字面量类型,以及 Capitalize。Vue 的{" "}
+ onXxx 属性、测试库的 mockXxx,类型层面都是这个形状。
本章的 Unbox 只剥一层:喂它{" "}
- Promise<Promise<Promise<string>>>,
- 剩下的两层还在。把它升级成 DeepUnbox<T>:
- 不管几层都剥到底,最后拿到 string。提示:
- 类型别名可以引用自己。
+ Promise<Promise<Promise<string>>>,剩下的两层还在。把它升级成 DeepUnbox<T>:不管几层都剥到底,最后拿到 string。提示:类型别名可以引用自己。
U 可能还是 Promise。
- 那就先别急着返回:把 U 再交给 DeepUnbox{" "}
+ 剥掉一层之后,抓到的 U 可能还是 Promise。那就先别急着返回:把 U 再交给 DeepUnbox{" "}
自己处理 —— ? DeepUnbox<U> : T。
>
),
@@ -387,8 +376,7 @@ export const LABS: Lab[] = [
hl={[2]}
/>
- 第 2 行就是类型系统里的递归:真分支里写出了正在定义的这个别名,
- 于是同一条规则反复应用,直到 T 不再是{" "}
+ 第 2 行就是类型系统里的递归:真分支里写出了正在定义的这个别名,于是同一条规则反复应用,直到 T 不再是{" "}
Promise。你刚刚写出了内置 Awaited{" "}
的核心。标准库那一版还会处理任何带 then{" "}
方法的对象,不只是 Promise,但形状是一样的。
@@ -399,8 +387,7 @@ export const LABS: Lab[] = [
error TS2589: Type instantiation is excessively deep and possibly
infinite.
{" "}
- 这个上限对真实数据够用;而且当递归调用占据了整个分支
- (就像这里),上限还会高得多 ——
+ 这个上限对真实数据够用;而且当递归调用占据了整个分支(就像这里),上限还会高得多 ——
这时编译器可以反复执行同一步,而不必层层嵌套。
MyOmit<T, K>,对 Order 验证它和内置{" "}
Omit 行为一致。第二步:把 K 的约束改成{" "}
K extends keyof T,做出严格版{" "}
- StrictOmit —— 键拼错要当场报错,而不是被默默忽略。 第 06
+ StrictOmit —— 键拼错要当场报错,而不是被默默忽略。第 06
章那个漏洞就补上了。
),
@@ -500,16 +487,12 @@ export const LABS: Lab[] = [
hl={[2, 9]}
/>
- 第 2 行就是 lib.es5.d.ts 里的原文,一字不差。
- 标准库的约束是 keyof any,也就是{" "}
- string | number | symbol,所以几乎任何键类型都收,
- 拼错也能过。第 9 行只改了约束,默默失效就变成了编译错误。
- 这两章要给你的就是这个:内置类型不合用的时候,你已经能自己造一个。
+ 第 2 行就是 lib.es5.d.ts 里的原文,一字不差。标准库的约束是 keyof any,也就是{" "}
+ string | number | symbol,所以几乎任何键类型都收,拼错也能过。第 9 行只改了约束,默默失效就变成了编译错误。这两章要给你的就是这个:内置类型不合用的时候,你已经能自己造一个。
Omit 宽松是故意的。约束松一点,它才能接受不在{" "}
- T 上的键 —— 当 T 本身是联合、
- 或者还是泛型时,这一点有用。想抓拼写错误就用 StrictOmit
+ T 上的键 —— 当 T 本身是联合、或者还是泛型时,这一点有用。想抓拼写错误就用 StrictOmit
,其他地方继续用 Omit。
string 精确得多:就是那六个名字,一个不多。 所以{" "}
+ 它比 string 精确得多:就是那六个名字,一个不多。所以{" "}
const k: keyof Order = "cup" 会报{" "}
Type '"cup"' is not assignable to type
@@ -708,8 +691,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- console.log 要的是实参,所以这里是表达式,
- 会真的执行,打印出 "object"。
+ console.log 要的是实参,所以这里是表达式,会真的执行,打印出 "object"。
>
),
},
@@ -724,8 +706,7 @@ export const QUIZ: QuizItem[] = [
zh: (
<>
const t = 后面是表达式位置,属于值世界。
- t 里装的是字符串 "object",
- 不是类型。
+ t 里装的是字符串 "object",不是类型。
>
),
},
@@ -742,9 +723,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 看位置。type 声明的等号右边、注解的冒号后面,
- 都是类型位置,那里的 typeof 属于 TypeScript,
- 编译时被擦除。其余位置都是 JavaScript 的那个运算符,会真的执行。
+ 看位置。type 声明的等号右边、注解的冒号后面,都是类型位置,那里的 typeof 属于 TypeScript,编译时被擦除。其余位置都是 JavaScript 的那个运算符,会真的执行。
>
),
},
@@ -850,8 +829,7 @@ export const QUIZ: QuizItem[] = [
<>
as const 把数组变成由字面量类型组成的只读元组,
T[number] 取出全部元素类型的联合:
- "boba" | "coconut jelly"。
- 数据只写一份,类型跟着它走。
+ "boba" | "coconut jelly"。数据只写一份,类型跟着它走。
>
),
},
@@ -956,8 +934,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 如果是整体判断,Exclude 就永远删不掉任何东西。
- 裸类型参数遇到联合时故意不整体判断,
+ 如果是整体判断,Exclude 就永远删不掉任何东西。裸类型参数遇到联合时故意不整体判断,
Exclude 才成立。
>
),
@@ -988,8 +965,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- never 只替换匹配上的那一个成员。它是空联合,
- 合并结果时安静消失,不影响其他成员。
+ never 只替换匹配上的那一个成员。它是空联合,合并结果时安静消失,不影响其他成员。
>
),
},
@@ -1007,8 +983,7 @@ export const QUIZ: QuizItem[] = [
zh: (
<>
这就是分布式条件类型:拆开联合、逐个判断、合并结果。
- "b" 变成 never,什么也没留下,
- 所以结果是 "a" | "c"。§04
+ "b" 变成 never,什么也没留下,所以结果是 "a" | "c"。§04
的可视化演的就是这一段。
>
),
@@ -1108,10 +1083,8 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- [T] 让 T 不再是裸类型参数,
- 于是不分发,联合被当成一个类型整体判断。
- ["a" | 1] 不能赋给 [string],
- 所以答案是 "mixed"。注意两边都要包,才有效。
+ [T] 让 T 不再是裸类型参数,于是不分发,联合被当成一个类型整体判断。
+ ["a" | 1] 不能赋给 [string],所以答案是 "mixed"。注意两边都要包,才有效。
>
),
},
@@ -1145,8 +1118,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 在 extends 后面的模式里声明一个类型变量;
- 模式匹配成功时,这个变量装着匹配到的类型,真分支里可以用
+ 在 extends 后面的模式里声明一个类型变量;模式匹配成功时,这个变量装着匹配到的类型,真分支里可以用
>
),
},
@@ -1218,8 +1190,7 @@ export const QUIZ: QuizItem[] = [
zh: (
<>
T extends Promise<infer U> ? U : T 读作:如果{" "}
- T 长成 Promise<某个类型> 的样子,
- 就把那个类型记作 U 并返回它。相当于对类型做解构。
+ T 长成 Promise<某个类型> 的样子,就把那个类型记作 U 并返回它。相当于对类型做解构。
>
),
},
@@ -1253,8 +1224,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 修饰符能加也能减:+ 是加,可以省略不写;
- 另一个符号是减。手写版 Required 用的就是它。
+ 修饰符能加也能减:+ 是加,可以省略不写;另一个符号是减。手写版 Required 用的就是它。
>
),
},
@@ -1271,7 +1241,7 @@ export const QUIZ: QuizItem[] = [
zh: (
<>
{"{ [K in keyof T]-?: T[K] }"} 把每个属性的{" "}
- ? 去掉,这正是 Required 的定义。 在{" "}
+ ? 去掉,这正是 Required 的定义。在{" "}
strictNullChecks 下,它同时把属性类型里的{" "}
undefined 也去掉。-readonly 同理,用来去掉{" "}
readonly。
@@ -1397,8 +1367,7 @@ export const QUIZ: QuizItem[] = [
<>
三个零件:映射类型对 K 逐键循环; 泛型约束保证{" "}
K 里的每个名字都是 T 上真实的键(
- Pick 严格的根源);
- 索引访问把原来的属性类型抄过来。没有条件类型,也没有 infer
+ Pick 严格的根源);索引访问把原来的属性类型抄过来。没有条件类型,也没有 infer
。
>
),
diff --git a/lib/types-data.tsx b/lib/types-data.tsx
index 84865de..ad838d2 100644
--- a/lib/types-data.tsx
+++ b/lib/types-data.tsx
@@ -41,9 +41,7 @@ export const LABS: Lab[] = [
comment next to it. Compare with the answer afterwards.>
}
zh={
- <>打开 {PLAYGROUND},贴入下面的代码 —— 里面一个注解都没有。
- 然后把鼠标依次悬停在每一个变量名上,
- 把 TS 推断出的类型写在旁边的注释里,写完再对答案。>
+ <>打开 {PLAYGROUND},贴入下面的代码 —— 里面一个注解都没有。然后把鼠标依次悬停在每一个变量名上,把 TS 推断出的类型写在旁边的注释里,写完再对答案。>
}
/>
@@ -155,8 +153,7 @@ const nothing: null // null 的类型就是 null`,
zh={
<>
在 Playground 里逐行输入下面四行,每写一行就 hover
- 一次变量名,记下类型。最后回答:哪几行是字面量类型,
- 哪几行被拓宽了?为什么?
+ 一次变量名,记下类型。最后回答:哪几行是字面量类型,哪几行被拓宽了?为什么?
>
}
/>
@@ -272,8 +269,7 @@ const d = 42; // 42 -- 同 b`,
price(数字)、sizes(字符串数组)、
soldOut(布尔);② 声明{" "}
const menu: MenuItem[] 并填两三款奶茶;③
- 故意把其中一款的 price 拼成 prise,
- 观察报错落在哪一行、说了什么。
+ 故意把其中一款的 price 拼成 prise,观察报错落在哪一行、说了什么。
>
}
/>
@@ -352,9 +348,7 @@ const menu: MenuItem[] = [
<>
报错精确落在错字上,还给了改法。现在把{" "}
: MenuItem[] 删掉再看:错误消失了 ——
- 因为推断把 prise 也当成一个合法字段收下了。
- 它会在很久以后、别处读 price 时才重新出现。
- 这就是给共享数据立契约的价值。
+ 因为推断把 prise 也当成一个合法字段收下了。它会在很久以后、别处读 price 时才重新出现。这就是给共享数据立契约的价值。
>
}
/>
@@ -702,8 +696,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 推断就是编译器替你把类型算出来。let 加初始值,
- 类型当场就定了,你一个冒号都没写 ——
+ 推断就是编译器替你把类型算出来。let 加初始值,类型当场就定了,你一个冒号都没写 ——
所以「用 TS 就得到处写注解」是个误解。
>
}
@@ -797,8 +790,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- const 意味着变量不会再变,所以类型可以就是那个值本身。
- 字面量类型是 03 章联合类型的地基:
+ const 意味着变量不会再变,所以类型可以就是那个值本身。字面量类型是 03 章联合类型的地基:
"small" | "medium" | "large"
{" "}
@@ -891,8 +883,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 恰恰相反 —— 官方建议注解用小写原始类型名;
- 大写的包装对象类型是历史遗留,几乎从来不是你想要的。
+ 恰恰相反 —— 官方建议注解用小写原始类型名;大写的包装对象类型是历史遗留,几乎从来不是你想要的。
>
}
/>,
@@ -918,8 +909,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 口诀:注解一律小写 —— string、number、boolean。
- 看到注解里出现 String / Number /{" "}
+ 口诀:注解一律小写 —— string、number、boolean。看到注解里出现 String / Number /{" "}
Boolean,直接改。
>
}
@@ -981,8 +971,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 想想「边界」两个字 —— 别人要依赖的地方,
- 以及声明时右边没有值的地方。你还漏了一个。
+ 想想「边界」两个字 —— 别人要依赖的地方,以及声明时右边没有值的地方。你还漏了一个。
>
}
/>
@@ -998,8 +987,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 你选的里面有一项,TS 看一眼初始值就知道了。
- 给它写注解不算错,但属于噪音 —— 就是 §08 的坑二。
+ 你选的里面有一项,TS 看一眼初始值就知道了。给它写注解不算错,但属于噪音 —— 就是 §08 的坑二。
>
}
/>
@@ -1018,8 +1006,7 @@ export const QUIZ: QuizItem[] = [
zh={
<>
注解是承诺,推断是观察。承诺写在边界上:参数(开着{" "}
- noImplicitAny 时是必写的)、返回值、共享契约、
- 先声明后赋值的变量。局部变量交给推断,真正的契约才显眼。
+ noImplicitAny 时是必写的)、返回值、共享契约、先声明后赋值的变量。局部变量交给推断,真正的契约才显眼。
>
}
/>
@@ -1089,8 +1076,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 不止一行。从 any 值上取的属性、由它算出的结果、
- 把它传进去的回调参数,统统也变成 any。
+ 不止一行。从 any 值上取的属性、由它算出的结果、把它传进去的回调参数,统统也变成 any。
>
}
/>,
@@ -1140,8 +1126,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- any 的意思是「别检查这个」。当临时手段有用,
- 长期留着就等于把 TS 关掉了。noImplicitAny{" "}
+ any 的意思是「别检查这个」。当临时手段有用,长期留着就等于把 TS 关掉了。noImplicitAny{" "}
这个开关,就是防止 any 从没注解的参数之类的地方悄悄溜进来。
>
}
@@ -1182,8 +1167,7 @@ export const QUIZ: QuizItem[] = [
zh={
<>
number[] 和 Array<number>{" "}
- 是完全相同的类型。元素类型简单时,社区习惯用前者;
- 后者的尖括号是泛型语法,05 章正式拆解。
+ 是完全相同的类型。元素类型简单时,社区习惯用前者;后者的尖括号是泛型语法,05 章正式拆解。
>
}
/>
@@ -1232,8 +1216,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 空数组没有推断材料,类型先落成 any[];
- 一旦这个值离开填充它的作用域被使用,noImplicitAny{" "}
+ 空数组没有推断材料,类型先落成 any[];一旦这个值离开填充它的作用域被使用,noImplicitAny{" "}
就会报错。出生就注解最稳
>
}
@@ -1316,8 +1299,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- new Array() 一样看不出元素类型。
- 问题不在写法,而在缺那张标签。
+ new Array() 一样看不出元素类型。问题不在写法,而在缺那张标签。
>
}
/>,
@@ -1333,8 +1315,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 空箱子出生就贴标签:const toppings: string[] = []。
- 之后 push 错任何东西都会当场报错。
+ 空箱子出生就贴标签:const toppings: string[] = []。之后 push 错任何东西都会当场报错。
>
}
/>
@@ -1353,8 +1334,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- let size = "small" 推断成了 string,
- 但你想让 size 只能取三种杯型。最对路的做法是?
+ let size = "small" 推断成了 string,但你想让 size 只能取三种杯型。最对路的做法是?
>
}
/>
@@ -1442,9 +1422,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- const 的意思是「不能重新赋值」,不是「只能取这三种」。
- 它会把值固定成 "small",
- 而订单里的杯型本来就要换。
+ const 的意思是「不能重新赋值」,不是「只能取这三种」。它会把值固定成 "small",而订单里的杯型本来就要换。
>
}
/>,
@@ -1459,8 +1437,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 做得到,而且这是 TS 最有用的能力之一:
- 把合法值的名单写进类型,名单外的值编译期直接拒收。
+ 做得到,而且这是 TS 最有用的能力之一:把合法值的名单写进类型,名单外的值编译期直接拒收。
>
}
/>,
@@ -1477,9 +1454,7 @@ export const QUIZ: QuizItem[] = [
}
zh={
<>
- 字面量联合就是把「只能是这几个」写成类型。
- 奶茶店案例在 03 章正是这样升级的:杯型、糖度、订单状态,
- 全靠这一招守住。
+ 字面量联合就是把「只能是这几个」写成类型。奶茶店案例在 03 章正是这样升级的:杯型、糖度、订单状态,全靠这一招守住。
>
}
/>
diff --git a/lib/utility-data.tsx b/lib/utility-data.tsx
index 05e6bbe..f3b2dbd 100644
--- a/lib/utility-data.tsx
+++ b/lib/utility-data.tsx
@@ -173,8 +173,7 @@ export const LABS: Lab[] = [
造出三个变体:DraftOrder(全部属性可选)、
PublicOrder(没有 internalNote
,其余属性全部只读)、CheckoutPatch(只有{" "}
- size 和 toppings,且都可选)。
- 再给每个类型各声明一个变量。给 PublicOrder 的变量写{" "}
+ size 和 toppings,且都可选)。再给每个类型各声明一个变量。给 PublicOrder 的变量写{" "}
internalNote 属性,必须报错。
}
@@ -193,8 +192,7 @@ export const LABS: Lab[] = [
zh={
<>
第二、第三个变体各要套两层。从里往外想:先决定「有哪些属性」(
- Pick 或 Omit),
- 再决定「这些属性怎么用」(Partial 或{" "}
+ Pick 或 Omit),再决定「这些属性怎么用」(Partial 或{" "}
Readonly)。
>
}
@@ -220,9 +218,7 @@ export const LABS: Lab[] = [
}
zh={
- 高亮的三行就是全部答案,六个属性一个都没重抄。
- 以后 Order 增加属性,三个变体自动跟着变。
- 工具类型省下的不是打字,而是「维护多份副本」的成本。
+ 高亮的三行就是全部答案,六个属性一个都没重抄。以后 Order 增加属性,三个变体自动跟着变。工具类型省下的不是打字,而是「维护多份副本」的成本。
}
/>
@@ -258,8 +254,7 @@ export const LABS: Lab[] = [
还是 Playground 里那个 Order。故意把键名拼错:先写{" "}
Omit<Order, "internalNotes">(多一个{" "}
s),再写{" "}
- Pick<Order, "internalNotes">。
- 看哪一个报错、哪一个一声不吭。然后悬停在 Omit{" "}
+ Pick<Order, "internalNotes">。看哪一个报错、哪一个一声不吭。然后悬停在 Omit{" "}
那个类型别名上,数一数它到底产出了几个属性。
}
@@ -276,8 +271,7 @@ export const LABS: Lab[] = [
}
zh={
<>
- 重点在悬停:类型别名会展开成完整的属性列表,
- 删掉了谁、没删掉谁,一眼就能看清。
+ 重点在悬停:类型别名会展开成完整的属性列表,删掉了谁、没删掉谁,一眼就能看清。
>
}
/>
@@ -302,10 +296,7 @@ export const LABS: Lab[] = [
}
zh={
- 记住这条规则:Pick 会检查键,Omit 不检查。
- 当你删掉一个属性是因为它绝对不能外泄时,一个不报错的拼写错误
- 就意味着这个属性还在。这种场合改用 Pick 白名单,
- 或者学完第 07 章,自己写一个严格版的 Omit。
+ 记住这条规则:Pick 会检查键,Omit 不检查。当你删掉一个属性是因为它绝对不能外泄时,一个不报错的拼写错误就意味着这个属性还在。这种场合改用 Pick 白名单,或者学完第 07 章,自己写一个严格版的 Omit。
}
/>
@@ -336,10 +327,8 @@ export const LABS: Lab[] = [
}
zh={
- 用 Record<Size, number> 给三个杯型建一张库存表。
- 然后删掉 large 这一项,读一下报错。再声明一个类型为{" "}
- {"{ [k: string]: number }"} 的版本,同样只写两项,
- 想清楚它为什么能编译过。
+ 用 Record<Size, number> 给三个杯型建一张库存表。然后删掉 large 这一项,读一下报错。再声明一个类型为{" "}
+ {"{ [k: string]: number }"} 的版本,同样只写两项,想清楚它为什么能编译过。
}
/>
@@ -389,11 +378,8 @@ export const LABS: Lab[] = [
zh={
一条实用规则:键是一份已知的、有限的名单时,用{" "}
- Record 配字面量联合,编译器会帮你查有没有漏;
- 键真的开放(用户输入、动态字典),再退回索引签名或{" "}
- Record<string, V>。注意最后两行:
- 两种写法在读取属性时都不会加上 | undefined。
- 那是另一个编译选项 noUncheckedIndexedAccess{" "}
+ Record 配字面量联合,编译器会帮你查有没有漏;键真的开放(用户输入、动态字典),再退回索引签名或{" "}
+ Record<string, V>。注意最后两行:两种写法在读取属性时都不会加上 | undefined。那是另一个编译选项 noUncheckedIndexedAccess{" "}
的事,第 10 章再讲。
}
@@ -476,9 +462,7 @@ export const LABS: Lab[] = [
}
zh={
- 第 7 行在真实项目里出现得很频繁。很多库只导出函数,
- 不导出返回结果的类型,这一行就是取回你需要的类型的标准写法。
- 库以后改了返回类型,这一行也依然是对的。
+ 第 7 行在真实项目里出现得很频繁。很多库只导出函数,不导出返回结果的类型,这一行就是取回你需要的类型的标准写法。库以后改了返回类型,这一行也依然是对的。
}
/>
@@ -601,8 +585,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 类型在编译后会被擦除(第 01 章讲过),所以类型不可能产生任何运行时行为。
- 默认值还是得你自己写。
+ 类型在编译后会被擦除(第 01 章讲过),所以类型不可能产生任何运行时行为。默认值还是得你自己写。
>
),
},
@@ -654,8 +637,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 不报错。新类型有 Order 的全部六个属性,
- 拼错等于什么都没删。
+ 不报错。新类型有 Order 的全部六个属性,拼错等于什么都没删。
>
),
},
@@ -691,8 +673,7 @@ export const QUIZ: QuizItem[] = [
<>
那是 Pick 的行为:它的键参数受{" "}
K extends keyof T 约束。Omit{" "}
- 的键参数只受 keyof any 约束,
- 任何 string、number、symbol 都收。
+ 的键参数只受 keyof any 约束,任何 string、number、symbol 都收。
>
),
},
@@ -707,8 +688,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- Omit 返回的是对象类型,不是 never。
- 它删掉名单上的键,名单上一个都对不上,就一个都不删。
+ Omit 返回的是对象类型,不是 never。它删掉名单上的键,名单上一个都对不上,就一个都不删。
>
),
},
@@ -722,8 +702,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- TypeScript 没有介于报错和通过之间的「警告」这一档。
- 这里就是干干净净地通过 —— 危险正在这里。
+ TypeScript 没有介于报错和通过之间的「警告」这一档。这里就是干干净净地通过 —— 危险正在这里。
>
),
},
@@ -739,9 +718,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- Omit 的键参数只要求是一个合法的属性键,
- 不要求真的存在于 T 上。拼错照样编译通过,属性照样留着。
- 删属性是为了防外泄时,优先用 Pick 白名单。
+ Omit 的键参数只要求是一个合法的属性键,不要求真的存在于 T 上。拼错照样编译通过,属性照样留着。删属性是为了防外泄时,优先用 Pick 白名单。
>
),
},
@@ -757,8 +734,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 已有 const o: Readonly<Order> = …,
- 下面哪一行会被编译器拒绝?
+ 已有 const o: Readonly<Order> = …,下面哪一行会被编译器拒绝?
>
),
},
@@ -817,8 +793,7 @@ export const QUIZ: QuizItem[] = [
<>
这一行合法。Readonly 是浅的:它拦住「给{" "}
o.toppings 赋值」,但拦不住修改{" "}
- o.toppings 指向的那个数组。
- 想连 push 一起拦,属性类型得写成{" "}
+ o.toppings 指向的那个数组。想连 push 一起拦,属性类型得写成{" "}
readonly string[]。
>
),
@@ -847,8 +822,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 只有赋值那一行被拒绝。读取不受限制,改动数组内容的方法也不受限制,
- 因为 Readonly 是浅的。
+ 只有赋值那一行被拒绝。读取不受限制,改动数组内容的方法也不受限制,因为 Readonly 是浅的。
>
),
},
@@ -865,8 +839,7 @@ export const QUIZ: QuizItem[] = [
zh: (
<>
readonly 拒绝的是「给属性赋值」,o.size = …{" "}
- 正是这种。push 没有给 o.toppings 赋值,
- 它改的是这个属性已经指向的那个数组。
+ 正是这种。push 没有给 o.toppings 赋值,它改的是这个属性已经指向的那个数组。
>
),
},
@@ -882,8 +855,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 下面哪些工具类型接受一个联合类型,
- 并返回一个「留下或去掉部分成员」的联合类型?(多选)
+ 下面哪些工具类型接受一个联合类型,并返回一个「留下或去掉部分成员」的联合类型?(多选)
>
),
},
@@ -922,8 +894,7 @@ export const QUIZ: QuizItem[] = [
zh: (
<>
你选中的有一个是作用在对象属性上的。Partial、
- Pick、Readonly 改的是对象类型的属性。
- 问自己一句:我手上这个类型,是一组属性,还是一份候选名单?
+ Pick、Readonly 改的是对象类型的属性。问自己一句:我手上这个类型,是一组属性,还是一份候选名单?
>
),
},
@@ -941,8 +912,7 @@ export const QUIZ: QuizItem[] = [
<>
三个作用在联合成员上:Exclude 去掉匹配的成员,
Extract 只留下匹配的成员,NonNullable{" "}
- 清掉 null 和 undefined。
- 三者都是条件类型,会对每个成员分别求值。第 07 章会讲这是怎么做到的。
+ 清掉 null 和 undefined。三者都是条件类型,会对每个成员分别求值。第 07 章会讲这是怎么做到的。
>
),
},
@@ -979,8 +949,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- Record<Size, number> 要求三个键都在,少一个就报错;
- 索引签名没有「必须齐全」的键名单。
+ Record<Size, number> 要求三个键都在,少一个就报错;索引签名没有「必须齐全」的键名单。
>
),
},
@@ -1039,8 +1008,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 两者的值类型都可以任意写,对象、数组、函数都行。区别在键:
- 一边是有限的名单,一边是无限的集合。
+ 两者的值类型都可以任意写,对象、数组、函数都行。区别在键:一边是有限的名单,一边是无限的集合。
>
),
},
@@ -1054,8 +1022,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 两者都是类型,编译时都会被擦除。运行时的数据结构就是那个对象字面量本身,
- 跟你用哪种类型标注无关。
+ 两者都是类型,编译时都会被擦除。运行时的数据结构就是那个对象字面量本身,跟你用哪种类型标注无关。
>
),
},
@@ -1073,8 +1040,7 @@ export const QUIZ: QuizItem[] = [
zh: (
<>
键类型是有限的字面量联合时,Record{" "}
- 会检查每个键都在,这就是它比索引签名严格的地方。
- 两者在读取属性时都不会加上 | undefined,除非开启{" "}
+ 会检查每个键都在,这就是它比索引签名严格的地方。两者在读取属性时都不会加上 | undefined,除非开启{" "}
noUncheckedIndexedAccess。
>
),
@@ -1171,8 +1137,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 它不做任何检查,只负责取类型。取到的类型合不合适,
- 由外面 ReturnType 的约束来判断。
+ 它不做任何检查,只负责取类型。取到的类型合不合适,由外面 ReturnType 的约束来判断。
>
),
},
@@ -1186,8 +1151,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 类型碰不到正在运行的程序,这里一行代码都不会执行。
- 类型是编译器算出来的,不是跑出来看到的。
+ 类型碰不到正在运行的程序,这里一行代码都不会执行。类型是编译器算出来的,不是跑出来看到的。
>
),
},
@@ -1203,9 +1167,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- makeOrder 是值,而 ReturnType 需要类型。
- 类型位置上的 typeof 就是「从值取到它的类型」的桥。
- 它和运行时的同名运算符做的不是一件事,第 07 章会详细讲。
+ makeOrder 是值,而 ReturnType 需要类型。类型位置上的 typeof 就是「从值取到它的类型」的桥。它和运行时的同名运算符做的不是一件事,第 07 章会详细讲。
>
),
},
@@ -1238,8 +1200,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- Awaited 会一直拆:一层不够就再拆一层,
- 直到没有 Promise 为止。这和运行时 await{" "}
+ Awaited 会一直拆:一层不够就再拆一层,直到没有 Promise 为止。这和运行时 await{" "}
的行为一致。
>
),
@@ -1255,8 +1216,7 @@ export const QUIZ: QuizItem[] = [
zh: (
<>
Awaited 会递归地拆壳,两层 Promise{" "}
- 都被拆掉,剩下 number。
- 类型为什么可以递归,第 07 章会讲。
+ 都被拆掉,剩下 number。类型为什么可以递归,第 07 章会讲。
>
),
},
@@ -1345,8 +1305,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 那是「深 Partial」的行为。内置的这个是浅的:
- 它只给最外层对象的属性加 ?,不会进到属性类型内部。
+ 那是「深 Partial」的行为。内置的这个是浅的:它只给最外层对象的属性加 ?,不会进到属性类型内部。
>
),
},
@@ -1360,8 +1319,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 方向反了。Partial 处理的是外层对象的键,
- 这里只有 meta 一个。
+ 方向反了。Partial 处理的是外层对象的键,这里只有 meta 一个。
>
),
},
@@ -1386,9 +1344,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- Partial 是浅的:它给最外层属性加 ?,
- 对这些属性的类型不做任何处理。深版本得自己写,
- 用到第 07 章的映射类型和条件类型。
+ Partial 是浅的:它给最外层属性加 ?,对这些属性的类型不做任何处理。深版本得自己写,用到第 07 章的映射类型和条件类型。
>
),
},
@@ -1481,8 +1437,7 @@ export const QUIZ: QuizItem[] = [
<>
这是常见的猜法,但 Required 是用 -?{" "}
定义的,去掉 ? 的同时也从属性类型里去掉了{" "}
- undefined,结果是 string。注意:
- 如果属性本来写成 note: string | undefined(没有{" "}
+ undefined,结果是 string。注意:如果属性本来写成 note: string | undefined(没有{" "}
?),那个 undefined 会保留。
>
),
@@ -1496,8 +1451,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 什么都没丢。Required 只改可选标记,
- 属性类型仍然是 string。
+ 什么都没丢。Required 只改可选标记,属性类型仍然是 string。
>
),
},
@@ -1510,8 +1464,7 @@ export const QUIZ: QuizItem[] = [
),
zh: (
<>
- 把可选属性变成必填正是 Required 的用途,
- 不会有任何东西变成 never。
+ 把可选属性变成必填正是 Required 的用途,不会有任何东西变成 never。
>
),
},
@@ -1530,8 +1483,7 @@ export const QUIZ: QuizItem[] = [
<>
Required<A> 得到 note: string:去掉{" "}
? 的同时,属性类型里的 undefined{" "}
- 也被去掉。这只适用于本来写了 ? 的属性;
- 必填属性上显式写的 | undefined 不受影响。
+ 也被去掉。这只适用于本来写了 ? 的属性;必填属性上显式写的 | undefined 不受影响。
>
),
},
diff --git a/test/zh-copy-spacing.test.ts b/test/zh-copy-spacing.test.ts
new file mode 100644
index 0000000..f303ba8
--- /dev/null
+++ b/test/zh-copy-spacing.test.ts
@@ -0,0 +1,155 @@
+// @vitest-environment node
+// JSX trims each line of a text block and joins the lines with one space, so a
+// line break inside Chinese copy renders as a stray space ("算。 建议"); a space
+// written after full-width punctuation ("。 暴力:") shows up the same way.
+// The site's convention keeps a space between Chinese and Latin letters or
+// digits ("第 3 章"), after an expression such as O(1), around "——" and around
+// math operators, and nowhere else. This scans every TSX file for the rest.
+
+import { readdirSync, readFileSync } from "node:fs";
+import { join } from "node:path";
+import ts from "typescript";
+import { describe, expect, it } from "vitest";
+
+const HAN = /[㐀-鿿豈-]/u;
+const FW = /[ -〿-]/u; // CJK and full-width punctuation
+const ZH = /[㐀-鿿豈- -〿-]/u;
+const PUNCT = /[、-〃〈-】〔-〟!-/:-?[-`{-・]/u;
+const K = ts.SyntaxKind;
+
+/** An ellipsis that closes Chinese text, not one inside a spaced math sequence. */
+function closesChinese(raw: string, i: number) {
+ let j = i;
+ while (j > 0 && raw[j - 1] === "…") j--;
+ return j > 0 && ZH.test(raw[j - 1]);
+}
+
+/** Whether the line break between raw[i] and the next text should not be a space. */
+function breakIsStray(raw: string, i: number, a: string, b: string) {
+ if (FW.test(a) && b !== "—") return true;
+ if (a === "…" && closesChinese(raw, i) && b !== "—") return true;
+ if (FW.test(b) && a !== "—") return true;
+ if (HAN.test(a) && (HAN.test(b) || b === "(" || b === "“")) return true;
+ if (/[,:;?!]/.test(a) && HAN.test(b)) return true;
+ if (a === ")" && HAN.test(b)) {
+ const open = raw.lastIndexOf("(", i);
+ return open > 0 && ZH.test(raw[open - 1]);
+ }
+ return false;
+}
+
+/** What JSX renders for a text node. */
+function rendered(raw: string) {
+ if (!raw.includes("\n")) return raw;
+ return raw
+ .split("\n")
+ .map((l, i, all) => (i === 0 ? l.trimEnd() : i === all.length - 1 ? l.trimStart() : l.trim()))
+ .filter((l) => l.length)
+ .join(" ");
+}
+
+function edge(node: ts.Node, sf: ts.SourceFile, last: boolean): string | null | undefined {
+ if (node.kind === K.JsxText) {
+ const t = rendered(node.getFullText(sf));
+ return t.length ? t[last ? t.length - 1 : 0] : null;
+ }
+ if (ts.isJsxExpression(node)) {
+ const e = node.expression;
+ if (e && (ts.isStringLiteral(e) || ts.isNoSubstitutionTemplateLiteral(e)))
+ return e.text.length ? e.text[last ? e.text.length - 1 : 0] : null;
+ return undefined;
+ }
+ if (ts.isJsxElement(node) || ts.isJsxFragment(node)) {
+ const kids = node.children.filter((c) => !(c.kind === K.JsxText && rendered(c.getFullText(sf)) === ""));
+ return kids.length ? edge(last ? kids[kids.length - 1] : kids[0], sf, last) : null;
+ }
+ return undefined;
+}
+
+/** The text a JSX node renders, ignoring expressions other than string literals. */
+function textOf(node: ts.Node, sf: ts.SourceFile): string {
+ if (node.kind === K.JsxText) return rendered(node.getFullText(sf));
+ if (ts.isJsxExpression(node))
+ return node.expression && ts.isStringLiteral(node.expression) ? node.expression.text : "";
+ if (ts.isJsxElement(node) || ts.isJsxFragment(node))
+ return node.children.map((c) => textOf(c, sf)).join("");
+ return "";
+}
+
+// A tag that holds a spaced formula ("反转值 ≥ 剩余值", "分 / 治 / 合") keeps a space on each
+// side, like the operators inside it
+const FORMULA = / [=≠≥≤<>/→←⟺+−×÷] /;
+
+function strays(file: string): string[] {
+ const text = readFileSync(file, "utf8");
+ const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
+ const found: string[] = [];
+ const at = (pos: number, what: string) =>
+ found.push(`${file}:${sf.getLineAndCharacterOfPosition(pos).line + 1} ${what}`);
+
+ const scanText = (start: number, raw: string) => {
+ for (const m of raw.matchAll(/(\S)([ \t]*\n[ \t\n]*)(\S)/g)) {
+ if (breakIsStray(raw, m.index!, m[1], m[3])) at(start + m.index!, `line break ${m[1]}⏎${m[3]}`);
+ }
+ for (const m of raw.matchAll(/(\S)( +)(\S)/g)) {
+ const a = m[1], b = m[3];
+ const stray = (PUNCT.test(a) || (a === "…" && closesChinese(raw, m.index!))) && ZH.test(b) && b !== "—";
+ if (stray) at(start + m.index!, `space ${a} ${b}`);
+ }
+ };
+
+ const visit = (node: ts.Node) => {
+ if (node.kind === K.JsxText) scanText(node.pos, node.getFullText(sf));
+ else if (ts.isStringLiteral(node) && node.parent?.kind === K.JsxAttribute)
+ scanText(node.getStart(sf), node.getText(sf));
+ if (ts.isJsxElement(node) || ts.isJsxFragment(node)) {
+ const kids = node.children.filter((c) => !(c.kind === K.JsxText && rendered(c.getFullText(sf)) === ""));
+ kids.forEach((c, i) => {
+ const prev = kids[i - 1];
+ const next = kids[i + 1];
+ // A space written on the same line between Chinese text and a tag whose text is
+ // Chinese ("叫 前缀函数", " 能解") is as stray as a line break there. A line
+ // break next to a tag renders as nothing, so only spaces count.
+ if (c.kind === K.JsxText) {
+ const raw = c.getFullText(sf);
+ const lead = /^( +)(\S)/.exec(raw);
+ const before = prev ? edge(prev, sf, true) : null;
+ if (lead && before && HAN.test(before) && HAN.test(lead[2]) && prev && ts.isJsxElement(prev) && !FORMULA.test(textOf(prev, sf)))
+ at(c.pos, `space after ${prev.openingElement.tagName.getText(sf)}> before ${lead[2]}`);
+ const trail = /(\S)( +)$/.exec(raw);
+ const after = next ? edge(next, sf, false) : null;
+ if (trail && after && HAN.test(after) && HAN.test(trail[1]) && next && ts.isJsxElement(next) && !FORMULA.test(textOf(next, sf)))
+ at(c.end - 1, `space before <${next.openingElement.tagName.getText(sf)}> after ${trail[1]}`);
+ }
+ if (!prev) return;
+ const a = edge(prev, sf, true);
+ if (!a || !PUNCT.test(a)) return;
+ if (ts.isJsxExpression(c) && c.expression && ts.isStringLiteral(c.expression) && /^ +$/.test(c.expression.text) && next) {
+ const b = edge(next, sf, false);
+ if (b && ZH.test(b)) at(c.getStart(sf), `{" "} after ${a}`);
+ }
+ if (ts.isJsxElement(c) && c.children[0]?.kind === K.JsxText) {
+ const m = /^( +)(\S)/.exec(c.children[0].getFullText(sf));
+ if (m && ZH.test(m[2])) at(c.children[0].pos, `space opening <${c.openingElement.tagName.getText(sf)}> after ${a}`);
+ }
+ });
+ }
+ ts.forEachChild(node, visit);
+ };
+ visit(sf);
+ return found;
+}
+
+function tsxFiles(dir: string): string[] {
+ return readdirSync(dir, { withFileTypes: true }).flatMap((e) =>
+ e.isDirectory() ? tsxFiles(join(dir, e.name)) : e.name.endsWith(".tsx") ? [join(dir, e.name)] : [],
+ );
+}
+
+describe("Chinese copy", () => {
+ it("has no line break or space that renders as a stray space", () => {
+ const files = [...tsxFiles("app"), ...tsxFiles("lib")];
+ expect(files.length).toBeGreaterThan(30);
+ expect(files.flatMap(strays)).toEqual([]);
+ });
+});
From 06cb4c680650646326c36ca7dbfbbb43ba7a57f4 Mon Sep 17 00:00:00 2001
From: WEIREN FENG
Date: Fri, 9 Oct 2026 22:58:29 -0700
Subject: [PATCH 2/2] Qualify chapter 09's module and declaration-lookup rules;
update chapter 08's details
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Chapter 09
- Whether a file is a module is decided by the file only with the default moduleDetection:
with "force", which TypeScript 5.9's tsc --init writes, every non-declaration file is a
module. The callout (title and text) and the quiz now say so; declaration files still
follow the top-level import/export rule, so the .d.ts discussion is unchanged.
- The three sources of a library's types are a checklist, not the compiler's lookup order:
a declare module you have already written takes precedence over the package's types and
@types. The section summary, the flow's first and last frames and the key point say so,
with the practical advice to delete a stopgap declaration once real types exist.
Chapter 08
- Since TypeScript 5.1 a getter and a setter may have unrelated types when both are
annotated; #field keeps a value out of JSON.stringify and logs, but DevTools still shows
it.
Both chapters
- Formal Chinese register (老老实实, 撞上, 坑) and the course terms 注解 / 标记 instead of
标注; quiz titles 本章测验 / Chapter quiz; aria-pressed on the segmented buttons of the
member, placement, erasure and output visuals.
Co-Authored-By: Claude Opus 5.5
---
app/classes/page.tsx | 24 ++++++++++++++----------
app/classes/viz.tsx | 3 +++
app/modules/page.tsx | 35 ++++++++++++++++++++---------------
app/modules/viz.tsx | 24 ++++++++++++++----------
lib/classes-data.tsx | 4 ++--
lib/modules-data.tsx | 26 ++++++++++++++------------
6 files changed, 67 insertions(+), 49 deletions(-)
diff --git a/app/classes/page.tsx b/app/classes/page.tsx
index d0692d5..ebeac08 100644
--- a/app/classes/page.tsx
+++ b/app/classes/page.tsx
@@ -730,7 +730,7 @@ export default function ClassesPage() {
}
zh={
<>
- 方法没有新规则。方法就是写在类里的函数,参数和返回值的标注方式和第 02 章完全一样。真正的新内容从下一节开始:规定哪些代码可以读某个成员。
+ 方法没有新规则。方法就是写在类里的函数,参数和返回值的注解方式和第 02 章完全一样。真正的新内容从下一节开始:规定哪些代码可以读某个成员。
>
}
/>
@@ -1123,9 +1123,10 @@ export default function ClassesPage() {
A password or a token stored in a private field is
not protected. One JSON.stringify(shop) prints it.
So does the browser's DevTools, and so does any log line
- that serializes the object. #field does keep the
- value out of those places, but it is still plain text in memory
- and it is still in your bundle if you hard-coded it. Real
+ that serializes the object. #field keeps the value out
+ of JSON.stringify and most serialized logs, but the
+ browser's DevTools still shows it, it is still plain text
+ in memory, and it is still in your bundle if you hard-coded it. Real
secrets belong on a server, not in a field of either kind.
>
}
@@ -1135,7 +1136,7 @@ export default function ClassesPage() {
字段并不构成保护。一句 JSON.stringify(shop){" "}
就打印出来了,浏览器 DevTools
一样看得见,任何序列化对象的日志也一样。
- #field 确实能挡住这几处,但值仍然是内存里的明文,如果是硬编码的,它照样在你的打包产物里。真正的机密应该放在服务端,而不是任何一种字段里。
+ #field 能让值不出现在 JSON.stringify 和多数序列化日志里,但浏览器 DevTools 照样看得见,值仍然是内存里的明文,如果是硬编码的,它照样在你的打包产物里。真正的机密应该放在服务端,而不是任何一种字段里。
>
}
/>
@@ -1179,7 +1180,7 @@ export default function ClassesPage() {
lang="ts"
title={{
en: "Written out · four lines of boilerplate",
- zh: "老老实实写 · 四行样板",
+ zh: "完整地写出 · 四行样板",
}}
code={S4_VERBOSE}
/>
@@ -1303,7 +1304,10 @@ export default function ClassesPage() {
set sugar(v: number | string) together with{" "}
get sugar(): number lets callers write{" "}
o.sugar = "30" while{" "}
- o.sugar still reads as a number.
+ o.sugar still reads as a number. Since
+ TypeScript 5.1 even that restriction is gone when both sides
+ have explicit types, although a property whose read and write
+ types are unrelated usually confuses callers.
>
}
zh={
@@ -1313,7 +1317,7 @@ export default function ClassesPage() {
set sugar(v: number | string) 配上{" "}
get sugar(): number,调用方可以写{" "}
o.sugar = "30",而读 o.sugar{" "}
- 拿到的仍然是 number。
+ 拿到的仍然是 number。TypeScript 5.1 起,只要两边都显式写了类型,连这条限制也取消了;不过读写类型毫不相关的属性通常会让调用方困惑。
>
}
/>
@@ -1450,7 +1454,7 @@ export default function ClassesPage() {
}
zh={
<>
- ① implements 不会改变类的类型:它不添加成员,也不替你标注任何东西。如果写成{" "}
+ ① implements 不会改变类的类型:它不添加成员,也不替你写任何类型注解。如果写成{" "}
pay(amount) {"{ … }"} 而不标类型,
amount 不会变成 number,而是隐式 any,在 strict 下直接报错
ts(7006)。参数类型得自己写,编译器再拿你写的去和接口核对。
@@ -1618,7 +1622,7 @@ export default function ClassesPage() {
setMember(i)}
>
{gm.ico} {gm.mod} {gm.key}
@@ -248,6 +249,7 @@ export function AccessGate() {
key={i}
type="button"
className={`seg-btn${place === i ? " on" : ""}`}
+ aria-pressed={place === i}
onClick={() => setPlace(i)}
>
{L(gp.label)}
@@ -414,6 +416,7 @@ export function ErasureViz() {
key={et.label}
type="button"
className={`seg-btn${tab === i ? " on" : ""}`}
+ aria-pressed={tab === i}
onClick={() => setTab(i)}
>
{et.label}
diff --git a/app/modules/page.tsx b/app/modules/page.tsx
index c100685..4707828 100644
--- a/app/modules/page.tsx
+++ b/app/modules/page.tsx
@@ -752,8 +752,8 @@ export default function ModulesPage() {
module option in{" "}
tsconfig.json chooses the output format:{" "}
esnext, commonjs, and so on. It does
- not decide whether a given file is a module. Only the file
- itself does that, by having a top-level import or export.
+ not decide whether a given file is a module. By default the
+ file itself does that, by having a top-level import or export.
+ A separate option, moduleDetection, can change the
+ rule: with "force", every non-declaration
+ file is treated as a module, and that is what{" "}
+ tsc --init writes in TypeScript 5.9. Declaration
+ files are not affected; for them a top-level import or export
+ still decides.
This matters most in .d.ts files, where it is easy
@@ -777,10 +783,7 @@ export default function ModulesPage() {
zh={
<>
- tsconfig.json 里的 module{" "}
- 选的是输出格式:esnext、
- commonjs 之类。它不决定某个文件是不是模块 ——
- 只有文件自己能决定,靠顶层的 import 或 export。
+ tsconfig.json 里的 module 选的是输出格式:esnext、commonjs 之类。它不决定某个文件是不是模块 —— 默认由文件自己决定,靠顶层的 import 或 export。tsconfig 里另有一个 moduleDetection 选项可以改变规则:设为 "force" 时,所有非声明文件都按模块处理 —— TypeScript 5.9 的 tsc --init 生成的配置就是这样。.d.ts 文件不受它影响,仍然看顶层的 import 或 export。
这一点在 .d.ts 里最要紧,因为那种文件很容易两者都没有。同一段{" "}
@@ -1452,8 +1455,8 @@ export default function ModulesPage() {
zh: "一个库的类型从哪里来",
}}
desc={{
- en: "Three places, checked in order. The compiler stops at the first one that answers.",
- zh: "三个地方,按顺序查。哪一个先有结果,编译器就停在那里。",
+ en: "Three sources. Check them in this order when you investigate, but a declare module you wrote yourself, once it exists, takes precedence over the other two.",
+ zh: "三个来源。排查时按这个顺序看;但要知道,你自己写的 declare module 一旦存在,优先级最高。",
}}
>
import type 和 export type{" "}
把一行标为「只有类型」,于是整行被删除,运行时不会加载任何模块。verbatimModuleSyntax{" "}
- 把这个标注变成必需:导入报 ts(1484),转手导出报 ts(1205)。
+ 把这个标记变成必需:导入报 ts(1484),转手导出报 ts(1205)。
>
),
},
@@ -2413,7 +2416,10 @@ export default function ModulesPage() {
package's own types (or the{" "}
types condition in exports), then{" "}
@types from DefinitelyTyped, then a declaration you
- write. All three empty means ts(7016). Keep the{" "}
+ write. All three empty means ts(7016). A{" "}
+ declare module you have already written takes
+ precedence over the other two, so remove a stopgap declaration
+ once real types exist. Keep the{" "}
@types major and minor version aligned with the
library.
>
@@ -2424,8 +2430,7 @@ export default function ModulesPage() {
types(或 exports 里的{" "}
types 条件)→ DefinitelyTyped 的{" "}
@types → 你自己写的声明。三个都空就是{" "}
- ts(7016)。@types 的 major 和 minor
- 版本要和库本体对齐。
+ ts(7016)。你已经写好的 declare module 优先于前两者,有了真正的类型之后,要删掉当初的临时声明。@types 的 major 和 minor 版本要和库本体对齐。
>
),
},
diff --git a/app/modules/viz.tsx b/app/modules/viz.tsx
index c8d20b7..e94f530 100644
--- a/app/modules/viz.tsx
+++ b/app/modules/viz.tsx
@@ -227,6 +227,7 @@ export function ErasedImports() {