第二周06天 Vue3 + TypeScript 实战与本周复盘

两小时学习讲义:Vue3 + TypeScript 实战与本周复盘

配套《学习计划.md》使用,本讲义把每个知识点打碎到颗粒级 ,并逐点给出类比 + 例子 + 真实开发场景 。 学习方法建议:先猜后跑。看到代码先写下类型/结果,再对照验证。 全程约 120 分钟,与学习计划的时间轴一一对应。


⏱️ 时间轴总览

时间段 环节 本讲义位置
20:00--20:10 🔁 回顾与预习 第 1 段
20:10--20:25 📚 知识块①:<script setup lang="ts"> 的泛型组件 第 2 段
20:25--20:40 📚 知识块②:ref / computed / reactive 的类型策略 第 3 段
20:40--20:50 📚 知识块③:.d.ts 全局声明 + 本周复盘 第 4 段
20:50--21:00 📝 费曼复述 第 5 段
21:00--21:20 🛠️ 练习①:TodoItem 组件(defineProps / defineEmits) 第 6 段
21:20--21:40 🛠️ 练习②③:ref 空值收窄 + .d.ts 全局声明 第 7 段
21:40--21:50 💭 思考提高题 + 本周复盘 第 8 段
21:50--22:00 ✅ 测验 第 9 段

第 1 段(10min)回顾与预习:把类型能力"上真身"

1.1 先接昨日(tsconfig 工程配置)的线

昨天你学的是工程配置strict 开关、include/exclude 范围、路径别名。今天把这些"配置出来的类型能力"真正放进一个 Vue3 项目------从'会配'到'会用'

先把本周 5 天连成一条线(这是今天第 4 段的复盘底稿):

php 复制代码
周一 类型收窄    联合类型收窄:typeof / instanceof / in / 字面量;never 穷尽检查
周二 泛型        函数/接口/类泛型;约束 extends;默认类型
周三 工具类型一  Partial / Required / Readonly / Pick / Omit / Record ...
周四 类型体操    条件类型 T extends U ? X : Y;infer 抓取
周五 工程配置    tsconfig:strict、include/exclude、路径别名
周六 实战        ★ 把上面这些全部组合进 Vue3 组件 ------ 本日

今天你只会"用已经学过的东西",但会发现:Vue3 的组件类型,就是把本周的泛型、工具类型、条件类型 + infer 全部穿起来的外套。

1.2 先凭直觉回答 4 个小问题(不用对,先写下答案)

ts 复制代码
// 问题 1:defineProps 两种写法,差别在哪?
const props1 = defineProps<{ title: string }>();        // 泛型写法
const props2 = defineProps({ title: String });          // 运行时写法
// 哪个的类型信息更完整?

// 问题 2:ref 什么时候必须显式写泛型?
const a = ref("");                                      // 推断成什么?
const b = ref<string | null>(null);                     // 为什么这里要写 <string | null>?

// 问题 3:window.__APP_VERSION__ 为什么报错?
console.log(window.__APP_VERSION__);   // TS 的报错信息大概是?

// 问题 4:defineEmits 里的这行是什么?
const emit = defineEmits<{ (e: "toggle", id: number): void }>();
// 它描述了事件的什么?

学完第 2、3、4 段再回来对照。第 1 段只做一件事:记住 Vue3 的组件类型 = 泛型 + 工具类型 + 调用签名的组合


第 2 段(15min)知识块①:<script setup lang="ts"> 的泛型组件

2.1 编译器宏:defineProps 不是普通函数

知识点(细化)

  • defineProps / defineEmits / defineExpose 都是编译器宏(compile-time macro) :不需要 import,写出来就会被 Vue 编译器在编译期替换掉
  • 它们不是普通函数------不会在运行时被调用 。所以你不能在普通 JS 环境里 import { defineProps } from 'vue' 再用(vue 包里虽然导出同名 API,但 SFC 里的用法是编译期魔法)。
  • 结论:"组件接口用宏声明,值用 ref/reactive 声明" ------ 宏管接口,ref 管数据。

类比(点菜机) :defineProps 像餐厅门口的点菜机------你"扫了一眼"(声明),机器(编译器)当场把菜单换成后厨能认的纸条(运行时 props 选项),并给你一张打印好的小票(类型)。你拿到的是小票,不是点菜机本身。

2.2 defineProps:泛型参数直接声明 props

知识点(细化)

  • 泛型写法:defineProps<{ id: number; title: string; done: boolean }>() ------ 直接传一个类型
  • 更好:先 interface Props { ... }defineProps<Props>(),接口可以复用、可以导出。
  • 优势 :模板里的 props.idprops.title 有完整类型提示;可以搭配 Pick/Omit 等工具类型派生变体(第 4 段复盘会用到)。
  • 对比普通写法 props: { title: String }:那是运行时校验 写法,类型信息弱(String 推断为 string,无字面量、无精确结构),无法用工具类型派生。

例子:两种写法对比

ts 复制代码
// 写法 A:泛型写法(类型来自独立接口)
interface Props { id: number; title: string; done: boolean; }
const props = defineProps<Props>();

// 写法 B:运行时写法(类型信息弱)
// props: { title: String }

真实场景:业务组件几乎都用写法 A------因为能复用接口、能工具类型派生、编辑器提示完整。一个典型的表单组件:

ts 复制代码
interface UserFormProps { id?: number; name: string; age?: number; }
const props = withDefaults(defineProps<UserFormProps>(), {
  id: 0,        // 默认值,类型上 id 会变成非可选
  age: 18,
});

2.3 withDefaults:泛型 + 默认值

知识点(细化)

  • withDefaults(defineProps<Props>(), { ... }) 给泛型 props 设默认值。
  • 类型上的效果:有默认值的属性,在类型层面会从"可选"变成"必有值"(编辑器会给你收窄后的类型)。

例子

ts 复制代码
interface Props { title: string; done?: boolean; }
const props = withDefaults(defineProps<Props>(), { done: false });
// 此后 props.done 的类型是 boolean(不再是 boolean | undefined)

类比(套餐默认辣度)? 表示"可以不加辣",withDefaults 就是"默认微辣"------点餐时你什么也不用说,厨师默认按微辣做,且菜单上写明了"一定是微辣,不会再问你"。

2.4 defineEmits:用"调用签名"声明事件

知识点(细化)

  • 写法:defineEmits<{ (e: "toggle", id: number): void }>() ------ 一个调用签名:描述"有一个叫 toggle 的事件,它带一个 number 载荷"。
  • 好处:emit("toggle", ...) 时编辑器检查事件名参数类型 ------写错 "togglo" 或传 id: "abc" 立刻标红。
  • 多个事件就写多个签名:{ (e: "toggle", id: number): void; (e: "remove", id: number): void }

例子

ts 复制代码
const emit = defineEmits<{
  (e: "toggle", id: number): void;
  (e: "remove", id: number): void;
}>();

function handleRemove() { emit("remove", props.id); }
// emit("remove", "abc")   ← 类型错误:id 应该是 number

类比(电梯按钮 + 楼层牌):事件名是按钮,载荷是楼层数。调用签名就是"这张楼层牌"------上面写着"去 3 楼要按 3,不能按'三'"。编辑器照着楼层牌检查你按的每个按钮。

2.5 defineExpose:显式暴露子组件方法(Vue 3.3+)

知识点(细化)

  • <script setup> 默认关闭 组件内部的一切对外暴露------父组件用 ref 拿到的子组件实例是空的。
  • defineExpose<{ reset: () => void }>()(Vue 3.3+ 支持泛型)显式声明"允许父组件调用 reset"。
  • 用途:父组件需要主动调用子组件方法时(如表单校验、清空输入框)。

例子

ts 复制代码
// 子组件
const reset = () => { inputRef.value = ""; };
defineExpose<{ reset: () => void }>({ reset });
// 注意:带泛型时也要把实现对象传进去

// 父组件
const child = ref<InstanceType<typeof Child>>();
child.value?.reset();   // 有类型提示

真实场景 :父页面"一键重置"一个复杂的筛选组件------筛选组件把 reset 通过 defineExpose 暴露出来,父组件调用。接口收窄到"只暴露该暴露的",是组件封装的基本功。


第 3 段(15min)知识块②:ref / computed / reactive 的类型策略

3.1 ref:自动推断 vs 显式泛型

知识点(细化)

  • ref泛型函数 ref<T>(value)
  • 自动推断ref(1)Ref<number>ref("")Ref<string>ref([1,2])Ref<number[]>。初始值能体现类型时,不用写。
  • 必须显式 :初始值是 null / undefined,而将来要存别的类型时------null 猜不出未来是 string 还是别的。

例子

ts 复制代码
const count = ref(1);                  // Ref<number>,自动推断 ✅
const name = ref("");                  // Ref<string>,自动推断 ✅
const input = ref<string | null>(null); // ❌ 不写会推断成 Ref<null>,用不了

类比(猜谜 vs 贴标签)ref(1) 像看到盒子上印着 1------不用问就知道是 number。ref(null) 像看到一个空盒子------光看空盒子,你猜不到将来装字符串还是装对象 ,所以你必须自己贴标签 ref<string | null>(null)

3.2 拿到 string | null 后:先收窄再用

知识点(细化)

  • ref<string | null>(null) 之后,input.value 的类型是 string | null
  • 直接 input.value.length → 报错 Object is possibly 'null'strictNullChecks 在保护你)。
  • 处理三件套:① if (val === null) return ...(收窄后继续)② val?.length(可选链,先短路再取)③ ?? 兜底。

例子

ts 复制代码
const input = ref<string | null>(null);

function getText(): string {
  const val = input.value;
  if (val === null) return "";   // 空值分支先行返回
  return val.trim();             // 收窄后 val 是 string,安全调用
}

// 偷懒写法(也要有兜底)
const len: number = input.value?.length ?? 0;

真实场景 :输入框 / 弹窗 / 下拉框"可能还没渲染出来"的值,几乎都是 ref<T | null>(null)------比如 const selectedUser = ref<User | null>(null),展示前必须先判空。判空不是麻烦,是'空值分支'被显式化了。

3.3 computed:推断 + 显式泛型

知识点(细化)

  • computed(() => 计算) 自动推断返回 ComputedRef<T>(T 由回调返回类型决定)。
  • 需要时显式:computed<number>(() => ...)------回调返回类型含糊、或想约束时用。

例子

ts 复制代码
const total = computed(() => items.value.length);      // ComputedRef<number>
const page = computed<number>(() => Number(route.query.page) || 1);  // 显式约束

类比(自动计算器):computed 像计算器------你给算式,它自动报结果的类型。显式泛型是"给计算器贴个标签":你说"我要 number",它按 number 校准。

3.4 reactive:先定义接口,再交给它

知识点(细化)

  • 复杂对象建议:先 interface 定义结构 → reactive<User>({...}) 或直接 reactive({...}) 让 TS 推断。
  • reactive 适合嵌套深的对象ref 适合单值 / 需要整体替换的值 。两者可混用(reactive 里的 ref 会被自动解包)。

例子

ts 复制代码
interface User { id: number; name: string; tags: string[] }
const state = reactive<User>({ id: 1, name: "vue", tags: [] });
state.tags.push("ts");   // 类型完整:tags: string[]

3.5 三个内置类型:Ref / ComputedRef / UnwrapRef

知识点(细化)

  • Ref<T>:ref 的返回值类型。
  • ComputedRef<T>:computed 的返回值类型。
  • UnwrapRef<T>把 ref 壳解掉拿里面的 T------写"接收 ref 或普通值的参数"时常用(Vue 内部大量用它)。

例子(透传场景)

ts 复制代码
import type { Ref, UnwrapRef } from "vue";

function setValue<T>(target: Ref<T>, v: UnwrapRef<T>) {
  target.value = v;   // v 的类型 = T 解包后的值,传普通值也符合
}

const n = ref(1);
setValue(n, 42);          // ✅ 传 number
// setValue(n, "42")      // ❌ UnwrapRef<number> 是 number

真实场景 :抽公共 hook 时,参数写成 Ref<T> | T、返回值写成 Ref<T>,类型既灵活又安全。记住一句:ref 是壳,UnwrapRef 是拆壳。

3.6 props 的 readonly 语义

知识点(细化)

  • props 是父→子的单向数据流,子组件不该改------语义上应只读。
  • TS 表达:interface Props { readonly id: number };Vue 里 defineProps 返回的 props 本身已只读(直接改会警告)。
  • readonly 是编译期约束,Vue 的 props 只读是运行时约束,双层保护

例子

ts 复制代码
interface Props { readonly id: number }
const props = defineProps<Props>();
// props.id = 2   ← 类型错误(readonly);Vue 运行期也会警告

第 4 段(10min)知识块③:.d.ts 全局声明 + 本周复盘

4.1 .d.ts:纯类型声明文件

知识点(细化)

  • .d.ts = declaration(声明)文件 :只写类型,不产生任何运行时代码
  • 用途:给"没有类型信息的 JS 库"补类型、给"项目外的全局变量"(windowimport.meta.env)补类型。
  • 后缀 d 就是 declaration------编译器知道"这里只有类型,编译后什么都不剩"。

类比(图纸 / 说明书).d.ts 是产品说明书------只有规格参数(类型),没有实物(运行时)。Vue 包能给你类型提示,靠的正是它自带的 vue/dist/vue.d.ts

4.2 declare global + export {}:扩展全局

知识点(细化)

  • 想让 window.__APP_VERSION__ 有类型:扩展 Window 接口。
  • 关键规则.d.ts 一旦有 export / import,文件就变成模块 ,里面的 interface Window 是"模块内的局部声明",不会合并到全局。
  • 所以要用 declare global { ... } 明确说"我就是要改全局命名空间",并且前面写 export {} 让文件成为模块(不写的话 declare global 也不合法)。

例子

ts 复制代码
// global.d.ts
export {};   // 让本文件成为模块
declare global {
  interface Window {
    __APP_VERSION__?: string;   // ? 表示可选:老浏览器/测试环境可能没有
  }
}
// 之后 window.__APP_VERSION__ 类型 = string | undefined,读取时判空

真实场景 :第三方 SDK 挂到 window 上(如 window._sdk)、全局配置、灰度标记------凡是"挂在全局上、项目各处的值",都用这个方法补类型。

4.3 env.d.ts:环境变量也有类型

知识点(细化)

  • Vite 项目里,import.meta.env 的类型在 env.d.ts 里扩展 ImportMetaEnv
  • VITE_ 前缀铁律 :只有 VITE_ 开头的环境变量才会被 Vite 暴露给前端代码------命名不带前缀,import.meta.env.VITE_API_BASE 拿到 undefined

例子

ts 复制代码
// env.d.ts
interface ImportMetaEnv {
  readonly VITE_API_BASE: string;   // .env 里写 VITE_API_BASE=...
  readonly VITE_MODE: "dev" | "prod";
}

4.4 本周复盘:一张表检查掌握度

复盘方法 :把本周的工具类型与手写实现逐个写出作用------写不出 = 没掌握,标记"周日补"。

① 8 个内置工具类型(每个 3 秒说出作用)

工具类型 作用一句话 底层本质
Partial<T> 所有属性变可选 映射 [K in keyof T]?: T[K]
Required<T> 所有属性变必填 映射 [K in keyof T]-?: T[K]
Readonly<T> 所有属性变只读 映射 [K in keyof T]: readonly T[K]
Pick<T, K> 挑出 K 对应的属性 映射 + 约束 K extends keyof T
Omit<T, K> 剔除 K 对应的属性 Pick<T, Exclude<keyof T, K>>
Record<K, V> 以键集合 K 构造对象 映射 [P in K]: V
Exclude<T, U> 从联合 T 剔除 U 条件类型 T extends U ? never : T
Extract<T, U> 从联合 T 提取与 U 交集 条件类型 T extends U ? T : never

② 5 个手写实现(能默写才算数)

  • MyPartial<T>{ [K in keyof T]?: T[K] }
  • MyPick<T, K extends keyof T>{ [P in K]: T[P] }
  • MyOmit<T, K>Pick<T, Exclude<keyof T, K>>
  • MyRecord<K extends keyof any, V>{ [P in K]: V }
  • MyReturnType<T>T extends (...args: any[]) => infer R ? R : never

今天练习 3 会用到"工具类型组合"(Readonly<Pick<...>>),把这张表跑一遍,本周就闭环了。


第 5 段(10min)费曼复述:讲给"零基础同学"听

5.1 复述提纲(出声讲一遍,卡壳处回去重看)

  1. defineProps 泛型写法强在哪? ------ 类型来自独立接口,编辑器提示完整,还能用工具类型派生;普通 props: { title: String } 类型信息弱。
  2. defineEmits 的调用签名是什么? ------ 描述"事件名 + 载荷类型",写错事件名或参数立刻标红。
  3. ref 什么时候必须写泛型? ------ 初始值是 null/undefined 时;否则推断成 Ref<null> 用不了。拿到 string | null 先收窄再用。
  4. computed / reactive 怎么定类型? ------ computed 自动推断 ComputedRef<T>,可显式泛型;reactive 先定义 interface 再传入。
  5. .d.ts 怎么扩展全局? ------ export {} 使文件成模块 → declare global { interface Window {...} }

5.2 自测清单(每条能写出例子才算过)

  • 能说出 defineProps / defineEmits / defineExpose 为什么"不用 import"
  • 能默写"泛型 props + 调用签名 emits"的 TodoItem 组件骨架
  • 能解释 ref<string | null>(null) 为什么必须显式、拿到后怎么收窄
  • 能写出 global.d.ts 扩展 Window 的完整代码
  • 能默写 8 个工具类型的作用 + 至少 4 个手写实现

第 6 段(20min)练习①:TodoItem 组件(code\TodoItem.vue)

目标:先自己写 ,卡住再看提示。写完用验证清单测。 📄 参考实现已放好:code\TodoItem.vue + 配套父组件 code\TodoApp.vue

6.1 分步提示(不要一上来就看)

  • 第 1 步 :建一个 TodoItem.vue<script setup lang="ts">(一定带上 lang="ts",这是 TS 生效的前提)。
  • 第 2 步 :写 interface Props { id: number; title: string; done: boolean }const props = defineProps<Props>()
  • 第 3 步 :写 const emit = defineEmits<{ (e: "toggle", id: number): void }>()
  • 第 4 步 :模板里渲染 {{ title }}<li @click="handleToggle">handleToggleemit("toggle", props.id)
  • 第 5 步 :写一个父组件 TodoApp.vue,用 v-for 渲染列表,@toggle="toggleTodo" 接收事件,形成闭环。
  • 第 6 步 :对照参考实现,重点看"模板里直接写 title/done(script setup 自动暴露 props)"和事件名是否一致。

6.2 验证清单(每一条都应通过)

ts 复制代码
// ① 模板里的 props 有类型提示:写 props.title 能补全
// ② emit("toggle", props.id) 参数类型正确;写 emit("toggl", 1) 会标红
// ③ 父组件 @toggle="toggleTodo" 收到 (e: "toggle", id: number)
// ④ 点击 <li> → 控制台能看到 id 变化 / done 切换

6.3 常见错误排查

报错/现象 原因 修法
defineProps 无类型提示 <script> 没写 lang="ts" 改成 <script setup lang="ts">
props 无法解构使用 Vue 3.4 之前解构会丢响应式 props.title;或 3.5+ 开 reactivePropsDestructure
emit("toggle") 标红 事件名与调用签名不一致 检查拼写;defineEmits 里写的就是唯一合法名
模板里 title 不显示 props 没传 / 名字不一致 父组件 :title="todo.title" 与接口字段对齐全

第 7 段(20min)练习②③:ref 空值收窄 + .d.ts 全局声明

7.1 练习②:ref<string | null> 的空值收窄(code\refNullHandling.ts)

分步提示

  • const input = ref<string | null>(null)必须显式泛型 ,否则推断成 Ref<null>)。
  • getText(): string:先 const val = input.value,再 if (val === null) return "",然后 return val.trim()
  • 故意写一个"错误版本"注释在旁边(input.value.length),理解 strictNullChecks 为什么拦它。
  • npx tsc --noEmit 验证(已备好 tsconfig.json)。

核心理解"判空不是麻烦,是空值分支被显式化了。 " 运行时不判空会 TypeError,编译期不判空会报 possibly null------类型系统把 bug 提前到写代码时。

7.2 练习③:global.d.ts + 工具类型收尾(code\global.d.ts / main.ts / toolTypes.ts)

分步提示

  • global.d.tsexport {} + declare global { interface Window { __APP_VERSION__?: string } }
  • main.tsconst v = window.__APP_VERSION__; console.log(v ?? "unknown") ------ 读到的类型是 string | undefined,用 ?? 兜底(本周收窄技能的实战应用)。
  • toolTypes.tstype ReadonlyTodo = Readonly<Pick<BaseTodo, "id" | "title">>,并写一个"给只读属性赋值会报错"的注释验证。
  • 组合逻辑自测:ReadonlyTodo 的结果类型应该是 { readonly id: number; readonly title: string }

7.3 验证清单

  • refNullHandling.ts 能通过类型检查,getText() 空值返回 ""、赋值后返回 trim 结果
  • 能说清"为什么 ref(null) 不显式写泛型就用不了"
  • global.d.ts 之后 window.__APP_VERSION__ 不再报类型错误
  • 能写出 Readonly<Pick<...>> 的组合并说出每一步在做什么

第 8 段(10min)思考提高题:先独立想,再看提示

思考题①:泛型 defineProps 比普通 props 写法强在哪?

提示

  • 类型来源不同:泛型写法类型来自独立接口,普通写法基于运行时校验对象。
  • 后果不同:泛型写法能"补全 + 精确字面量 + 工具类型派生(Pick/Omit)";普通写法只有粗略 string/number,派生不了。
  • 追问:两者能同时存在吗?→ 可以,defineProps<Props>() 里类型与运行时校验分离;要运行时校验仍可用对象 + 类型断言。

思考题②:拿到 string | null 为什么不能直接 .length?模板和逻辑里分别怎么处理?

提示

  • 类型上:strictNullChecksstring | null 调用 .length 不合法------编译器不知道此刻非空。
  • 运行时:真的为 null 时 .length 直接 TypeError 崩溃。
  • 逻辑里:if (val === null) return ...(收窄)/ val?.length ?? 0(可选链兜底)。
  • 模板里:Vue 模板自动解包 ref,但 null 仍要处理------用 v-if="val"val ?? "暂无" 这类展示兜底。

思考题③(本周复盘):列出工具类型与手写实现清单,哪个最模糊?周日怎么补?

提示

  • 对着第 4.4 的表格,逐个"作用一句话"说出来;说不出 = 模糊项。
  • 手写实现里最容易模糊的是 MyReturnType(infer 抓取)和 MyOmit(组合 Exclude)------周日对照源码重敲一遍。
  • 补法建议:不看答案,手写一遍 → 跑 tsc --noEmit 验证 → 对着官方实现找差异,而不是"再看一遍"。

第 9 段(10min)测验与收尾

9.1 测验

  • 打开 test\测验.md,限时 15 分钟完成 10 题(巩固 5 + 提升 3 + 横向 2)。
  • 做完再翻答案,把错题按"概念错误 / 实现失误 / 表达不清"分类标记,回到讲义对应章节重读。

9.2 今日能力自检(打分 1--5,低于 3 分明天补)

能力 自评
能徒手写出泛型 defineProps + 调用签名 defineEmits 的组件
能解释 ref 何时必须显式泛型、拿到 null 联合怎么收窄
能写 global.d.ts 扩展 Window / ImportMetaEnv
能默写 8 个工具类型的作用 + 至少 4 个手写实现
能说清"Vue3 组件类型 = 泛型 + 工具类型 + 调用签名的组合"

📌 今日核心速记卡(可截图/摘抄带走)

scss 复制代码
编译器宏:defineProps / defineEmits / defineExpose 不用 import,编译期被替换
  defineProps<Props>()       泛型写法:类型来自接口,可工具类型派生
  withDefaults(props, {...}) 设默认值,类型上可选→必有值
  defineEmits<{ (e: "toggle", id: number): void }>()   调用签名描述事件+载荷
  defineExpose<{ reset: () => void }>()   Vue 3.3+,暴露子组件方法

ref 类型策略:
  ref(1) 自动推断 Ref<number>;初始 null/undefined 必须显式 ref<T | null>(null)
  拿到 string | null → 先收窄:if (val === null) return;或 val?.length ?? 0
  computed(() => ...) → ComputedRef<T>;可显式 computed<number>(...)
  reactive<User>({...}) 复杂对象先 interface 再传入
  Ref<T> 是壳,UnwrapRef<T> 拆壳(透传参数时用)

.d.ts 全局声明:
  export {}(成模块)→ declare global { interface Window { __APP_VERSION__?: string } }
  env.d.ts 里扩展 ImportMetaEnv;环境变量必须 VITE_ 前缀才能暴露

本周 8 工具类型:Partial(全可选) Required(全必填) Readonly(全只读)
  Pick(挑) Omit(剔) Record(键集构造) Exclude(联合剔除) Extract(联合提取)
手写 5:MyPartial MyPick MyOmit MyRecord MyReturnType(能默写 = 过关)

❌ 常见误区清单(学完自查,哪个说过/想过就划掉)

  • "defineProps 是普通函数,要 import" ------ ❌ 编译器宏,不用 import,编译期替换
  • "props: { title: String } 和泛型写法类型信息一样" ------ ❌ 泛型写法类型更完整、可派生
  • "ref(null) 不写泛型也能当字符串用" ------ ❌ 会推断成 Ref<null>,先显式再收窄
  • "string | null 可以直接 .length,反正运行时不一定空" ------ ❌ 编译期报 possibly null,运行时可能 TypeError
  • "props 可以在子组件里改" ------ ❌ 单向数据流,TS readonly + Vue 运行时双层禁止
  • ".d.ts 文件会生成运行时代码" ------ ❌ 纯声明,编译后什么都不剩
  • "declare global 不用 export {} 也能用" ------ ❌ 文件是模块时,全局扩展必须 export {} + declare global
  • "环境变量不写 VITE_ 前缀也能读到" ------ ❌ 只有 VITE_ 开头的才会暴露给前端
  • "defineProps 解构出来直接是响应式的" ------ ❌ Vue 3.4 之前解构丢响应式,用 props.xxx
相关推荐
heyCHEEMS1 小时前
切页回来组件消失了?一个浏览器渲染机制引起的容器高度坍塌 bug
前端·浏览器
iaku1 小时前
Prompt 不是玄学:写给前端的 Prompt 工程指南
前端·人工智能
爱丶不疚1 小时前
在 dsh 仓库里扒到的宝藏工作流:详解 .agents/notes 决策沉淀系统
前端·agent·vibecoding
喜欢睡觉1 小时前
从"送花"讲懂 JavaScript:对象、数据类型与代理模式
前端
渣波1 小时前
NestJS 企业级后端架构实战:从核心代码到工程化思维的深度重构
前端·typescript·nestjs
BreezeJiang1 小时前
别再背工厂模式了:NestJS 第一行代码就是它的工业级落地
前端·javascript
光影少年1 小时前
RN 常见性能问题:JS卡顿、UI卡顿、桥接通信耗时
前端·react native·react.js
liuxiaocheng1 小时前
文本生成的进阶:generateText / streamText 里迟早会撞上的东西
前端·后端·ai编程
渣波1 小时前
深度解析工厂模式:从蜜雪冰城到 NestFactory,彻底搞懂“创建与使用分离”
前端·javascript