|
|
### 6.5.1 问题:为什么是 `() => ({})` 而不是 `() => {}`?
**疑问代码**:
```typescript
state: () => ({ // ← 为什么多了一层括号?
isLogin: false
})
// 为什么不是这样?
state: () => { // ← 这种写法是错误的
isLogin: false
}
```
---
### 6.5.2 答案:这是箭头函数返回对象的语法
**核心语法**:箭头函数返回对象时,必须用圆括号包裹对象
```typescript
// ✅ 正确:用圆括号包裹对象
const fn1 = () => ({ name: 'Alice' });
// ❌ 错误:没有圆括号,会被识别为代码块
const fn2 = () => { name: 'Alice' };
// ✅ 正确:显式 return
const fn3 = () => {
return { name: 'Alice' };
};
```
---
### 6.5.3 语法解析
#### 语法 1:`() => ({})` 箭头函数直接返回对象
```typescript
state: () => ({
isLogin: false
})
```
**解析**:
```
state: () => ({ isLogin: false })
│ │ └─────────────────┘
│ │ │
│ │ 返回的对象字面量
│ │
│ └─ 箭头(=>)
│
└─ 参数列表(空)
```
**等价写法**:
```typescript
// 写法 1:箭头函数直接返回对象(简洁)
state: () => ({
isLogin: false
})
// 写法 2:箭头函数 + return(完整)
state: () => {
return {
isLogin: false
};
}
// 写法 3:传统函数
state: function() {
return {
isLogin: false
};
}
```
---
#### 语法 2:`() => {}` 箭头函数的代码块
```typescript
// ❌ 错误示例
state: () => {
isLogin: false // ← 这会被解析为标签(label),不是对象!
}
```
**为什么错误**:
- 在 JavaScript 中,`{ }` 可以表示**代码块**或**对象字面量**
- 箭头函数的 `=>` 后面紧跟 `{ }` 时,会被解析为**代码块**,而不是对象
- `{ isLogin: false }` 在代码块中会被解析为:
- `isLogin:` 是一个**标签(label)**
- `false` 是一个**表达式语句**
**完整解释**:
```typescript
// 这段代码:
() => {
isLogin: false
}
// 会被解析为:
function() {
isLogin: false // 标签(label),类似于 goto 的标签
// 等价于:
// label: false;
}
```
**标签(Label)示例**:
```typescript
// JavaScript 的标签语法
outerLoop:
for (let i = 0; i < 5; i++) {
for (let j = 0; j < 5; j++) {
if (j === 3) break outerLoop; // 跳出外层循环
}
}
```
---
### 6.5.4 JavaScript 语法规则详解
#### 规则 1:`{ }` 的歧义性
```typescript
// 情况 1:对象字面量
const obj = { name: 'Alice' }; // ✅ 明确是对象
// 情况 2:代码块
{
console.log('Hello'); // ✅ 明确是代码块
}
// 情况 3:箭头函数后的歧义
const fn = () => {
console.log('Hello'); // ✅ 这是代码块
};
const fn2 = () => ({
name: 'Alice' // ✅ 这里的括号强制解析为对象
});
```
---
#### 规则 2:箭头函数的语法优先级
```typescript
// 语法 A:单行表达式(不需要 return 关键字)
() => expression // 自动返回表达式的值
// 语法 B:代码块(需要 return 才能返回值)
() => {
// 代码块
return value; // 必须显式 return
}
// 关键:对象字面量属于表达式,但 { } 看起来像代码块
() => { name: 'Alice' } // ❌ 被解析为代码块
() => ({ name: 'Alice' }) // ✅ 用括号强制解析为表达式
```
---
### 6.5.5 完整对比示例
#### 示例 1:箭头函数的各种写法
```typescript
// ✅ 写法 1:直接返回对象(推荐)
const fn1 = () => ({ name: 'Alice', age: 30 });
// ✅ 写法 2:使用 return
const fn2 = () => {
return { name: 'Alice', age: 30 };
};
// ✅ 写法 3:传统函数
function fn3() {
return { name: 'Alice', age: 30 };
}
// ❌ 写法 4:错误(没有圆括号)
const fn4 = () => { name: 'Alice', age: 30 };
// 等价于:
const fn4 = () => {
name: 'Alice', // 标签
age: 30 // 表达式语句
};
// 返回 undefined(没有 return)
```
---
#### 示例 2:Pinia state 的正确写法
```typescript
// ✅ 正确:箭头函数直接返回对象
export const useUserStore = defineStore('user', {
state: () => ({
isLogin: false,
userInfo: null
})
});
// ✅ 正确:箭头函数 + return
export const useUserStore = defineStore('user', {
state: () => {
return {
isLogin: false,
userInfo: null
};
}
});
// ✅ 正确:传统函数
export const useUserStore = defineStore('user', {
state: function() {
return {
isLogin: false,
userInfo: null
};
}
});
// ❌ 错误:没有圆括号
export const useUserStore = defineStore('user', {
state: () => {
isLogin: false, // 标签
userInfo: null // 表达式语句
// 没有 return,返回 undefined
}
});
```
---
### 6.5.6 为什么需要这个语法?
#### 原因 1:箭头函数的设计
**箭头函数的语法糖**:
```typescript
// 设计目标:让简单的函数更简洁
// 传统写法
function add(a, b) {
return a + b;
}
// 箭头函数(单行表达式)
const add = (a, b) => a + b;
// 箭头函数(多行代码块)
const add = (a, b) => {
const result = a + b;
return result;
};
```
**问题**:如何区分"单行表达式"和"代码块"?
```typescript
// 表达式
() => 42 // 返回数字 42
() => 'hello' // 返回字符串 'hello'
// 代码块
() => { // 代码块开始
console.log('hello');
return 42;
}
// 对象字面量的歧义
() => { x: 1 } // 这是代码块还是对象?
// JavaScript 规则:优先解析为代码块
() => ({ x: 1 }) // 用括号强制解析为表达式
```
---
#### 原因 2:对象字面量的特殊性
```typescript
// 对象字面量在赋值时的语法
const obj = { x: 1 }; // ✅ 明确是对象
// 但在箭头函数后会有歧义
const fn = () => { x: 1 };
// ^^^^^^^^
// 这是代码块还是对象?
//
// JavaScript 解析器:
// 1. 看到 `=> {`,认为 `{` 是代码块开始
// 2. 看到 `x:`,认为是标签(label)
// 3. 看到 `1`,认为是表达式语句
// 4. 代码块结束,没有 return,返回 undefined
// 解决方案:用括号包裹
const fn = () => ({ x: 1 });
// ^^^^^^^
// 括号内只能是表达式,所以解析为对象
```
---
### 6.5.7 语法速查表
| 写法 | 含义 | 返回值 |
|------|------|--------|
| `() => ({ x: 1 })` | 箭头函数返回对象 | `{ x: 1 }` ✅ |
| `() => { x: 1 }` | 箭头函数的代码块 | `undefined` ❌ |
| `() => { return { x: 1 }; }` | 箭头函数代码块 + return | `{ x: 1 }` ✅ |
| `() => x` | 箭头函数返回变量值 | `x` 的值 |
| `() => { x }` | 箭头函数代码块 | `undefined` ❌ |
---
### 6.5.8 实际应用示例
#### 示例 1:Pinia Store 的 state
```typescript
export const useUserStore = defineStore('user', {
// ✅ 箭头函数直接返回对象(最简洁)
state: () => ({
isLogin: false,
userInfo: null,
permissions: []
}),
// ✅ 等价写法(如果需要逻辑)
state: () => {
const initialState = {
isLogin: false,
userInfo: null,
permissions: []
};
return initialState;
},
});
```
---
#### 示例 2:Vue 组件的 data
```typescript
// Vue 3 Composition API
import { ref } from 'vue';
const count = ref(0); // 简单值用 ref
// Vue 3 Options API
export default {
data() {
return { // ✅ 返回对象
count: 0,
message: 'hello'
};
}
};
// ❌ 错误:直接返回对象字面量
export default {
data: () => {
count: 0, // 标签
message: 'hello' // 表达式语句
// 没有 return
}
};
```
---
#### 示例 3:React 组件的 state
```typescript
// React 函数组件
function Counter() {
const [count, setCount] = useState(0); // Hook
// ...
}
// React 类组件
class Counter extends React.Component {
state = { // ✅ 类属性,直接赋值
count: 0
};
}
// ❌ 不相关的语法(仅作对比)
const fn = () => {
count: 0 // 标签,不是对象
};
```
---
### 6.5.9 调试技巧
#### 如何检测错误的写法?
```typescript
// ❌ 错误写法
const state = () => {
isLogin: false
};
// 测试
console.log(state()); // undefined(而不是 { isLogin: false })
// ✅ 正确写法
const state = () => ({
isLogin: false
});
console.log(state()); // { isLogin: false }
```
---
#### TypeScript 的类型检查
```typescript
// TypeScript 能够检测到错误
const fn1 = () => ({ x: 1 }); // ✅ 类型: () => { x: number }
const fn2 = () => { x: 1 }; // ❌ 可能报错:x 没有定义(作为变量)
// 显式类型注解
const fn3: () => { x: number } = () => ({ x: 1 }); // ✅
const fn4: () => { x: number } = () => { x: 1 }; // ❌ 类型错误
```
---
### 6.5.10 关键要点总结
1. **`()` 表示参数列表**
- `()` 表示空参数列表
- 如果有参数:`(arg1, arg2) => ({ ... })`
2. **`=>` 表示箭头函数**
- 分隔参数列表和函数体
3. **`({})` 表示返回对象**
- **外层括号**:强制解析为表达式
- **内层花括号**:对象字面量
- **合起来**:`({ x: 1 })` 是包裹在括号中的对象字面量
4. **如果没有外层括号**
- `{ x: 1 }` 会被解析为代码块
- `x:` 会被解析为标签
- 函数返回 `undefined`
5. **记忆口诀**
```
箭头函数返回对象,外层括号不能少
() => ({ object }) ✅ 正确
() => { object } ❌ 错误
```
---
### 6.5.11 完整语法图解
```
state: () => ({
isLogin: false
})
│ │ │ └────────────────┘
│ │ │ │
│ │ │ 对象字面量
│ │ │ { isLogin: false }
│ │ │
│ │ └─ 圆括号(强制解析为表达式)
│ │
│ └─ 箭头(=>)
│
└─ 参数列表(空)
```
**等价展开**:
```typescript
state: function() {
return {
isLogin: false
};
}
```
---
### 6.5.12 常见错误与解决方案
| 错误代码 | 问题 | 解决方案 |
|---------|------|---------|
| `() => { x: 1 }` | 被解析为代码块 | 改为 `() => ({ x: 1 })` |
| `() => { return { x: 1 } }` | 多余的代码块 | 改为 `() => ({ x: 1 })` |
| `() => { x: 1, y: 2 }` | 多个标签 | 改为 `() => ({ x: 1, y: 2 })` |
---
### 6.5.13 最佳实践建议
```typescript
// ✅ 推荐 1:简单对象直接返回
state: () => ({
isLogin: false,
userInfo: null
})
// ✅ 推荐 2:需要逻辑时使用完整写法
state: () => {
const defaultState = {
isLogin: false,
userInfo: null
};
if (process.env.NODE_ENV === 'development') {
defaultState.debugMode = true;
}
return defaultState;
}
// ❌ 避免:不必要的代码块
state: () => {
return {
isLogin: false
};
}
```
---
**总结**:`state: () => ({})` 是箭头函数返回对象的简洁语法,外层括号是必需的,用于告诉 JavaScript 这是一个对象字面量,而不是代码块。
---
|
|
|
|
|
|
|
|
```typescript
numbers.reduce(
(total, num) => total + num,
0
)
```
`reduce` 在这里的作用是**将数组中的所有元素累加起来**。
## 逐步解析:
### 1. **基本语法**
`reduce` 方法接收两个参数:
- **回调函数**:对每个元素执行的函数
- **初始值**:累加的起始值(这里是 `0`)
### 2. **回调函数的参数**
```typescript
(total, num) => total + num
```
- total:累加器,保存当前的累加结果
- num:当前正在处理的数组元素
### 3. **执行过程**
假设调用 `sum(1, 2, 3, 4)`:
- numbers 数组 = `[1, 2, 3, 4]`
- 初始值 total = `0`
执行步骤:
1. 第一次:`total = 0 + 1 = 1`
2. 第二次:`total = 1 + 2 = 3`
3. 第三次:`total = 3 + 3 = 6`
4. 第四次:`total = 6 + 4 = 10`
最终返回 `10`
## 实际应用示例:
```typescript
// 各种调用方式
console.log(sum(1, 2, 3, 4)); // 输出: 10
console.log(sum(5, 10, 15)); // 输出: 30
console.log(sum()); // 输出: 0(空数组,只返回初始值)
// 也可以用数组调用
const numbers = [1, 2, 3, 4];
console.log(sum(...numbers)); // 输出: 10(展开运算符)
```
## 如果没有 reduce,等效的实现:
```typescript
function sum(...numbers: number[]): number {
let total = 0;
for (let i = 0; i < numbers.length; i++) {
total += numbers[i];
}
return total;
}
```
或者用 `for...of` 循环:
```typescript
function sum(...numbers: number[]): number {
let total = 0;
for (const num of numbers) {
total += num;
}
return total;
}
```
`reduce` 的优势在于代码更简洁、函数式,而且可以链式调用其他数组方法。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
分析一下面代码中单个问号?与双问号??的语法含义
```
catch (error: any) {
// if (type === 'run' || type === 'refreRun') {
console.log('SQL执行失败:', error);
const msg = error?.data?.msg || '';}
if (savedState) {
this.model = savedState.model ?? 0;
this.data = savedState.data ?? {};
this.screen = savedState.screen ?? {};
this.analyze = savedState.analyze ?? {};
}
```
## 代码中 `?` 与 `??` 的语法含义分析
### 1. 可选链操作符 (`?.`)
**语法含义:**
- 可选链操作符允许安全地访问深层嵌套的对象属性,而不必显式验证每个中间引用是否有效。
- 如果引用为 `null` 或 `undefined`,表达式会短路返回 `undefined`。
**代码示例:**
```typescript
const msg = error?.data?.msg || '';
```
**等价写法:**
```typescript
// 传统写法
const msg = (error !== null && error !== undefined && error.data !== null && error.data !== undefined)
? error.data.msg
: undefined;
const finalMsg = msg || '';
// 或者更简洁的传统写法
const msg = error && error.data && error.data.msg || '';
```
**执行逻辑:**
1. 检查 `error` 是否为 `null` 或 `undefined`
2. 如果是,返回 `undefined`,不再继续访问 `.data`
3. 如果不是,继续访问 `error.data`
4. 检查 `error.data` 是否为 `null` 或 `undefined`
5. 如果是,返回 `undefined`
6. 如果不是,返回 `error.data.msg` 的值
7. 最后通过 `|| ''` 将 `undefined` 或假值转换为空字符串
---
### 2. 空值合并操作符 (`??`)
**语法含义:**
- 空值合并操作符是一个逻辑操作符,当左侧操作数为 `null` 或 `undefined` 时,
- 返回右侧操作数;
- 否则返回左侧操作数。
**代码示例:**
```typescript
this.model = savedState.model ?? 0;
this.data = savedState.data ?? {};
this.screen = savedState.screen ?? {};
this.analyze = savedState.analyze ?? {};
```
**等价写法:**
```typescript
// 传统写法1:使用三元运算符
this.model = savedState.model !== null && savedState.model !== undefined
? savedState.model
: 0;
// 传统写法2:使用逻辑或(注意:这种方式有陷阱)
this.model = savedState.model || 0; // 当 model = 0 或 false 时会错误地使用默认值
```
**执行逻辑:**
- 如果 `savedState.model` 是 `null` 或 `undefined`,使用默认值 `0`
- 如果 `savedState.model` 是其他任何值(包括 `0`、`false`、`''`、`NaN`),都使用该值本身
---
### 3. 两者的关键区别
| 操作符 | 名称 | 触发默认值的条件 | 典型使用场景 |
|--------|------|-----------------|--------------|
| `??` | 空值合并 | 仅当值为 `null` 或 `undefined` | 需要保留 `0`、`false`、`''` 等有效值 |
| `||` | 逻辑或 | 值为假值(`null`、`undefined`、`0`、`false`、`''`、`NaN`) | 需要过滤所有假值,用默认值替换 |
| `?.` | 可选链 | 访问不存在属性时不报错 | 安全访问深层嵌套对象 |
---
### 4. 组合使用示例
```typescript
// 可选链 + 空值合并的组合使用
const value = obj?.nested?.property ?? 'default';
// 等价于:
const value = obj !== null && obj !== undefined
&& obj.nested !== null && obj.nested !== undefined
? (obj.nested.property ?? 'default')
: 'default';
```
---
### 5. 注意事项
1. **不能与 `&&` 或 `||` 混用而不加括号:**
```typescript
// ❌ 错误 - 语法错误
const result = null || undefined ?? 'default';
// ✅ 正确
const result = (null || undefined) ?? 'default';
```
2. **`??` 会保留假值:**
```typescript
const count = 0 ?? 100; // 结果为 0
const count = 0 || 100; // 结果为 100
```
3. **可选链支持函数调用:**
```typescript
const result = obj?.method?.(); // 仅当 obj.method 存在且为函数时才调用
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 学习资料总览(docs/study/study_typescript)
| 资料 | 内容 | 建议顺序 |
|------|------|----------|
| 01-基础类型示例.ts | 类型声明、类型推断、联合类型 | 第 1 步 |
| 02-数组操作示例.ts | 数组增删查改、map/filter/reduce | 第 2 步 |
| 03-循环控制示例.ts | for/for-of/while/迭代器 | 第 3 步 |
| 04-类与继承示例.ts | class、继承、修饰符、抽象类 | 第 4 步 |
| 05-泛型与工具类型示例.ts | 泛型函数/类、Partial/Pick 等工具类型 | 第 5 步 |
| 06-实战示例.ts | 综合应用场景 | 第 6 步 |
| 07-Node.js集成示例.ts | Node 环境下使用 TS | 第 7 步 |
| **08-类型收窄与守卫示例.ts**(系列补充) | **收窄/守卫/可辨识联合/any-unknown-never/断言家族/映射类型/模板字面量类型** | 第 8 步(进阶) |
| TypeScript入门指南.md | 12 章完整文档 + 问号?语法 + 可选链?.深度解析 | 全程查阅 |
| lodash.md | Lodash 工具库配合 TS 的实用指南 | 按需 |
### 系统性学习路线(建议 4 个阶段)
```
阶段一 基础语法(01~03) 类型声明 → 数组 → 流程控制 目标:能读写简单 TS
阶段二 面向对象与泛型(04~05)类/继承 → 泛型 → 工具类型 目标:理解类型系统的复用
阶段三 进阶类型编程(08+指南§12~15)收窄/守卫 → 可辨识联合 → 断言 → 映射类型
目标:精确建模业务状态
阶段四 工程与实战(06~07 + lodash)综合场景 → Node 集成 → 工具库
目标:落地到真实项目
```
> 运行方式:`npx tsx 08-类型收窄与守卫示例.ts`(Node 24 下 ts-node@10 不兼容,推荐 tsx)
|
|
### 什么是类型收窄(Narrowing)
联合类型 `string | number` 的值,经过**某种检查**后,TS 能在对应分支里把它"收窄"成更精确的类型,从而放心调用该类型的方法:
```typescript
function demo(value: string | number) {
if (typeof value === "string") {
value.length // ✅ 此分支 value: string
} else {
value.toFixed(2) // ✅ 此分支 value: number
}
}
```
### 四种常用收窄手段
```typescript
// 1. typeof 收窄(原始类型)
if (typeof x === "string") { ... }
// 2. 真值收窄(排除 null / undefined / '' / 0)
if (maybeName) { maybeName.toUpperCase() } // maybeName: string(已排除 null)
// 3. in 操作符收窄(对象有没有某属性)
if ("swim" in pet) { pet.swim() } // pet 收窄为 Fish
// 4. instanceof 收窄(类的实例)
if (err instanceof Error) { err.message } // err 收窄为 Error
```
### 自定义类型守卫(is 谓词)
普通函数返回 `boolean` 只是判断结果;返回 **`参数名 is 类型`** 才能触发收窄:
```typescript
function isCat(p: Cat | Dog): p is Cat {
return p.kind === "cat";
}
function sound(p: Cat | Dog) {
if (isCat(p)) p.meow(); // p 已收窄为 Cat
else p.bark(); // p 已收窄为 Dog
}
```
> 这是处理复杂联合类型的关键武器:判断逻辑集中写一次,收窄效果到处生效。
|
|
### 可辨识联合(Discriminated Union)
联合的每个成员都带同名的**判别字段**(值是字面量类型),`switch` 判别字段后自动收窄。这是 TS 建模"多种状态"最推荐的方式:
```typescript
type Shape =
| { type: "circle"; radius: number }
| { type: "square"; side: number }
| { type: "rect"; width: number; height: number };
function area(s: Shape): number {
switch (s.type) {
case "circle": return Math.PI * s.radius ** 2; // s: circle
case "square": return s.side ** 2; // s: square
case "rect": return s.width * s.height; // s: rect
}
}
```
### 穷尽检查(Exhaustiveness Check)+ never 保险丝
用 `never` 兜底:将来给 `Shape` 新增成员却忘了处理,**编译期**就会报错,而不是运行时出 bug:
```typescript
function assertNever(x: never): never {
throw new Error("未处理的类型: " + x);
}
function areaSafe(s: Shape): number {
switch (s.type) {
case "circle": return Math.PI * s.radius ** 2;
case "square": return s.side ** 2;
case "rect": return s.width * s.height;
default: return assertNever(s); // 若加了新成员,s 这里不是 never → 编译报错
}
}
```
### 适用场景
- 接口请求状态:`idle | loading | success | error`(每态带不同字段)
- 消息/事件建模:不同事件带不同 payload
- 替代"一堆可选字段 + any"的弱类型写法
|
|
### any / unknown / never 三兄弟
| 类型 | 含义 | 能否直接使用 | 使用建议 |
|------|------|--------------|----------|
| `any` | 放弃类型检查(逃生舱) | ✅ 随便用(危险) | 尽量不用,错误推迟到运行时 |
| `unknown` | 安全的 any:能接收任何值 | ❌ 必须先收窄 | 第三方入参、`JSON.parse` 返回值、`catch(err)` |
| `never` | 不可能存在的值 | - | 抛错函数、死循环、穷尽检查 |
```typescript
const u: unknown = "hello";
// u.toUpperCase(); // ❌ 编译错误
if (typeof u === "string") {
u.toUpperCase(); // ✅ 收窄后可用
}
```
### 断言家族
```typescript
// 1. as 断言:"我比编译器更懂"(JSON.parse 返回 any,断言成具体类型)
const user = JSON.parse('{"id":1}') as User;
// 2. const 断言:把值锁定为只读的字面量类型
const arr = [1, 2, 3] as const; // readonly [1, 2, 3]
// arr.push(4); // ❌ 编译错误
// 3. 非空断言 ! :断言不是 null/undefined(确信时才用)
function f(id?: number) { return id!.toFixed(0); }
// 4. 函数重载:同一个函数名按参数类型给出不同的精确返回类型
function parse(x: string): number;
function parse(x: number): string;
function parse(x: string | number): string | number {
return typeof x === "string" ? Number(x) : String(x);
}
parse("12") + 1; // 13(number)
parse(12) + "!"; // "12!"(string)
```
> 断言是"绕过"检查而非"证明"——用之前务必确认运行时类型真的符合,否则类型系统会替你把错误藏起来。
|
|
### 交叉类型(&):多个类型合并成"都要"
```typescript
type Draggable = { drag(): void };
type Resizable = { resize(): void };
type Widget = Draggable & Resizable; // 同时具备两者
```
### 映射类型:像 for 循环一样生成新类型
```typescript
type User = { id: number; name: string };
// 所有字段变可空(遍历 keyof T,取出字段类型 T[K])
type Nullable<T> = { [K in keyof T]: T[K] | null };
// as 重映射键名:加前缀、改名的利器
type UserPreview = { [K in keyof User as `preview_${K}`]: string };
const p: UserPreview = { preview_id: "1", preview_name: "tanpf" };
```
内置工具类型 `Partial` / `Pick` / `Omit` / `Record` 本质都是映射类型(见 05-泛型与工具类型示例)。
### 模板字面量类型:字符串的"排列组合"
```typescript
type Route = "user" | "order";
type Method = "get" | "post";
type ApiName = `${Method}:${Route}`;
// 得到 4 个字面量:"get:user" | "get:order" | "post:user" | "post:order"
const api: ApiName = "post:order"; // ✅
// const bad: ApiName = "delete:user"; // ❌ 编译错误
```
适合:API 路由名、事件名、CSS 属性等"有限组合"的字符串,拼错立刻编译报错。
|
|
### tsconfig 严格模式要点(本系列 tsconfig 已开启)
| 配置 | 作用 |
|------|------|
| `strict: true` | 总开关:启用下面全部严格检查 |
| `strictNullChecks` | null/undefined 不再属于任何类型,必须显式处理(大量运行时空指针错误提前到编译期) |
| `noImplicitAny` | 禁止隐式 any(漏写类型且推断不出时报错) |
| `skipLibCheck: true` | 跳过第三方库内部类型检查,加快编译 |
### 学习建议
1. **顺序**:按"学习路线"四阶段推进,每个示例文件用 `npx tsx 文件名.ts` 跑一遍,改几行看输出
2. **查文档**:TypeScript入门指南.md 共 15 章,§12~§15(问号?语法、可选链?.)是查阅频率最高的部分
3. **少用 any**:遇到不知道类型先想 `unknown` + 收窄;实在要断言,注释写清为什么
4. **状态建模优先用可辨识联合**,配合穷尽检查,新增状态时编译器自动提醒你补分支
5. **对照真实代码练**:如 inspectai/frontend_v2 前端里的 `group?.selected_tables`、`(list) => ...` 都是本系列语法的直接应用
### 环境提示
- Node 24 下 `ts-node@10` 与内置 TS API 不兼容(启动即报 `Cannot read properties of undefined (reading 'fileExists')`),**用 `npx tsx` 替代**
- 编译产物:`npm run build` → `dist/`(tsconfig 中 outDir)
|
|
### 疑问:filter 的能力是否完全包含 find?
**结论**:从"表达能力"上看确实如此(`filter(pred)[0]` 可模拟 `find` 的结果),但两者在**性能、判空方式、语义**上有本质区别——`find` 并非多余,两者是**互补关系而非替代关系**。
### 1. 表达能力:filter 确实可以模拟 find 的结果
```typescript
let users = [
{ id: 1, name: "aa", age: 10 },
{ id: 2, name: "bb", age: 30 },
{ id: 3, name: "cc", age: 50 }
];
users.find(u => u.age === 30); // { id: 2, name: 'bb', age: 30 }
users.filter(u => u.age === 30)[0]; // 同上 ✅
users.find(u => u.age === 99); // undefined
users.filter(u => u.age === 99)[0]; // undefined([][0] 也是 undefined)
```
结果层面完全等价——"能力包含"的直觉在【返回值】维度成立。
### 2. 区别一:性能——find 命中即停(短路),filter 全量遍历
```typescript
let scanCount1 = 0, scanCount2 = 0;
users.find(u => { scanCount1++; return u.age === 10; }); // 第 1 个就命中,停止
users.filter(u => { scanCount2++; return u.age === 10; }); // 仍要遍历全部 3 个
console.log(`find 谓词执行次数: ${scanCount1}`); // 1
console.log(`filter 谓词执行次数: ${scanCount2}`); // 3
```
- `find` 最好情况 O(1):命中即停,不再调用谓词
- `filter` 恒为 O(n):永远扫完整个数组,且**总是分配一个新数组**(额外内存开销)
- 大数组 + 匹配项靠前时,find 快得多
### 3. 区别二:判空方式——空数组 [] 是 truthy,是常见 bug 源
```typescript
let found1 = users.find(u => u.age === 99); // undefined
let found2 = users.filter(u => u.age === 99); // []
console.log(!found1); // true ✅ find 判空:直接取反即可
console.log(!found2); // false ❌ [] 是 truthy!
console.log(found2.length === 0); // true ✅ filter 判空:必须用 .length
```
### 4. 区别三:语义与类型——向读者/编译器声明不同的意图
- `find`:"我只要第一个/唯一的一个",返回类型 `T \| undefined`
- `filter`:"我要所有满足条件的",返回类型 `T[]`
- 用 `filter(...)[0]` 表达"只要一个"意图模糊:是只要一个,还是忘了处理多个?
- TS 5.5+ 的 `find` 还支持类型谓词收窄:`users.find((u): u is Admin => ...)`
|
|
### 谓词(predicate)= 返回布尔值的函数
即"对输入做一个判断,回答是/否"的函数。这个词借自数学的**谓词逻辑**:`x > 5` 本身不是命题(无法判断真假),但给定 x 后就能得出 true/false——这种"等待输入才能判断真假的表达式"就叫谓词。
### 在 find / filter 中的位置
```typescript
users.find(u => u.age === 30)
// └── 谓词 ──┘
```
`find` / `filter` 拿谓词去**逐个询问每个元素**:"这个符合条件吗?"谓词回答 true 的元素就是匹配项。所以对比实验里的"谓词执行次数",意思就是这个判断函数**被调用了几次**。
### 你可能已经写过很多谓词
```typescript
const isAdult = (u: { age: number }) => u.age >= 18; // 是成年人吗?
const isEven = (n: number) => n % 2 === 0; // 是偶数吗?
const notNull = (x: string | null) => x !== null; // 非空吗?
users.filter(isAdult); // 谓词作为参数传入,判断逻辑可复用
numbers.filter(isEven);
```
命名习惯:谓词函数通常用 `is` / `has` / `contains` / `should` 开头,读起来像是在"提问"。
### 进阶:TypeScript 的"类型谓词"(x is T)
普通谓词影响**运行时**筛选哪些元素;类型谓词额外影响**编译时**的类型推断——返回 true 时编译器认定参数就是 T:
```typescript
function isString(x: unknown): x is string {
return typeof x === "string";
}
if (isString(val)) {
val.toUpperCase(); // ✅ 此分支 val 已被收窄为 string
}
```
TS 5.5+ 起,`find` / `filter` / `findIndex` 传入类型谓词时也能自动收窄结果类型。
|
|
### 箭头函数:从传统 function 到 "=>"
```typescript
// 传统函数
function add(a: number, b: number) {
return a + b;
}
// 箭头函数·表达式体(单行表达式:自动 return)
const add1 = (a: number, b: number) => a + b;
// 箭头函数·代码块体(多行:必须显式 return)
const add2 = (a: number, b: number) => {
const result = a + b;
return result;
};
```
### 语法规则速记
| 写法 | 说明 |
|------|------|
| `(a, b) => a + b` | 表达式体:自动返回表达式的值 |
| `u => u.age === 30` | 单参数可省略圆括号 |
| `() => 42` | 无参数时圆括号不能省 |
| `(a, b) => { return a + b; }` | 代码块体:必须显式 return |
| `() => ({ x: 1 })` | 返回对象必须加外层圆括号(见下方陷阱) |
### 最常见的坑:`=> { }` 是代码块不是对象
```typescript
// ❌ 错误:{} 被解析为代码块,x: 1 是"标签(label)",没有 return,返回 undefined
const fn = () => { x: 1 };
// ✅ 正确:外层圆括号强制解析为表达式 → 对象字面量
const fn2 = () => ({ x: 1 });
```
记忆口诀:**箭头函数返回对象,外层括号不能少**(详见本页"函数定义"章节的 `state: () => ({})` 解析)。
### 核心差异:没有自己的 this(词法 this)
箭头函数不创建自己的 `this`,而是捕获**定义时外层作用域**的 `this`:
```typescript
const timer = {
seconds: 0,
start() {
// ❌ 传统函数:this 指向 window(严格模式为 undefined),seconds 加不上去
setInterval(function () { this.seconds++; }, 1000);
// ✅ 箭头函数:this 继承 start() 的 this,即 timer 实例
setInterval(() => { this.seconds++; }, 1000);
}
};
```
### 适用与不适用
- ✅ 适合:数组方法回调(`map` / `filter` / `find` / `reduce` 的谓词)、`setTimeout` / `Promise` 回调、需要沿用外层 this 的场景
- ❌ 不适合:对象方法(this 不指向对象本身)、需要 `arguments` 对象的场景、构造函数(不能 new)
|
|
### 选型速查表
| 维度 | find | filter |
|------|------|--------|
| 返回值 | 第一个匹配元素 \| undefined | 所有匹配元素的新数组 T[] |
| 遍历 | 命中即停,最好 O(1) | 恒 O(n) 扫完全部 |
| 内存 | 不创建新数组 | 总是分配新数组 |
| 判空 | `!result` 即可 | `[]` 是 truthy,必须 `.length === 0` |
| 语义 | "只要第一个/唯一一个" | "要所有满足条件的" |
| TS 类型 | `T \| undefined` | `T[]` |
### 一句话结论
1. **表达能力**:filter ⊇ find(结果层面可互相模拟),直觉正确
2. **但"只要一个"的场景应使用 find**:短路更快、不建新数组、undefined 判空更直观、语义对读者和编译器更清晰
3. **按意图选择,而不是按能力选择**:要所有匹配用 filter,只要第一个/唯一一个用 find
### 关联知识
- 谓词:find/filter 的回调参数就是谓词(返回 true/false 的判断函数)
- 箭头函数:写谓词最常用的语法(单参数可省括号、表达式体自动 return)
- 源码笔记:`docs/study/ts/02-数据操作.ts`(第 6 节含可运行的对比验证代码)
|