定义

和JS的关系

TS全名Typescript,微软开发的开源编程语言,是JS语言的超集,在JS基础上添加了类型支持,所有合法的 JavaScript 代码都是合法的 TypeScript 代码。

编译时类型检查

JS是一门弱类型的语言,在定义变量的时候可以不指定类型,类型在运行中可以动态改变。而TS是强类型语言,在定义变量的时候就要指定类型,并在编译的时候检查类型,配合等开发工具(不需要借助插件,因为TS和VSCode都是微软开发的,VS Code 内置了对 TypeScript 的完整支持),TS可以提前到在编写代码的同时就发现代码中的错误。同时鼠标放到一个函数或者变量上,可以查看它的具体类型。

类型擦除

TypeScript 的类型校验只在编译时(开发阶段)起作用,编译后的 JavaScript 代码中完全不包含类型信息,这被叫做类型擦除,因此运行时没有类型校验。

快速开始

Node.js/浏览器,只认识JS代码,不认识TS代码。需要先将TS代码转化为JS代码,然后才能运行

安装:

1
npm i -g typescript

typescript包是用来编译TS代码的包,提供了tsc命令,实现了TS->JS的转化

1
tsc hello.ts

编译生成同名js文件,由TS编译生成的JS文件,代码中就没有类型信息了

其他命令:

tsc --init:工程中会生成一个tsconfig.json配置文件,其中包含着很多编译时的配置。

tsc --watch:监视目录中ts文件的变化

在使用脚手架开发时,创建项目的时候可以直接勾选TS,创建好后直接编写TS代码

分析一个TS配置文件如下:

1
2
3
4
5
{
"compilerOptions": { ... }, // ← 核心:编译和类型检查规则
"include": [ ... ], // ← 哪些文件要被 TS 处理
"exclude": [ ... ] // ← 哪些文件要被排除
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
{
"compilerOptions": {
"target": "esnext", // 编译后的 JS 语法目标版本。 "esnext" = 使用最新的 ECMAScript 语法
"module": "esnext", // 模块系统格式。"esnext" = 输出 ES Modules(import/export),适合现代打包工具(Vite、Webpack 5+)
"strict": false,
"importHelpers": true, // 将 TS 辅助函数(如 __awaiter)从每个文件中提取,统一从 tslib 导入,减少 bundle 体积
"moduleResolution": "node", // 使用 Node.js 的模块解析规则(查找 node_modules、package.json 的 main 字段等)
"experimentalDecorators": true,
"skipLibCheck": true, // 跳过对 node_modules 中 .d.ts 的类型检查(加快编译)
"esModuleInterop": true, // 允许从 CommonJS 模块中默认导入
"allowSyntheticDefaultImports": true, // 让 TS 接受 import foo from 'cjs-module' 的写法
"sourceMap": true, // 生成 .map 文件,方便调试
"baseUrl": ".", // 所有非相对路径(如 @/xxx)都基于项目根目录
"types": [
"webpack-env" // 显式包含全局类型声明包,webpack-env 提供了 Webpack 构建时的全局变量类型
],
// 路径映射(别名)
// 仅 TS 编译器识别此配置!打包工具(Webpack/Vite)需额外配置才能识别 @ 别名。
"paths": {
"@/*": [
"src/*"
]
},
"lib": [ //告诉 TS 哪些全局 API 可用
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
}

类型

类型声明

类型声明可用在变量和函数中,用来限制变量和函数

限制变量:

1
let a: string

限制函数:

1
function count(x :number, y : number) :number{}

函数限制包含函数参数的限制,和函数返回值的限制

类型推断

TS会根据我们的代码,进行类型推导

1
2
let d = 99;
d = false;

上诉后代码在js中不会报错,d 最初是 number,后来变成 boolean,JS 允许动态类型变更。

但在 TypeScript 中上述代码会报编译错误,当你写:let d = 99,TypeScript 会通过类型推断(Type Inference) 自动推断出d 的类型是 number因此,后续你试图赋值一个 boolean(false)给 d 时,TS 编译器会拒绝,因为 boolean 不能赋值给 number 类型的变量。

字面量类型:

1
const str2 = ' Hello TS'

此处的’Hello TS’,就是一个字面量类型,也就是说某个特定的字符串,也可以作为TS中的类型

类型总览

在Js 中的这些内置构造函数: Number ,String ,Boolean,它们用于创建对应的包装对象,在日常开发时很少使用,在TypeScript中也是同理,所以在TypeScript中进行类型声明时,通常都是用小写的number,string ,boolean

如number,string,boolean,null,undefined在JavaScript中是简单数据类型,它们在内存中占用空间少,处理速度快。

如Number对象、(String 对象、Boolean对象),是复杂类型,在内存中占用更多空间,在日常开发时,很少由开发人员自己创建包装对象。 JavaScript在必要时会自动将原始类型包装成对象(自动装箱),以便调用方法或访问属性,因此,我们能看到直接对普通字符串调用方法

undefined和null

默认情况下nullundefined是所有类型的子类型, 就是说你可以把 nullundefined赋值给 number类型的变量

ts特有的类型

any

显式any:

1
let a: any

隐式any:

1
let a

any类型的变量,可以赋值给任意类型的变量

1
2
3
4
let a: any = "hello";
let num: number = a; // 编译通过(但运行时可能出错!)
let bool: boolean = a;
let obj: { x: number } = a;

这是因为 any 被设计为 “完全绕过类型检查” 的逃生舱,TypeScript 把 any 视为 “你可以把它当作任何类型”,这虽然方便,但也破坏了类型安全,是 any 被诟病的原因。

unknown

可以给unknown类型的变量,赋予任意类型的值。unknown 类型的数据,不能直接赋值给有类型的变量,赋值前必须加类型断言或类型判断

1
2
3
4
5
let a: any = "hello";
let u: unknown = "hello";

let str1: string = a; // OK(危险!)
let str2: string = u; // Error: Type 'unknown' is not assignable to type 'string'

读取any类型数据的任何属性都不会报错,而unknown正好与之相反,必须加断言或者判断

1
2
3
4
5
let a: any = 123;
let u: unknown = 123;

console.log(a.toUpperCase()); // 编译通过(但运行时报错!)
console.log(u.toUpperCase()); // 编译错误:Object is of type 'unknown'

想要安全的使用unknown必须通过类型守卫或 类型断言

1
2
3
4
5
6
7
// 方式一:类型守卫(推荐)
if (typeof u === "string") {
console.log(u.toUpperCase()); // ✅ OK
}

// 方式二:类型断言(需确保安全)
console.log((u as string).toUpperCase()); // ✅ 编译通过,但若 u 不是 string,运行时会错

never

never 是 TypeScript 中的一个底层类型,表示“永远不会发生”或“永远不会有值” 的类型。没有值能属于 never 类型(连 null、undefined 都不是),它是所有类型的子类型(可以赋值给任何类型),但没有任何类型(包括 any)可以赋值给 never。

1
let x: never = ???; // ← 你根本无法给它赋一个合法的值!

不存在一个运行时值是 never 类型的,所以手动声明一个 never 类型的变量没有实际意义

void

用于限制函数返回值,只允许返回undefined,return和return undefined是一样的。虽然声明为void仍然会返回值,但是我们不能去使用它的返回值,而应当把它当作不存在

object

只允许存储非原始类型的数据(原始类型的数据包括:string, number, boolean, null, undefined, symbol, bigint)

1
2
3
4
5
6
7
8
9
10
let o1: object = {};           // ✅ OK
let o2: object = []; // ✅ OK(数组是对象)
let o3: object = new Date(); // ✅ OK
let o4: object = Math; // ✅ OK

let o5: object = "hello"; // ❌ Error! string 是原始类型
let o6: object = 42; // ❌ Error!
let o7: object = true; // ❌ Error!
let o8: object = null; // ❌ Error!(在 strictNullChecks 下)
let o9: object = undefined; // ❌ Error!

但是通常情况下,不光要校验类型是对象,还要校验对象内的具体每个属性(如果访问了的话)

补充:Object

是 JavaScript 全局构造函数 Object 的类型,在 TypeScript 中,它等价于 object | null | undefined(在非严格模式下),但实际行为更宽松。几乎所有值都可以赋给 Object 类型,包括原始类型

1
2
3
4
5
let a: Object = "hello";     // ✅ OK
let b: Object = 42; // ✅ OK
let c: Object = true; // ✅ OK
let d: Object = null; // ✅ OK(除非 strictNullChecks)
let e: Object = {}; // ✅ OK

因为在 JavaScript 中,原始类型在访问属性时会自动装箱(boxing) 成包装对象,所以 TS 认为原始类型“可以调用 Object 方法”,于是允许赋值给 Object。实际上null和undefined不能调用方法,但是可以复制给Object类型的变量,但是不能调用方法。

声明对象

1
let person3: {name: string, age?: number} //加?表示可选

这是一个对象字面量类型,表示该对象必须有一个 name 属性,且类型为 string;可以有一个 age 属性,如果存在,则必须是 number 类型;? 表示该属性是可选的(optional)。

上述写法和接口写法与接口(interface)等价写法

1
2
3
4
5
6
interface Person {
name: string;
age?: number;
}

let person3: Person;

声明函数

声明参数类型和返回值类型

1
2
3
4
let count: (a : number, b : number) => number //声明一个函数
count = function (x, y){ //匿名函数
return x +y
}

表示参数的类型,函数的返回值,都是number。如果一个函数没有指定返回值的类型,则由TS根据函数体自动推断

可选参数

1
function myslice(start: number, end?: number): void{} 

可选参数,必须在必选参数后面,这时候参数end可以是number类型或者undefined类型,即可以传一个number类型或者不传都可以

剩余类型

剩余参数与JavaScript的语法类似,需要用 ... 来表示剩余参数

如果剩余参数 rest 是一个由number类型组成的数组,则如下表示:

1
const add = (a: number, ...rest: number[]) => rest.reduce(((a, b) => a + b), a)

声明数组

1
2
3
4
let arr1: string[]//声明字符串数组
let arr2: Array<string>//声明字符串数组
let nums1: number[]; //数字数组
let nums2: Array<number>; //数字数组

tuple元组

不是关键字,没有关键字tuple,元组是一种已知元素数量和各位置类型数组,它是 TypeScript 对 JavaScript 数组的类型增强,不是新数据结构,底层仍是数组。

1
2
3
4
5
6
7
8
9
10
11
12
//第 0 位必须是 string,第 1 位必须是 number,长度固定为 2(在严格模式下)
let arr: [string, number] = ["hello", 42];
let arr2: [string, boolean?];
arr2 = ["hello"]; // ✅ 合法(boolean 可省略)
arr2 = ["hello", true]; // ✅ 合法
arr2 = ["hello", false]; // ✅ 合法
arr2 = []; // ❌ 错误!第一个元素 string 是必填的
//这表示第 0 位:必须是 number, 第 1 位及之后:可以有任意多个 string(包括 0 个)
let arr3: [number, ...string[]];
arr3 = [1]; // ✅ 只有 number,后面 string[] 为空
arr3 = [1, "a"]; // ✅
arr3 = [1, "a", "b", "c"]; // ✅

enum枚举

枚举,其实就是被命名的常数的集合

1
2
3
4
5
6
enum Direction {
Up, Down, Left, Right
}//可以把Direction作为一种自定义的数据类型,用来限制
//此时 Direction 就像一个自定义类型,变量只能取这四个值之一:
let move: Direction = Direction.Up; // ✅
move = 99; // ❌ Error! 类型不匹配

数字枚举

枚举成员的值为数字的枚举,称为数字枚举。不给枚举常量赋值,枚举成员的值默认是从0自增的,也可以给枚举成员赋值数字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//默认行为
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right // 3
}
//一旦手动赋值,后续未赋值成员会在此基础上递增。
enum StatusCode {
OK = 200,
NotFound, // 201(自动 +1)
Forbidden = 403,
Timeout // 404
}

编译后的结果:

1
2
3
4
5
6
7
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 0] = "Up";
Direction[Direction["Down"] = 1] = "Down";
Direction[Direction["Left"] = 2] = "Left";
Direction[Direction["Right"] = 3] = "Right";
})(Direction || (Direction = {}));

双向映射:

  • Direction.Up0
  • Direction[0]"Up"(反向查找)

字符枚举

给每个枚举成员赋值,字符串枚举没有自增长行为,因此,字符串枚举的每个成员必须有初始值

1
2
3
4
5
6
7
8
9
10
enum HttpMethod {
GET = "GET",
POST = "POST",
PUT = "PUT",
DELETE = "DELETE"
}
enum Color {
Red = "RED",
Green // ❌ 错误!字符串枚举不能省略初始值
}

编译后的结果:

1
2
3
4
5
6
7
var HttpMethod;
(function (HttpMethod) {
HttpMethod["GET"] = "GET";
HttpMethod["POST"] = "POST";
HttpMethod["PUT"] = "PUT";
HttpMethod["DELETE"] = "DELETE";
})(HttpMethod || (HttpMethod = {}));

只有正向映射:HttpMethod.GET → “GET”

常量枚举

常量枚举是一种特殊枚举类型,它使用const 关键字定义,在编译时会被内联,避免额外代码。所谓“内联”其实就是TypeScript在编译时,会将枚举成员引用,替换为它们的实际值,而不是生成额外的枚举对象。这可以减少生成的JavaScript代码量,并提高运行时性能

1
2
3
4
5
6
const enum Status {
Success = 200,
Error = 500
}

let code = Status.Success;

编译后(.js 文件):

1
let code = 200; // ← 枚举完全消失!

常量枚举不能被动态访问:

1
2
const enum E { A = 1 }
const x = E[Math.random() > 0.5 ? 'A' : 'B']; // ❌ 错误!不能用表达式索引 const enum

typeof

TS中的typeof区别于JS中的typeof,用来返回一个数据的TS类型

1
2
Let p = { x: 1,y: 2 }
function formatPoint(point: typeof p){} //typeof p 返回的是{x:number,y:number}

高级类型

交叉类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface Dog {
bark(): void;
name: string;
}

interface Bird {
fly(): void;
feathers: number;
}

// 交叉类型:既有狗的特性,又有鸟的特性
type DogBird = Dog & Bird;

// creature 必须同时实现 Dog 和 Bird 的所有成员。
const creature: DogBird = {
name: "Hybrid",
feathers: 100,
bark() { console.log("Woof!"); },
fly() { console.log("Flying!"); }
};

与原始类型交叉通常无意义

1
type Weird = string & number; // ❌ 实际上是 never 类型!

因为没有任何值能同时是 string 又是 number

联合类型

联合类型是一种非常核心且常用的类型机制,它表示一个值可以是多种类型中的一种。使用 |(竖线)符号来定义。

1
type MyType = TypeA | TypeB | TypeC;

表示:MyType 的值 可以是 TypeA,也可以是 TypeB,或者 TypeC —— 但一次只能是其中一种

类型别名type

type是TS中的关键字,作用包括:

给已有类型取别名

1
type shuzi = number

定义联合类型

1
2
type Gender ='男’|'' // 限制要么是男要么是女,有点像枚举了
typer Status = number | string // 多个类型联合组成新的类型,表示既可以是 string也可以是number

定义交叉类型

1
2
3
4
5
6
7
8
9
10
11
type Area = { //给对象类型取别名
height: number;//高
width: number;//宽
};
//地址
type Address = { //给对象类型取别名
num : number //楼号
cell: number //单元号
room : string //房间号
}
type House = Area & Address //House具有五个属性

在函数定义时,限制函数返回值为void,那么函数的返回值就必须是空。但使用类型声明限制函数返回值为void时,TypeScript 并不会严格要求函数返回空。

1
2
type LogFunc = () => void
const f1: LogFunc = ()=>{ return 100;//允许返回非空值 };

泛型

就相当于可变参数类型,具体是什么类型由调用的时候确定

在函数中使用泛型

1
2
3
4
5
6
// 定义
function returnItem<T>(para: T): T {
return para
}
// 使用
returnItem(123) //此时函数的返回值就是number类型

函数参数的类型和返回值的类型,都在函数调用后,传入具体的值后才确定。

在定义函数时使用了泛型,后续调用函数并没有泛型的标志

在接口中使用泛型

1
2
3
4
5
6
// 定义
interface ReturnItemFn<T> {
(para: T): T
}
// 使用
const returnItem: ReturnItemFn<number> = para => para
1
2
3
4
5
6
7
8
9
10
11
12
interface ApiResponse<T> {
code: number;
message: string;
data: T; // T 可以是任何类型
}

// 使用
const userResponse: ApiResponse<User> = {
code: 200,
message: 'success',
data: { id: 1, name: 'Alice' }
};

在接口中使用泛型,能很好的起到接口复用的效果

在定义接口时候使用了泛型,使用接口的时候,也要保留泛型的标志,手动指定类型

在类中使用泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义
class Stack<T> {
private arr: T[] = []

public push(item: T) {
this.arr.push(item)
}

public pop() {
this.arr.pop()
}
}
// 使用
const stack = new Stack<number>()

在定义类的时候使用了泛型,使用类创建实例的时候也要手动传入类型,保留泛型的标志

泛型约束

泛型约束(Generic Constraints)是通过 extends 关键字实现的,用于限制泛型参数的类型范围,确保传入的类型满足特定结构或能力

1
2
3
function fn<T extends Constraint>(arg: T): T {
// ...
}

T extends Constraint 表示泛型 T 必须是 Constraint 的子类型(即 T 至少包含 Constraint 定义的所有属性/方法)。

Constraint 可以是接口,类型别名(type)或者原始类型(如 string、number)

1
2
3
4
5
6
7
8
9
10
11
12
13
// ❌ 不安全:T 可能没有 length
function getLength<T>(arg: T): number {
return arg.length;
}

// 使用 extends 约束
interface HasLength {
length: number;
}

function getLength<T extends HasLength>(arg: T): number {
return arg.length; // ✅ 安全!
}
1
2
3
4
// 只接受 string 或其子类型(如字面量类型)
function logString<T extends string>(value: T): void {
console.log(`String: ${value}`);
}
1
2
3
4
5
type b = number | string
// 传入的参数只能是number或者string类型
function func<T extends b>(a:T):T{
return a
}

class

是用户定义的引用类型,组成包括字段,构造函数和方法

字段:

1
2
3
4
class Person {
age: number
gender = '男' //gender: string ='男'
}

构造函数:

1
2
3
4
constructor(age: number, gender: string) {
this.age = age
this.gender = gender
}//用来初始化类的实例

属性和构造函数简写:不写属性,构造函数不写函数体

1
2
3
class Person{
constructor(public name: string, public age: number) {}
}//简写的话public不能丢,或者指定其他类型

实例化:

1
new Person('tom','男')

实例方法:就是在class里面写ts函数,方法体里面可以通过this访问实例属性

类的继承

子类实例可以调用父类的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Animal {
move(distance:number = 0){
console.log'移动的距离是:distance');
}
}
class Dog extends Animal{
bark(){
console.log('woof')
}
}

const dog = new Dog()
dog.bark();
dog.move(10)

在子类中可以重写父类的方法

1
2
3
4
5
6
7
8
9
10
11
12
class Parent {
doPrint() {
console.log("parent class");
}
}

class Child extends Parent {
override doPrint() { // ✅ 明确声明重写
super.doPrint();
console.log("child class do print");
}
}

修饰符

在JS的类中,并没有修饰符

public:表示公有的,公有成员可以被任何地方访问,是默认的可见性。

protected:表示受保护的,仅对其声明所在类和子类中可见。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Father {
private name: string;
constructor(name: string){
this.name = name
}
}
class Son extends Father {
say(){
console.log(this.name) //能正常访问
}
}
const father = new Father('chenghuai')
father.name //报错,外部无法访问

private:只在该类内部中可见

1
2
3
4
5
6
7
8
9
10
11
12
13
class Father {
private name: string;
constructor(name: string){
this.name = name
}
}
class Son extends Father {
say(){
console.log(this.name) //报错,子类内部也无法访问
}
}
const father = new Father('chenghuai')
father.name //报错,无法访问

readonly:

1
2
3
4
5
6
class Person{
readonly age: number = 18
constructor(age: number){
this.age = age
}
}

使用readonly关键字修饰该属性是只读的,不能在构造函数之外对属性赋值(18是默认值),类型推断age的类型就是18,这样构造器内部也只能给age赋值18,只要是readonly修饰的属性,就要提供明确的类型。

readonly只能修饰属性不能修饰方法

抽象类

使用abstract关键字定义一个抽象类,还可以定义一个抽象方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
abstract class Package {
//构造方法
constructor(public weight: number) { }
//抽象方法
abstract calculate(): number
//具体方法
printPackage() {
console.log(`包裹重量为: ${this.weight}kg,运费为:${this.calculate()}元`);
}
}

class StandardPackage extends Package {
constructor(
weight: number,//这里不指定修饰符是因为父类指定了
public unitPrice: number
) { super(weight) } //调用父类构造器,初始化父类数据}
// 重写父类的抽象方法
override calculate(): number {
return this.weight * this.unitPrice;
}
}

抽象类既可以包含抽象方法,也可以包含具体方法,一个类只能继承一个抽象类。

interface

interface是一种定义结构的方式,主要作用是为:类、对象(用的最多)、函数等规定一种契约,这样可以确保代码的一致性和类型安全,但要注意interface只能定义格式,不能包含任何实现。

当一个对象类型被多次使用时,一般会使用接口( interface)来描述对象的类型,达到复用的目的

1
2
3
4
5
6
7
8
9
10
11
interface IPerson {
name?: string //name属性是可选的
readonly age: number // age属性是只读的
sayHi(): void
}
type PersonType = {
name: string;
age: number;
sayHi(): void;
};//这两个的用法都一样
let person1:IPerson = {...}

接口继承

1
2
3
4
5
6
7
8
9
10
11
interface Father{
color:string
}

interface Mother {
height: number
}
interface Son extends Father,Mother{
name:string
age: number
}

这样Son接口就有四个属性了,不需要手动书写重复的属性

实现接口

通过implements关键字实现接口

1
2
3
4
5
6
7
8
interface Singable{
sing:()=>void
}
class Person implements Singable {
sing( ){
console.log('你是我的小呀小苹果儿')
}
}

与抽象类的区别和联系:接口只能描述结构,不能有任何实现代码,—个类可以实现多个接口,但是只能继承一个抽象类。

只要对象包含接口要求的所有属性,并且类型兼容,那么多余的属性是被允许的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface User {
name: string;
age: number;
}

// 一个有3个属性的对象
const person = {
name: "Alice",
age: 25,
email: "alice@example.com" // 多出来的属性
};

// 赋值给 User 类型变量 ✅ 完全合法,不会报错!
const user: User = person;

文件

.ts:其实就是普通 TypeScript 文件,既包含类型信息又包含 JavaScript 代码。在编译时,TypeScript 会利用其中的类型进行检查;同时生成对应的 .js 文件;如果启用了 declaration: true,还会生成 .d.ts 声明文件

.d.ts:只包含类型声明,不包含任何可执行代码,不会被编译;用于为 纯 JavaScript 代码 提供类型信息,或全局类型扩展。**.d.ts是真正的类型声明文件**。

内置的类型声明文件

TS为JS运行时可用的所有标准化内置API,都提供了声明文件,这些文件既不需要编译生成,也不需要三方提供

@types文件夹

node_modules 中的 @types 文件夹(例如 node_modules/@types/node、node_modules/@types/react 等)存储的是 TypeScript 的类型声明文件(.d.ts 文件),用于为没有原生 TypeScript支持的 JavaScript 库 提供类型信息。社区(主要是 DefinitelyTyped 项目)为这些库编写了对应的 .d.ts 声明文件,并以 @types/xxx 的形式发布到 npm。

自定义类型声明文件

自定义.d.ts文件主要有2中作用,一是共享TS类型,二是给JS文件提供类型

先介绍.d.ts文件是如何给JS文件提供类型的:

假设你有如下的工具函数:

1
2
3
4
5
6
// utils/math.js
export function add(a, b) {
return a + b;
}

export const PI = 3.14159;

你不能/不想将其立即转成 TS模块,那就需要为它编写一个同名的 .d.ts 声明文件

1
2
3
// utils/math.d.ts
export function add(a: number, b: number): number; //给函数指明类型,不需要函数体
export const PI: number;

要注意的是.d.ts 文件必须与 .js 文件同名、同目录,TypeScript 才会自动关联。

然后再TS文件中使用,就能获得完整的类型支持

1
2
3
4
5
6
7
// app.ts
import { add, PI } from './utils/math';

const result = add(2, 3); // ✅ 类型推断为 number
const area = PI * 2 * 2; // ✅ PI 是 number

add("2", 3); // ❌ 编译错误!Argument of type 'string' is not assignable to parameter of type 'number'

@types文件夹中的包的工作原理也是如此。

再来说第二个用途:共享 TS 类型 / 扩展全局环境。这类 .d.ts 文件不对应任何 .js 文件。在这类 .d.ts 文件中可以有2中写法:declareexport

使用declare声明的类型是全局类型, 在任何 .ts 文件中直接使用:

1
2
3
4
5
6
7
8
9
// types/global-types.d.ts
declare interface User {
id: string;
name: string;
}
declare type UserRole = 'admin' | 'user';

// app.ts
const user: User = { id: '1', name: 'Alice' }; // ✅ 无需 import

因为.d.ts 文件中的 declare ,会将类型注入全局命名空间,TS 自动可见。

第二种情况是使用export导出类型

1
2
3
4
5
6
7
// types/user.d.ts
export interface User {
id: string;
name: string;
}

export type UserRole = 'admin' | 'user';

在其他ts文件中必须导入使用,不能直接使用,否则会报错

1
2
3
4
5
6
7
// app.ts
const user: User = { ... }; // ❌ Cannot find name 'User'

// app.ts
// 必须显示导入
import type { User, UserRole } from './types/user';
const user: User = { id: '1', name: 'Alice' }; // ✅

项目重构

将一个JS项目重构为TS项目,可以手动编写 .d.ts 文件来重构, 也可以直接将 .js 文件重命名为 .ts ,并逐步添加类型,即直接改造源码文件,让 TS 编译它

对于Vue项目,使用Vue重构,最好的方式是新建一个项目,并勾选TS支持。Vue 官方脚手架(Vite / Vue CLI)对 TS 支持非常成熟;这么做可以避免手动配置出错,能获得最佳实践的默认配置。还能保留原有JS项目的代码。

ref的类型

Vue 的 ref()自动推断类型(比如 ref(0)Ref<number>),但在以下情况显式指定类型更安全、更清晰

1
2
3
4
5
import { ref } from 'vue'

// ✅ 显式指定类型(推荐写法)
const unread_msgs_num = ref<number>(0)
const total = ref<number>(0)

其他常用实践:

如果ref用来捕获一个组件实例,那么它的类型应该这样定义:

1
2
3
import Cropper from "@/components/cropper.vue";
type CropperInstance = InstanceType<typeof Cropper>; // 使用typeof获取Cropper组件的类型
const videoCropper = ref<null | CropperInstance>(null);

当我们将一个Ref的类型限制为一个接口类型,但是又不想初始化全部属性,如果我们写成

1
const userinfo = ref<UserInfo | object>({});

后续代码就很可能报错

1
2
3
// 使用时报错:
userinfo.value.id = 1; // ❌ 错误:userinfo.value 可能是 {},没有 id 属性
userinfo.value.name = '张三'; // ❌ 同样错误

此时我们就可以借助null来解决这个问题,写成

1
const userinfo = ref<UserInfo | null>(null);

后续我们只需判断userinfo.value是否存在即可

inject的类型

在TS+Vue3项目中,我们通过inject注入的数据,也需要指定类型,方法和给ref数据指定类型一样,使用的是泛型

1
2
3
4
5
6
7
8
9
10
//先定义用户信息的接口
interface UserInfo {
id: number;
nickname: string;
avatar_url: string;
// ... 其他字段
}

// 2. 在使用 inject 的组件中,通过泛型指定类型
const userinfo = inject<UserInfo | null>('userinfo', null);

这告诉 TypeScript:我注入的这个值要么是 UserInfo 类型,要么是 null(默认值)。

但是如果注入的是一个RefImpl,则需要通过Ref<UserInfo >的方式来校验,,还要导入Ref类型,感觉比较麻烦。

1
import type { Ref } from "vue"

如果注入的是一个计算属性,方法也是一样的,只需将Ref替换为ComputedRef

1
import type { ComputedRef } from 'vue';
1
2
3
4
const is_myself = computed(() => {
return id == userStore.userinfo.id;
});
provide("is_myself", is_myself);
1
const is_myself = inject<ComputedRef<boolean>>("is_myself", computed(() => false));

值得注意的是,在TS中,注入一个值,必须提供默认值,否则会认为这个值可能不存在,而且指定的默认值,也必须满足自己指定的注入类型

处理Vue文件

在vue3组件中启用TS,需要指定lang

1
<script setup lang="ts">

然后启用Volar插件,现在叫做Vue(offical)插件 ID 仍然是 Vue.volar,Vetur老旧,对 Vue 3 + <script setup> + TS 路径别名支持极差。

在 TypeScript 项目中,**.vue 文件默认没有类型定义**,TS 不知道如何处理 import xxx from '*.vue',需要在项目中创建一个 全局类型声明文件src/shims-vue.d.ts,告诉 TypeScript:“所有 .vue 文件导出的是一个 Vue 组件(即 DefineComponent)”,文件内容如下。

1
2
3
4
5
declare module '*.vue' {
import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}

在TS项目中,我们也可以使用TS,来限制组件通过props接收的数据类型

1
2
3
4
interface Comment {//...}
const props = defineProps<{
comment: Comment; //在泛型中书写类型声明
}>();

DOM及事件类型

元素

Video元素的类型是:HTMLVideoElement

img元素的类型是:HTMLImageElement

Canvas元素的类型是:HTMLCanvasElement

普通元素结点的类型是:HTMLElement

input元素的类型是:HTMLInputElement

textarea元素的类型是:HTMLTextAreaElement

事件

updateProgess事件的类型是:ProgressEvent

鼠标事件的类型是:MouseEvent,

拖拽事件的类型是:DragEvent

通用的事件类型是:Event

键盘事件类型:KeyboardEvent

滚轮事件的类型是:WheelEvent

其他需要注意的点

  • TypeScript 默认不允许导入没有类型声明的 JavaScript 模块

  • JS项目中,有的JS库是不支持TS的,没有对应的类型声明文件,所以直接在TS项目中使用会报错,隐式拥有 “any” 类型。

  • 使用vue脚手架新建TS项目,得到的初始Vite配置文件和JS项目中的没有区别。

  • 可以使用unknown[]来泛指任意类型的数组。使用func:(...args: unknown[])来指定具有任意数量和类型的参数的函数,但是使用这类数组中的参数的时候,必须进行类型检查

  • 即便一个文件被修改为TS,以JS方式引入还是不会报错

  • 非空断言a.seconds!,告诉TS,seconds属性一定不为空

  • v-model指令赋值时,不能传入类型privacy?.show_school这样的可选表达式,因为privacy?.show_school后续可能被赋值,如果privacy?.show_school的值为null,后续v-model会尝试给null赋值。

  • 在TS中使用 @ts-ignore注释,可以让一行代码不被TS检查