类型推断与兼容性(Type Inference & Compatibility)
核心问题解析
问题一:为什么函数参数少一个也能通过?
看这个例子
ts
type GetUserName = (user: { name: string }) => string;
const getUser = (user: { name: string; age: number }) => user.name;
const fn: GetUserName = getUser; // ✅ 竟然通过了?看起来 getUser 接收的参数“更多”,按理说不兼容,但 TS 允许。
答案:TypeScript 使用结构化类型(Structural Typing),只要 getUser 的参数包含 GetUserName 所需的结构(即 name: string),就认为它是兼容的。
类型兼容性是“有没有我需要的”,而不是“是否完全一样”。
问题二:对象多了属性为什么报错?
ts
const user = { name: "Alice", age: 30 };
const input: { name: string } = user; // ✅ OK
const badInput: { name: string } = { name: "Bob", email: "b@b.com" }; // ❌ 报错?奇怪,user 多属性可以,直接对象字面量却报错?
答案:这是 TS 的字面量类型特殊规则——超额属性检查(Excess Property Checking)。
- 从变量赋值:只看结构,多属性无妨(结构性兼容)
- 直接用对象字面量赋值:TS 会严格检查是否有“多余”属性,防止拼写错误
修正方式:
ts
const temp = { name: "Bob", email: "b@b.com" };
const badInput: { name: string } = temp; // ✅ OK问题三:string 能赋给 number 吗?
ts
let num: number = 10;
let str: string = "hello";
num = str; // ❌ 报错:不能将 string 赋给 number不能。TypeScript 的类型系统是安全的,基本类型之间不可互换,即使 JS 运行时可能“自动转换”。
这正是 TS 的价值:阻止你把 "5" 当 5 用的隐式错误。
学习目标详解
目标一:理解结构化类型(Structural Typing)而非名义类型
| 类型系统 | 特点 | 示例语言 |
|---|---|---|
| 结构化类型 | 看“长什么样” | TypeScript |
| 名义类型 | 看“叫什么名” | Java, C# |
ts
interface Point { x: number; y: number }
class Vector { x: number; y: number; z?: number }
const p: Point = new Vector(); // ✅ 只要结构匹配,即使不同名也兼容原则:TS 不关心“你是谁”,只关心“你有什么”。
目标二:掌握赋值兼容性规则(协变、逆变)
TS 中的兼容性是单向的:A = B 是否成立,取决于结构是否满足。
1. 协变(Covariance)——属性类型协变
ts
let source: { name: 'string', info: { age: number } };
let target: { name: string, info: { age: number, email: string } };
target = source; // ❌ 报错!info 缺少 email
source = target; // ✅ OK!target 更“宽”属性类型必须是目标类型的超集才能赋值。
2. 逆变(Contravariance)——函数参数逆变
ts
type Fn = (input: { name: string }) => void;
const f1: Fn = (user: { name: string; age: number }) => { }; // ✅ OK
const f2: Fn = (name: string) => { }; // ❌ 报错!参数结构不够函数参数是逆变的:
目标三:理解上下文类型与最佳通用类型
上下文类型(Contextual Typing)
TS 会根据“你在哪”推断类型:
ts
window.addEventListener('click', (e) => {
console.log(e.button); // ✅ e 被推导为 MouseEvent
});虽然没写 e: MouseEvent,但 TS 根据 click 事件的定义自动推导。
最佳通用类型(Best Common Type)
ts
let items = [1, 2, false]; // 推导为 (number | boolean)[]TS 会找一个能容纳所有值的“最小公共类型”。
可用 as const 强制更精确推导:[1, 2, false] as const → [number, number, boolean]
目标四:区分“可分配性”与“可扩展性”
| 概念 | 含义 | 示例 |
|---|---|---|
| 可分配性(Assignability) | A = B 是否合法 | {x:1,y:2} → {x:number} ✅ |
| 可扩展性(Extensibility) | 对象能否添加新属性 | const o = {x:1}; o.y = 2; ❌(若推导为 {x: number}) |
ts
const user = { name: "Alice" }; // 推导为 { name: string }
user.age = 30; // ❌ 报错!类型不可扩展
// 修正:显式声明可扩展
const user2: { name: string; [key: string]: any } = { name: "Alice" };
user2.age = 30; // ✅ OK可分配性是“能不能接住”,可扩展性是“能不能往里加”。
总结:本节核心要点
| 概念 | 关键点 |
|---|---|
| 结构化类型 | “有这个结构”即可,不看名字 |
| 超额属性检查 | 字面量直接赋值时更严格 |
| 函数参数逆变 | 参数越“宽”,函数越“通用” |
| 上下文类型 | 根据使用位置自动推导 |
| 可分配 vs 可扩展 | 赋值安全 ≠ 对象可变 |
练习题
练习题 1:判断类型兼容性
以下赋值是否合法?为什么?
ts
interface Logger { log(message: string): void }
class ConsoleLogger {
log(msg: string) { console.log(msg); }
debug(s: string) { } // 多一个方法
}
const logger: Logger = new ConsoleLogger(); // ✅ 还是 ❌?练习题 2:超额属性检查
以下代码报错,请解释原因,并提供 两种 修复方式。
ts
function renderUser(user: { name: string }) {
console.log(user.name);
}
renderUser({ name: "Tom", age: 25 }); // ❌ 报错?练习题 3:函数参数逆变
分析以下代码,哪一行报错?为什么?
ts
type Handler = (input: { id: string }) => void;
const h1: Handler = (u: { id: string; role: string }) => { }; // ✅ or ❌?
const h2: Handler = (id: string) => { }; // ✅ or ❌?练习题 4:上下文类型推导
写出以下变量的推导类型:
ts
// 1.
const numbers = [1, 2, 3, null]; // 类型是?
// 2.
document.addEventListener('keydown', (e) => {
console.log(e.key); // e 是什么类型?
});
// 3.
const data = [true, 100, "hello"]; // 类型是?练习题 5:可分配性 vs 可扩展性
以下代码哪些行会报错?如何修正?
ts
const config = { apiUrl: "https://api.com" };
config.version = "1.0"; // 报错?
let settings: { apiUrl: string } = config;
settings.timeout = 5000; // 报错?