前言
2012年10月微软发布了Typescript第一个开源的版本,那时候作为一个前端开发者可能只是知道它,随着近几年前端工程化的发展,加上Vue这类框架也开始全面拥抱Typescript,才让越来越多的前端开发者开始使用它。但是反观使用了Typescript的开发者,对这个语言知之甚少,一部分还停留在使用interface
和type
定义类型的阶段。
类型兼容性
Typescript类型系统是按照结构子类型设计的,它基于类型的成员或属性来决定类型之间的关系,而不是基于类型的显式声明或继承关系。
对象类型兼容性(协变)
typescript
interface Animal {
name: string;
}
interface Dog {
name: string;
color: string;
}
let animal: Animal = {
name: 'animal',
}
let dog: Dog = {
name: 'dog',
color: 'white',
}
animal = dog; // Ok 可以赋值
dog = animal; // Error 不可以赋值
在这个例子中,Dog
同时包含name
和color
属性,包含了Animal
所有属性,所以在typescript
中Dog
被认为是Animal
的子类型,在赋值的时候dog
可以赋值给animal
,但是反过来却不行,这主要是typescript出于类型安全考虑,比如dog
赋值给animal
,在后面使用animal
的时候能够保证所有的成员都是可用的,但是反过来赋值的话,却不能保证dog
的每个成员都是可用的。
函数参数兼容性(逆变)
typescript
interface Animal {
name: string;
}
interface Dog {
name: string;
color: string;
}
type AnimalHandler = (animal: Animal) => void;
type DogHandler = (dog: Dog) => void;
let handleDog: DogHandler = (dog) => {};
let handleAnimal: AnimalHandler = (animal) => {};
handleDog = handleAnimal; // Ok 可以赋值
handleAnimal = handleDog; // Error 不可以赋值
这个例子可以用上面的方法来分析,把handleAnimal
赋值给handleDog
可以,后面调用handleDog
的时候实际调用的是handleAnimal
函数,传入Dog
类型的参数是可以保证正常工作的,因为Dog
包含了handleAnimal
需要的Animal
参数的所有成员。反过来把handleDog
赋值给handleAnimal
就是不可以的
一些操作符
is
is
是 TypeScript 中的一种类型谓词(Type Predicate),用于自定义类型保护(Custom Type Guard)。它允许你编写函数来显式地告诉 TypeScript 编译器某个值的具体类型。
- 基本语法
typescript
function isType(value: any): value is TargetType {
// 返回布尔值
// 如果返回 true,TypeScript 会认为 value 是 TargetType 类型
}
- 举例
比如Vue3的isRef
函数,用来判断一个值是不是ref
对象
typescript
// 类型定义
function isRef<T>(r: Ref<T> | unknown): r is Ref<T>
// 使用
let foo: unknown
if (isRef(foo)) {
// foo的类型被收缩为 Ref<unknown>
foo.value
}
typeof
typeof
在 TypeScript 中有两种主要用法,分别用于值上下文 和类型上下文
- 值上下文中的 typeof (JavaScript 行为)
在表达式/值层面使用,与 JavaScript 相同,返回值的类型字符串:
typescript
const str = "hello";
const num = 42;
console.log(typeof str); // "string"
console.log(typeof num); // "number"
function greet() {}
console.log(typeof greet); // "function"
- 类型上下文中的
typeof
(TypeScript 扩展)
在类型位置使用,可以获取变量或属性的类型:
typescript
const person = {
name: "Alice",
age: 30
};
// 获取 person 的类型
type Person = typeof person;
/* 等价于:
type Person = {
name: string;
age: number;
}
*/
// 获取特定属性的类型
type Age = typeof person["age"]; // number
keyof
keyof
是 TypeScript 中的一个类型操作符,用于获取一个类型的所有属性名(键)组成的联合类型。
- 用法
typescript
interface Person {
name: string;
age: number;
address: string;
}
type PersonKeys = keyof Person;
// 等价于: "name" | "age" | "address"
in
in
在 TypeScript 中有两种主要用法,分别用于值上下文 和类型上下文
- 运行时上下文(JavaScript 行为)
在 JavaScript/TypeScript 运行时逻辑中,in
用于检查对象是否包含特定属性:
typescript
const user = { name: "Alice", age: 30 };
// 检查属性是否存在
console.log("name" in user); // true
console.log("email" in user); // false
// 也可用于检查原型链上的属性
console.log("toString" in user); // true
- 类型上下文(TypeScript 类型系统)
在类型系统中,in
主要有两种用途:
(1) 类型保护
缩小联合类型的范围:
typescript
interface Bird {
fly(): void;
}
interface Fish {
swim(): void;
}
function move(pet: Bird | Fish) {
if ("fly" in pet) {
pet.fly(); // 这里 pet 被识别为 Bird
} else {
pet.swim(); // 这里 pet 被识别为 Fish
}
}
(2) 映射类型
用于遍历联合类型的每个成员:
typescript
type FeatureFlags = {
darkMode: boolean;
newProfile: boolean;
};
// 将每个属性变为可选
type Partial<T> = {
[P in keyof T]?: T[P];
};
type Options = Partial<FeatureFlags>;
/* 等价于:
type Options = {
darkMode?: boolean;
newProfile?: boolean;
};
*/
extends
extends
是 TypeScript 中一个多功能的关键字,在不同的上下文中有不同的作用。
- 接口/类继承 (面向对象继承)
用于接口或类之间的继承关系:
typescript
// 接口继承
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
// 类继承
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
}
class Dog extends Animal {
breed: string;
constructor(name: string, breed: string) {
super(name);
this.breed = breed;
}
}
- 类型约束
限制泛型参数的类型范围:
typescript
// 要求 T 必须具有 length 属性
function logLength<T extends { length: number }>(arg: T): void {
console.log(arg.length);
}
logLength("hello"); // 5
logLength([1, 2, 3]); // 3
logLength(42); // 错误,数字没有length属性
- 类型推断 与
infer
关键字配合使用,在条件类型中提取类型信息
typescript
function move(distance: number) {
return distance;
}
// typescript内置工具ReturnType
// 获取函数的返回值类型
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
type MoveReturn = ReturnType<typeof move>;
/* 等价于
type MoveReturn = number
*/
类型查找规则
模块类型查找规则
- 非相对导入(如 import {cloneDeep} from 'lodash-es')
- 查找
node_modules/@types/lodash-es/index.d.ts
- 根据模块自身的
package.json
中定义的types
字段,查找指定的文件 - 查找
node_modules/lodash-es/index.d.ts
- 查找
typeRoots
指定的目录(如果配置了)
- 查找
- 相对导入(如
import { Foo } from './foo'
)- 查找同目录下的文件,
./foo.ts
、./foo.tsx
、./foo.d.ts
、./foo/index.ts
、./foo/index.d.ts
- 查找同目录下的文件,
全局类型查找规则
-
Typescript会根据指定的target加载内置的类型声明文件
target: "ES5"
→ 默认包含DOM
,ES5
,ScriptHost
target: "ES6"
→ 默认包含DOM
,ES6
,DOM.Iterable
,ScriptHost
这些内置声明文件位于 TypeScript 语言安装目录的lib
文件夹内,数量大概有几十个,下面是其中一些主要文件
- lib.d.ts
- lib.dom.d.ts
- lib.es2015.d.ts
- lib.es2016.d.ts
- lib.es2017.d.ts
- lib.es2018.d.ts
- lib.es2019.d.ts
- lib.es2020.d.ts
- lib.es5.d.ts
- lib.es6.d.ts
-
默认会包含所有
.ts
和.d.ts
、.tsx
文件
如果没有指定files
和include
,编译器默认包含当前目录和子目录下所有的TypeScript文件(.ts
,.d.ts
和.tsx
),这些文件中定义的全局类型会在全局可用
typescript
// global.d.ts
// 因为这个文件没有明确的export或import,所以文件定义就是全局类型
interface Window {
a: number;
b: number;
c: number;
}
declare let d: number;
// 如果有了export导出,可以使用global声明全局类型, 下面写法和上面写法是等价的
declare global {
interface Window {
a: number;
b: number;
c: number;
}
let d: number;
}
export {};
类型扩展
有时候开发过程中,不免会遇到一些情况需要扩展现在既有的第三方库的类型
- 扩展第三方库
vue-router
的meta属性
typescript
// global.d.ts 或 router.ts
import 'vue-router';
declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean
}
}
- 扩展全局对象
window
typescript
// global.d.ts
export {};
declare global {
interface Window {
handleJump: () => void;
}
}
typescript文件的执行和编译工具
- 执行typescript文件的工具有
ts-node
、tsx
- 做typescript文件的转义的工具有
Typescript
自带的tsc
、esbuild
等 - 做typescript文件类型检查的工具有
Typescript
自带的tsc
、tsserver