两小时学习讲义: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.id、props.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 库"补类型、给"项目外的全局变量"(
window、import.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 复述提纲(出声讲一遍,卡壳处回去重看)
- defineProps 泛型写法强在哪? ------ 类型来自独立接口,编辑器提示完整,还能用工具类型派生;普通
props: { title: String }类型信息弱。 - defineEmits 的调用签名是什么? ------ 描述"事件名 + 载荷类型",写错事件名或参数立刻标红。
- ref 什么时候必须写泛型? ------ 初始值是 null/undefined 时;否则推断成
Ref<null>用不了。拿到string | null先收窄再用。 - computed / reactive 怎么定类型? ------ computed 自动推断
ComputedRef<T>,可显式泛型;reactive 先定义 interface 再传入。 - .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">,handleToggle里emit("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.ts:export {}+declare global { interface Window { __APP_VERSION__?: string } }。main.ts:const v = window.__APP_VERSION__; console.log(v ?? "unknown")------ 读到的类型是string | undefined,用??兜底(本周收窄技能的实战应用)。toolTypes.ts:type 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?模板和逻辑里分别怎么处理?
提示
- 类型上:
strictNullChecks下string | 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