核心内容:对于Typescript的类型系统、类型推断、类型兼容的理解
=========
1. Typescript和Javascript的对比
-
javascript是动态类型语言,运行的时候才知道具体的类型是什么,导致的问题
- 书写的时候无法确定类型导致容易出错,特别是在大型项目中。
- 运行的时候会影响性能,JavaScript引擎的优化已经大大减轻了这个问题。
- 一些解决方案:JSDoc注释、使用静态类型检查工具(如Flow)等。
-
Typescript提供了静态类型检查。它在编译时进行类型检查。
- TypeScript最终会编译成JavaScript,因此它的类型信息在运行时是不存在的。
- TypeScript提供了渐进式类型系统,允许开发者逐步添加类型注解。
2. Typescript的3个核心概念
-
类型系统:
- 类型系统包括基本类型(如
number、string)、复杂类型(如interface、class、enum)、泛型、联合类型和交叉类型等。
- 类型系统包括基本类型(如
-
类型推断:
- TypeScript 具有强大的类型推断能力,可以在没有显式类型注释的情况下推断变量的类型。这减少了开发者的负担,同时仍然提供类型安全性。
-
类型兼容性:
- TypeScript 使用结构化类型系统(或称为“鸭子类型”),这意味着类型的兼容性是基于它们的结构而不是显式声明。这使得不同类型之间的交互更加灵活。
3. 通用语言的类型推断
-
语法分析(Parsing):
- 词法分析(Lexical Analysis):将源代码分解成标记(tokens)。
- 语法分析(Syntax Analysis):根据语言的语法规则将标记序列转换为抽象语法树(AST)。
-
符号表管理(Symbol Table Management):
- 在语法分析过程中,编译器会构建符号表,用于存储变量、函数、类等的声明信息,包括它们的名称、作用域和类型信息。
- 符号表是类型推断的基础数据结构,因为它提供了关于标识符的所有已知信息。
-
类型检查和推断(Type Checking and Inference):
- 约束生成(Constraint Generation):编译器通过遍历 AST,生成类型约束。这些约束描述了程序中各个部分的类型关系。例如,如果变量 ( x ) 被赋值为一个整数,那么我们生成一个约束 ( \text{Type}(x) = \text{int} )。
- 约束求解(Constraint Solving):编译器使用特定算法解决这些约束,推断出未知类型。常用的算法包括:
- 合一算法(Unification Algorithm):用于在逻辑编程和类型推断中解决类型方程。这是一个递归算法,尝试通过合并类型变量来解决类型方程。
- 类型合并(Type Merging):在处理联合类型或多态类型时,编译器需要合并多种可能的类型以找到一个“最佳通用类型”。
- 控制流分析(Control Flow Analysis):通过分析程序的控制流图(CFG),编译器能够推断出变量在不同执行路径上的类型。这种分析对于处理条件语句和循环特别重要。
-
类型推断策略:
- 局部推断(Local Inference):在一个局部上下文中推断类型,通常针对函数体或单个表达式。
- 全局推断(Global Inference):通过分析整个程序来推断类型,适用于更复杂的类型关系和依赖。
-
优化和错误报告:
- 在类型推断过程中,编译器还可能进行优化,例如消除冗余类型检查。
- 如果类型推断失败,编译器会生成有意义的错误消息,帮助开发者定位和修复类型错误。
4. Typescript类型推断
- 生成抽象语法树(AST) :
- 当 TypeScript 编译器(
tsc)处理 TypeScript 源代码时,它首先进行词法分析和语法分析,将代码解析为抽象语法树(AST)。AST 是源代码的结构化表示,包含了所有语法元素及其关系。
- 当 TypeScript 编译器(
- 构建符号表:
- 在生成 AST 的同时,TypeScript 编译器构建符号表。符号表记录了每个标识符(如变量、函数、类等)的名称、作用域和声明位置等信息。
- 符号表是类型推断的基础数据结构,因为它提供了关于标识符的所有已知信息。
- 类型推断:
- TypeScript 使用上下文信息和类型推断规则来推断变量和表达式的类型。推断规则包括:
- 直接推断:根据赋值语句或函数返回值推断类型。
- 上下文推断:根据函数调用或对象属性访问的上下文推断类型。
- 控制流分析:通过分析程序的控制流,推断变量在不同路径上的可能类型。
- TypeScript 使用上下文信息和类型推断规则来推断变量和表达式的类型。推断规则包括:
- 类型检查:
- 在推断出类型后,TypeScript 会进行类型检查,确保代码中的类型使用是合法的。这包括检查:
- 类型兼容性:确保变量、函数参数和返回值的类型匹配。
- 类型安全性:防止类型错误,如访问未定义的属性或调用不存在的方法。
- 在推断出类型后,TypeScript 会进行类型检查,确保代码中的类型使用是合法的。这包括检查:
- 错误报告:
- 如果在类型检查过程中发现类型不匹配或其他类型错误,TypeScript 编译器会生成详细的错误信息,帮助开发者理解和修复问题。
5. Typescript类型兼容
-
Typescript类型兼容采用类型检查的部分
-
类型兼容有两种
-
结构化类型系统(structural typing)
- 类型的兼容性是基于它们的结构而非显式声明
- 优点:灵活性高、简化代码、更容易进行代码重构
- 缺点:类型安全性较低、难以表达意图、可能导致难以理解的错误
-
名义类型系统(nominal typing)
- Java是名义类型系统的经典例子。类和接口之间的关系必须通过显式的继承或实现来声明。
- 优点:清晰的意图表达、类型安全性高、更容易进行静态分析
- 缺点:灵活性较低 重构复杂 增加代码冗余
-
-
TypeScript类型兼容性:如果一个类型的所有属性及其类型都可以在另一个类型中找到,则这两个类型是兼容的。
interface Point2D {
x: number;
y: number;
}
interface Point3D {
x: number;
y: number;
z: number;
}
let point2D: Point2D = { x: 1, y: 2 };
let point3D: Point3D = { x: 1, y: 2, z: 3 };
// 这是允许的,因为Point3D包含Point2D的所有属性
point2D = point3D;
// 这是不允许的,因为Point2D缺少z属性
// point3D = point2D; // 错误
- 子类型关系:如果类型 A 的所有属性在类型 B 中都存在,并且这些属性的类型也兼容,则类型 A 是类型 B 的子类型。换句话说,类型 A 可以赋值给类型 B 的变量。
type Animal = {
name: string;
}
type Dog = {
name: string;
breed: string;
}
let animal: Animal;
let dog: Dog = { name: "Buddy", breed: "Labrador" };
// Dog 是 Animal 的子类型,因为 Dog 包含 Animal 的所有属性
animal = dog; // 这是允许的
// 反过来不行,因为 Animal 缺少 breed 属性
// dog = animal; // 这会报错
- 宽松的类型匹配:TypeScript 允许多余的属性存在于一个类型中,只要目标类型的所有必需属性都存在且类型兼容。例如,一个对象可以有比接口要求更多的属性,只要它至少具有接口中定义的属性。
interface Person {
name: string;
age: number;
}
let person: Person = {
name: "Alice",
age: 30,
occupation: "Engineer" // 额外的属性
}; // 这是允许的
- 函数类型兼容性:函数的类型兼容性不仅仅考虑参数和返回值的类型,还考虑参数的数量。TypeScript 允许函数有多余的参数,只要调用时所需的参数都存在并且类型兼容。
type SimpleFunction = (x: number) => void;
const detailedFunction = (x: number, y: string) => {
console.log(x, y);
};
let simpleFunc: SimpleFunction = detailedFunction; // 这是允许的
simpleFunc(10); // 正常工作,y 参数被忽略
- 泛型兼容性:泛型类型的兼容性基于具体类型实例化后的结构。例如,Array 类型的兼容性取决于 T 的具体类型。
interface Box<T> {
value: T;
}
let numberBox: Box<number> = { value: 10 };
let anyBox: Box<any> = numberBox; // 这是允许的
let stringBox: Box<string> = numberBox; // 错误:类型不兼容
- 协变与逆变:
-
协变(Covariant):
- 简单来说,协变意味着"跟着一起变"。
- 如果 A 是 B 的子类型,那么
Container<A>也是Container<B>的子类型。 - 在TypeScript中,返回值类型是协变的。
-
逆变(Contravariant):
- 逆变意味着"反方向变"。
- 如果 A 是 B 的子类型,那么
Container<B>反而是Container<A>的子类型。 - 在TypeScript中,参数类型是逆变的。
-
为什么这个设计:为了类型安全
- 目的:复杂的类型关系中,可以安全地替换函数,同时保持最大的灵活性。它允许我们在需要更具体类型的地方使用更一般的函数,反之亦然。
- 返回值类型的协变性允许我们返回更具体(更专门化)的类型。这是安全的,因为接收返回值的代码总是可以安全地处理更具体的类型。
class Animal { eat() {} } class Dog extends Animal { bark() {} } type AnimalGetter = () => Animal; type DogGetter = () => Dog; let getAnimal: AnimalGetter; let getDog: DogGetter = () => new Dog(); // 这是安全的 getAnimal = getDog;- 参数类型的逆变性允许我们传入更一般(更抽象)的类型。这看起来可能有点反直觉,但实际上是为了保证类型安全。
type AnimalConsumer = (a: Animal) => void; type DogConsumer = (d: Dog) => void; let feedAnimal: AnimalConsumer = (a) => { a.eat(); }; let feedDog: DogConsumer; // 这是安全的 feedDog = feedAnimal;
-