类型系统基础(Type System Basics)
核心问题解析
问题一:为什么写代码时能提前发现错误?
在纯 JavaScript 中,很多错误要到运行时才会暴露:
js
function add(a, b) {
return a + b;
}
add("1", "2"); // "12" —— 是你想要的吗?而 TypeScript 在 编写阶段 就能提示风险:
ts
function add(a: number, b: number): number {
return a + b;
}
add("1", "2"); // ❌ 编译报错:类型 'string' 不能赋给 'number'答案:TypeScript 是一门静态类型语言,它在代码编译阶段进行类型检查,无需执行即可发现类型不匹配的问题。
问题二:any 到底能不能用?
初学者常这样“绕过”类型检查:
ts
let value: any = "hello";
value = 123;
value.push(4); // ❌ 运行时报错,但 TS 不报!虽然 any 让你“自由”,但也意味着:
- 放弃了类型检查
- 失去了编辑器智能提示
- 增加了维护成本
建议使用场景:
- 迁移旧
JS项目时临时使用 - 确实无法确定类型的动态值(应尽快收敛)
避免滥用:any 是类型安全的“后门”,开多了等于没装防盗系统。
问题三:变量还没赋值,怎么就有类型了?
看这个例子:
ts
let age: number;
age = "30"; // ❌ 报错!虽然还没赋值,但类型已定即使 age 尚未被赋值,TS 也根据类型注解知道它将来只能是 number。
再看类型推导:
ts
let name = "Alice"; // TS 自动推导为 string
name = 123; // ❌ 报错结论:TS 要么通过类型注解明确指定,要么通过初始值自动推导,确保每个变量都有确定的类型。
学习目标详解
目标一:理解静态类型检查机制
- 静态类型:类型在编译时确定,而非运行时。
- 检查时机:你在 VSCode 里看到的红色波浪线,就是 TSC(TypeScript Compiler)在实时检查。
- 不改变运行逻辑:TS 编译后会擦除类型,生成纯 JS,所以最终运行的还是 JS 引擎。
目标二:掌握原始类型与字面量类型
| 类型 | 说明 | 示例 |
|---|---|---|
string | 字符串 | "hello" |
number | 数字(无 int/float 区分) | 42, 3.14 |
boolean | 布尔值 | true, false |
null | 空值 | null |
undefined | 未定义 | undefined |
void | 无返回值 | function log(): void { } |
never | 永不返回 | function error(): never { throw new Error() } |
字面量类型:更精确的类型,限定具体值。
ts
let direction: "left" | "right" | "up" | "down" = "left";
direction = "forward"; // ❌ 报错字面量类型 + 联合类型 = 枚举替代方案
目标三:区分类型注解与类型推导
| 方式 | 语法 | 场景 |
|---|---|---|
| 类型注解 | 显式声明 | 参数、返回值、未初始化变量 |
| 类型推导 | 自动判断 | 有初始值的变量、常量 |
ts
// 显式注解
let count: number;
let greet: (name: string) => void;
// 类型推导
const PI = 3.14; // 推导为 number
const isActive = true; // 推导为 boolean
const list = [1,2,3]; // 推导为 number[]规则:一旦推导完成,就不能违背。
ts
const user = { name: "Bob" };
user.age = 30; // ❌ 报错:Property 'age' does not exist解决:使用接口或类型扩展。
目标四:理解 unknown 与 any 的区别
| 类型 | 可读 | 可写 | 可调方法 | 安全性 |
|---|---|---|---|---|
| any | ✅ | ✅ | ✅ | ❌低 |
| unknown | ✅(需校验) | ✅ | ✅(需校验) | ✅ 高 |
ts
let anything: any = "test";
anything.call(); // ✅ 允许(可能运行时报错)
anything.toFixed(); // ✅ 允许
let unknownVal: unknown = "test";
unknownVal(); // ❌ 报错
unknownVal.toFixed(); // ❌ 报错
// 必须先做类型收窄
if (typeof unknownVal === "number") {
unknownVal.toFixed(); // ✅ 此时 OK
}最佳实践:
- 用
unknown替代any接收不可信输入(如 API 返回值) - 使用类型守卫进行安全转换
总结:本节核心要点
| 概念 | 关键点 |
|---|---|
| 静态检查 | 错误前置,提升开发体验 |
| 类型注解 vs 推导 | 明确优先,推导辅助 |
| any | 尽量不用,用则速改 |
| unknown | 安全接收任意值的首选 |
| 字面量类型 | 提升类型精度,减少非法值 |
练习题
练习题 1:类型注解纠错
题目:以下代码存在类型错误,请指出错误原因并修正。
ts
let age: number;
age = "25";问题:
- 哪里出错了?为什么?
- 如何修正才能让代码通过编译?
练习题 2:使用字面量类型限制取值
题目:定义一个变量 method,它只能是 "GET"、"POST"、"PUT" 或 "DELETE" 中的一个。
ts
let method: ____________ = "GET";要求:
- 补全类型注解。
- 写一行代码验证类型安全(例如尝试赋值 "PATCH",应报错)。
练习题 3:分析类型推导结果
题目:观察以下变量声明,写出 TypeScript 推导出的类型,并说明是否可以添加新属性。
ts
const user = {
name: "Alice",
age: 30
};
user.email = "alice@example.com"; // 是否允许?为什么?问题:
- user 的推导类型是什么?
- 能否添加 email 属性?如果不能,如何修改声明使其支持?
练习题 4:any vs unknown 安全性对比
题目:以下两段代码,哪一段更安全?为什么?并改写不安全的代码,使其类型安全。
ts
// 片段 A
function logLengthA(input: any) {
console.log(input.length);
}
// 片段 B
function logLengthB(input: unknown) {
console.log(input.length); // 报错?
}要求:
- 释 A 和 B 的行为差异。
- 修改
logLengthB,使其在安全的前提下访问length属性。
综合应用:函数参数类型设计
题目:实现一个函数 greet,满足以下要求:
- 接收一个参数 person,该参数必须包含 name(字符串)和 age(可选)。
- 返回值类型为字符串,格式如 "Hello, I'm Alice, 25 years old."。
- 如果没有 age,则不显示年龄部分。
ts
function greet(person: ____________) {
// 请补全类型并实现函数
}要求:
- 使用接口或类型别名定义 person 的结构。
- 利用可选属性处理 age。
- 验证以下调用是否合法:
ts
greet({ name: "Bob" });
greet({ name: "Charlie", age: 30 });
greet({ name: "David", email: "d@d.com" }); // 应该报错吗?