Skip to content

类型推断与兼容性(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; // 报错?