大家好,欢迎来到今天的“TypeScript 魔法学院”!我是你们的讲师,人称“Bug 终结者”—— 咳咳,开个玩笑。今天我们要聊聊 TypeScript 中那些让代码更安全、更智能的秘密武器:类型推断、控制流分析以及声明文件。准备好了吗?我们要起飞咯!
第一章:类型推断——让编译器当你的私人助理
想象一下,你雇了个私人助理,他不仅能帮你处理杂务,还能提前预知你的需求。TypeScript 的类型推断就有点像这样,它能根据你代码的上下文自动推断出变量的类型,让你少写很多重复的代码。
1.1 基础类型推断
最简单的例子:
let message = "Hello, TypeScript!"; // TypeScript 自动推断 message 的类型为 string
// message = 123; // Error: Type 'number' is not assignable to type 'string'.
看到了吗?你没有显式地声明 message
的类型,但 TypeScript 已经知道它是 string
类型了。如果你试图给它赋一个数字,编译器就会毫不留情地报错。
再来一个例子:
let num = 10; // TypeScript 自动推断 num 的类型为 number
let sum = num + 5; // TypeScript 自动推断 sum 的类型为 number
编译器不仅能推断出 num
的类型,还能根据 num
的使用方式推断出 sum
的类型。
1.2 函数的类型推断
函数也是类型推断的重要场所。
function add(a: number, b: number) {
return a + b; // TypeScript 自动推断返回值为 number
}
const result = add(3, 5); // TypeScript 自动推断 result 的类型为 number
这里,我们显式地声明了 a
和 b
的类型,TypeScript 就能推断出 add
函数的返回类型是 number
。
如果函数没有 return
语句,或者 return
语句返回的是 undefined
,那么 TypeScript 会推断返回类型为 void
。
function logMessage(message: string): void {
console.log(message);
// 没有 return 语句,所以 TypeScript 推断返回类型为 void
}
1.3 最佳通用类型推断 (Best Common Type)
当 TypeScript 需要从多个表达式中推断出一个类型时,它会尝试找到一个“最佳通用类型”。
let arr = [1, 2, null]; // TypeScript 推断 arr 的类型为 (number | null)[]
let arr2 = [1, "hello"]; // TypeScript 推断 arr2 的类型为 (string | number)[]
在第一个例子中,数组包含了数字和 null
,所以 TypeScript 推断数组的类型是 (number | null)[]
。在第二个例子中,数组包含了数字和字符串,所以 TypeScript 推断数组的类型是 (string | number)[]
。
最佳通用类型推断会尽可能地找到一个既能包含所有元素类型,又能提供尽可能精确的类型信息的类型。
1.4 上下文类型推断
有时候,类型推断会受到上下文的影响。例如:
window.addEventListener('click', function(event) {
// TypeScript 知道 event 的类型是 MouseEvent
console.log(event.clientX, event.clientY);
});
在这里,addEventListener
的第一个参数是 'click'
,TypeScript 就知道回调函数的 event
参数应该是 MouseEvent
类型,这被称为上下文类型推断。
另一个例子:
interface Person {
name: string;
age: number;
}
const people: Person[] = [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 40 }
];
people.forEach(person => {
// TypeScript 知道 person 的类型是 Person
console.log(person.name, person.age);
});
TypeScript 根据 people
的类型 Person[]
,推断出 forEach
回调函数中的 person
参数的类型是 Person
。
1.5 类型推断的局限性
虽然类型推断很强大,但它也不是万能的。有时候,你需要显式地声明类型,以避免 TypeScript 推断出错误的类型。
例如:
let myVariable; // TypeScript 推断 myVariable 的类型为 any
myVariable = 10;
myVariable = "hello"; // 没有任何错误,因为 myVariable 的类型是 any
在这个例子中,myVariable
没有被初始化,TypeScript 只能推断出它的类型是 any
。这意味着你可以给它赋任何类型的值,而不会得到编译器的警告。这显然不是我们想要的。
因此,为了安全起见,最好在声明变量时就显式地指定类型:
let myVariable: number | string;
myVariable = 10;
myVariable = "hello";
// myVariable = true; // Error: Type 'boolean' is not assignable to type 'string | number'.
第二章:控制流分析——让编译器成为你的代码卫士
控制流分析是 TypeScript 编译器用来理解代码执行路径的技术。它可以帮助编译器更精确地推断类型,并检测出潜在的错误。
2.1 基于可辨识联合类型的控制流分析
可辨识联合类型 (Discriminated Union Types) 是一种非常有用的 TypeScript 类型,它可以让我们定义一个类型,该类型可以是多种不同类型的联合,但这些类型都有一个共同的“辨识属性”。
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
// 这里 TypeScript 知道 shape 不可能是 Circle 或 Square,所以这里应该抛出一个错误
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}
在这个例子中,Shape
是一个可辨识联合类型,它有两种可能的类型:Circle
和 Square
。这两种类型都有一个共同的辨识属性 kind
,它的值分别是 "circle"
和 "square"
。
在 getArea
函数中,我们使用 switch
语句来根据 shape.kind
的值来判断 shape
的类型。TypeScript 的控制流分析能够理解这种模式,并根据不同的 case
分支来缩小 shape
的类型范围。
在 default
分支中,我们定义了一个 _exhaustiveCheck
变量,它的类型是 never
。never
类型表示永远不会发生的值。如果 shape
的类型不是 Circle
或 Square
,那么 shape
就会被赋值给 _exhaustiveCheck
,从而导致一个编译错误。这可以帮助我们确保 switch
语句处理了所有可能的类型。
2.2 类型守卫 (Type Guards)
类型守卫是一种特殊的函数或表达式,它可以缩小变量的类型范围。TypeScript 提供了几种内置的类型守卫:
typeof
:用于判断变量的原始类型("string"
、"number"
、"boolean"
、"symbol"
、"bigint"
、"undefined"
、"object"
、"function"
)。instanceof
:用于判断变量是否是某个类的实例。in
:用于判断对象是否包含某个属性。
我们也可以自定义类型守卫函数。类型守卫函数的返回值类型必须是类型谓词 (Type Predicate),类型谓词的形式是 variable is Type
。
function isString(value: any): value is string {
return typeof value === "string";
}
function processValue(value: string | number) {
if (isString(value)) {
// TypeScript 知道 value 的类型是 string
console.log(value.toUpperCase());
} else {
// TypeScript 知道 value 的类型是 number
console.log(value * 2);
}
}
在这个例子中,isString
函数就是一个类型守卫函数。它的返回值类型是 value is string
,表示如果 isString(value)
返回 true
,那么 value
的类型就是 string
。
在 processValue
函数中,我们使用 isString
函数来判断 value
的类型。TypeScript 的控制流分析能够理解 isString
函数的作用,并根据 if
语句的条件来缩小 value
的类型范围。
2.3 真值收窄 (Truthiness Narrowing)
TypeScript 会根据表达式的真值来缩小变量的类型范围。
function greet(name?: string) {
if (name) {
// TypeScript 知道 name 的类型是 string
console.log(`Hello, ${name.toUpperCase()}!`);
} else {
// TypeScript 知道 name 的类型是 undefined
console.log("Hello, stranger!");
}
}
在这个例子中,name
的类型是 string | undefined
。在 if (name)
语句中,TypeScript 会检查 name
是否是真值。如果 name
是真值(即不是 null
、undefined
、""
、0
、NaN
或 false
),那么 TypeScript 就会认为 name
的类型是 string
。否则,TypeScript 就会认为 name
的类型是 undefined
。
2.4 赋值收窄 (Assignment Narrowing)
TypeScript 会根据变量的赋值来缩小变量的类型范围。
let value: string | number = "hello";
value = 10; // TypeScript 知道 value 的类型是 number
if (typeof value === "number") {
// TypeScript 知道 value 的类型是 number
console.log(value * 2);
}
在这个例子中,value
的初始类型是 string | number
。当我们给 value
赋值为 10
时,TypeScript 就会认为 value
的类型是 number
。
第三章:声明文件 (.d.ts)——让 TypeScript 拥抱 JavaScript
JavaScript 世界浩瀚无垠,其中包含了大量的第三方库。很多 JavaScript 库并没有提供 TypeScript 类型定义,这意味着我们在 TypeScript 代码中使用这些库时,无法获得类型检查和代码补全的支持。
为了解决这个问题,TypeScript 引入了声明文件 (.d.ts
) 的概念。声明文件是一种描述 JavaScript 代码类型的 TypeScript 文件,它不包含任何实际的 JavaScript 代码,只包含类型声明。
3.1 声明文件的作用
- 提供类型信息:声明文件可以告诉 TypeScript 编译器 JavaScript 代码的类型信息,从而使 TypeScript 代码可以使用 JavaScript 代码,并获得类型检查和代码补全的支持。
- 描述 JavaScript API:声明文件可以描述 JavaScript API 的结构和类型,从而使 TypeScript 开发者可以更容易地理解和使用 JavaScript API。
- 支持渐进式迁移:声明文件可以帮助我们将 JavaScript 代码逐渐迁移到 TypeScript 代码,而无需一次性重写所有代码。
3.2 声明文件的语法
声明文件的语法与 TypeScript 代码的语法非常相似,但有一些不同之处。
- 声明文件只包含类型声明:声明文件不包含任何实际的 JavaScript 代码,只包含类型声明,例如
interface
、type
、class
、function
、const
、let
、var
等。 - 声明文件使用
declare
关键字:在声明文件中,我们需要使用declare
关键字来声明全局变量、函数、类等。declare
关键字告诉 TypeScript 编译器,这些变量、函数、类已经在其他地方定义了,不需要在声明文件中提供实际的实现。
例如,假设我们有一个 JavaScript 库 my-library.js
,它定义了一个全局变量 myGlobalVariable
和一个函数 myGlobalFunction
:
// my-library.js
var myGlobalVariable = "Hello from JavaScript!";
function myGlobalFunction(name) {
return "Hello, " + name + "!";
}
我们可以创建一个声明文件 my-library.d.ts
来描述 my-library.js
的类型信息:
// my-library.d.ts
declare var myGlobalVariable: string;
declare function myGlobalFunction(name: string): string;
然后,我们就可以在 TypeScript 代码中使用 my-library.js
,并获得类型检查和代码补全的支持:
// my-app.ts
/// <reference path="my-library.d.ts" />
console.log(myGlobalVariable); // TypeScript 知道 myGlobalVariable 的类型是 string
console.log(myGlobalFunction("TypeScript")); // TypeScript 知道 myGlobalFunction 的参数类型是 string,返回值类型是 string
3.3 如何获取声明文件
-
Definitely Typed:Definitely Typed 是一个由社区维护的 TypeScript 声明文件仓库,它包含了大量的第三方 JavaScript 库的声明文件。我们可以使用 npm 来安装 Definitely Typed 提供的声明文件:
npm install @types/<library-name>
例如,要安装 jQuery 的声明文件,可以使用以下命令:
npm install @types/jquery
-
库自带的声明文件:有些 JavaScript 库会自带声明文件,这些声明文件通常位于库的根目录下,或者在
package.json
文件中指定。 -
自己编写声明文件:如果找不到现成的声明文件,我们可以自己编写声明文件。
3.4 声明文件的查找规则
TypeScript 编译器会按照以下规则来查找声明文件:
typeRoots
选项:如果tsconfig.json
文件中指定了typeRoots
选项,TypeScript 编译器会在typeRoots
指定的目录中查找声明文件。node_modules/@types
目录:TypeScript 编译器会在node_modules/@types
目录中查找声明文件。- 与 TypeScript 文件同名的
.d.ts
文件:如果 TypeScript 文件my-module.ts
引用了一个 JavaScript 文件my-module.js
,TypeScript 编译器会查找与my-module.ts
同名的.d.ts
文件my-module.d.ts
。
3.5 一些声明文件中的常用语法
-
模块声明 (Module Declaration):用于声明一个模块。
declare module "my-module" { export const myVariable: string; export function myFunction(name: string): string; }
-
命名空间声明 (Namespace Declaration):用于声明一个命名空间。
declare namespace MyNamespace { export const myVariable: string; export function myFunction(name: string): string; }
-
类声明 (Class Declaration):用于声明一个类。
declare class MyClass { constructor(name: string); myMethod(): string; }
-
接口声明 (Interface Declaration):用于声明一个接口。
declare interface MyInterface { name: string; age: number; }
-
类型声明 (Type Declaration):用于声明一个类型别名。
declare type MyType = string | number;
-
枚举声明 (Enum Declaration):用于声明一个枚举。
declare enum MyEnum { Value1, Value2, Value3 }
-
函数重载 (Function Overload):用于声明一个函数的多个重载版本。
declare function myFunction(name: string): string; declare function myFunction(age: number): string;
第四章:总结与进阶
今天我们一起探索了 TypeScript 的类型推断、控制流分析和声明文件。它们是 TypeScript 强大的类型系统的基石,可以帮助我们编写更安全、更可靠的代码。
- 类型推断 让我们少写很多重复的代码,让编译器当我们的私人助理。
- 控制流分析 让编译器成为我们的代码卫士,帮助我们检测出潜在的错误。
- 声明文件 让 TypeScript 拥抱 JavaScript,使我们可以使用 JavaScript 代码,并获得类型检查和代码补全的支持。
想要更上一层楼吗? 建议深入研究以下主题:
- 泛型 (Generics):编写可重用的类型代码。
- 条件类型 (Conditional Types):根据条件选择不同的类型。
- 映射类型 (Mapped Types):从现有类型创建新类型。
- 高级类型守卫 (Advanced Type Guards):编写更复杂的类型守卫函数。
希望今天的讲座对你有所帮助!下次再见!祝大家 Bug 永退!