定义
和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 | { |
1 | { |
类型
类型声明
类型声明可用在变量和函数中,用来限制变量和函数
限制变量:
1 | let a: string |
限制函数:
1 | function count(x :number, y : number) :number{} |
函数限制包含函数参数的限制,和函数返回值的限制
类型推断
TS会根据我们的代码,进行类型推导
1 | let d = 99; |
上诉后代码在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
默认情况下null和undefined是所有类型的子类型, 就是说你可以把 null和 undefined赋值给 number类型的变量
ts特有的类型
any
显式any:
1 | let a: any |
隐式any:
1 | let a |
any类型的变量,可以赋值给任意类型的变量
1 | let a: any = "hello"; |
这是因为 any 被设计为 “完全绕过类型检查” 的逃生舱,TypeScript 把 any 视为 “你可以把它当作任何类型”,这虽然方便,但也破坏了类型安全,是 any 被诟病的原因。
unknown
可以给unknown类型的变量,赋予任意类型的值。unknown 类型的数据,不能直接赋值给有类型的变量,赋值前必须加类型断言或类型判断
1 | let a: any = "hello"; |
读取any类型数据的任何属性都不会报错,而unknown正好与之相反,必须加断言或者判断
1 | let a: any = 123; |
想要安全的使用unknown必须通过类型守卫或 类型断言:
1 | // 方式一:类型守卫(推荐) |
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 | let o1: object = {}; // ✅ OK |
但是通常情况下,不光要校验类型是对象,还要校验对象内的具体每个属性(如果访问了的话)
补充:Object
是 JavaScript 全局构造函数 Object 的类型,在 TypeScript 中,它等价于 object | null | undefined(在非严格模式下),但实际行为更宽松。几乎所有值都可以赋给 Object 类型,包括原始类型
1 | let a: Object = "hello"; // ✅ OK |
因为在 JavaScript 中,原始类型在访问属性时会自动装箱(boxing) 成包装对象,所以 TS 认为原始类型“可以调用 Object 方法”,于是允许赋值给 Object。实际上null和undefined不能调用方法,但是可以复制给Object类型的变量,但是不能调用方法。
声明对象
1 | let person3: {name: string, age?: number} //加?表示可选 |
这是一个对象字面量类型,表示该对象必须有一个 name 属性,且类型为 string;可以有一个 age 属性,如果存在,则必须是 number 类型;? 表示该属性是可选的(optional)。
上述写法和接口写法与接口(interface)等价写法
1 | interface Person { |
声明函数
声明参数类型和返回值类型
1 | let count: (a : number, b : number) => number //声明一个函数 |
表示参数的类型,函数的返回值,都是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 | let arr1: string[]//声明字符串数组 |
tuple元组
不是关键字,没有关键字tuple,元组是一种已知元素数量和各位置类型的数组,它是 TypeScript 对 JavaScript 数组的类型增强,不是新数据结构,底层仍是数组。
1 | //第 0 位必须是 string,第 1 位必须是 number,长度固定为 2(在严格模式下) |
enum枚举
枚举,其实就是被命名的常数的集合
1 | enum Direction { |
数字枚举
枚举成员的值为数字的枚举,称为数字枚举。不给枚举常量赋值,枚举成员的值默认是从0自增的,也可以给枚举成员赋值数字。
1 | //默认行为 |
编译后的结果:
1 | var Direction; |
双向映射:
Direction.Up→0Direction[0]→"Up"(反向查找)
字符枚举
给每个枚举成员赋值,字符串枚举没有自增长行为,因此,字符串枚举的每个成员必须有初始值。
1 | enum HttpMethod { |
编译后的结果:
1 | var HttpMethod; |
只有正向映射:HttpMethod.GET → “GET”
常量枚举
常量枚举是一种特殊枚举类型,它使用const 关键字定义,在编译时会被内联,避免额外代码。所谓“内联”其实就是TypeScript在编译时,会将枚举成员引用,替换为它们的实际值,而不是生成额外的枚举对象。这可以减少生成的JavaScript代码量,并提高运行时性能。
1 | const enum Status { |
编译后(.js 文件):
1 | let code = 200; // ← 枚举完全消失! |
常量枚举不能被动态访问:
1 | const enum E { A = 1 } |
typeof
TS中的typeof区别于JS中的typeof,用来返回一个数据的TS类型
1 | Let p = { x: 1,y: 2 } |
高级类型
交叉类型
1 | interface Dog { |
与原始类型交叉通常无意义
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 | type Gender ='男’|'女' // 限制要么是男要么是女,有点像枚举了 |
定义交叉类型
1 | type Area = { //给对象类型取别名 |
在函数定义时,限制函数返回值为void,那么函数的返回值就必须是空。但使用类型声明限制函数返回值为void时,TypeScript 并不会严格要求函数返回空。
1 | type LogFunc = () => void |
泛型
就相当于可变参数类型,具体是什么类型由调用的时候确定
在函数中使用泛型
1 | // 定义 |
函数参数的类型和返回值的类型,都在函数调用后,传入具体的值后才确定。
在定义函数时使用了泛型,后续调用函数并没有泛型的标志
在接口中使用泛型
1 | // 定义 |
1 | interface ApiResponse<T> { |
在接口中使用泛型,能很好的起到接口复用的效果
在定义接口时候使用了泛型,使用接口的时候,也要保留泛型的标志,手动指定类型
在类中使用泛型
1 | // 定义 |
在定义类的时候使用了泛型,使用类创建实例的时候也要手动传入类型,保留泛型的标志
泛型约束
泛型约束(Generic Constraints)是通过 extends 关键字实现的,用于限制泛型参数的类型范围,确保传入的类型满足特定结构或能力
1 | function fn<T extends Constraint>(arg: T): T { |
T extends Constraint 表示泛型 T 必须是 Constraint 的子类型(即 T 至少包含 Constraint 定义的所有属性/方法)。
Constraint 可以是接口,类型别名(type)或者原始类型(如 string、number)
1 | // ❌ 不安全:T 可能没有 length |
1 | // 只接受 string 或其子类型(如字面量类型) |
1 | type b = number | string |
class
是用户定义的引用类型,组成包括字段,构造函数和方法
字段:
1 | class Person { |
构造函数:
1 | constructor(age: number, gender: string) { |
属性和构造函数简写:不写属性,构造函数不写函数体
1 | class Person{ |
实例化:
1 | new Person('tom','男') |
实例方法:就是在class里面写ts函数,方法体里面可以通过this访问实例属性
类的继承
子类实例可以调用父类的方法
1 | class Animal { |
在子类中可以重写父类的方法
1 | class Parent { |
修饰符
在JS的类中,并没有修饰符
public:表示公有的,公有成员可以被任何地方访问,是默认的可见性。
protected:表示受保护的,仅对其声明所在类和子类中可见。
1 | class Father { |
private:只在该类内部中可见
1 | class Father { |
readonly:
1 | class Person{ |
使用readonly关键字修饰该属性是只读的,不能在构造函数之外对属性赋值(18是默认值),类型推断age的类型就是18,这样构造器内部也只能给age赋值18,只要是readonly修饰的属性,就要提供明确的类型。
readonly只能修饰属性不能修饰方法
抽象类
使用abstract关键字定义一个抽象类,还可以定义一个抽象方法
1 | abstract class Package { |
抽象类既可以包含抽象方法,也可以包含具体方法,一个类只能继承一个抽象类。
interface
interface是一种定义结构的方式,主要作用是为:类、对象(用的最多)、函数等规定一种契约,这样可以确保代码的一致性和类型安全,但要注意interface只能定义格式,不能包含任何实现。
当一个对象类型被多次使用时,一般会使用接口( interface)来描述对象的类型,达到复用的目的
1 | interface IPerson { |
接口继承
1 | interface Father{ |
这样Son接口就有四个属性了,不需要手动书写重复的属性
实现接口
通过implements关键字实现接口
1 | interface Singable{ |
与抽象类的区别和联系:接口只能描述结构,不能有任何实现代码,—个类可以实现多个接口,但是只能继承一个抽象类。
只要对象包含接口要求的所有属性,并且类型兼容,那么多余的属性是被允许的
1 | interface User { |
文件
.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 | // utils/math.js |
你不能/不想将其立即转成 TS模块,那就需要为它编写一个同名的 .d.ts 声明文件
1 | // utils/math.d.ts |
要注意的是
.d.ts文件必须与.js文件同名、同目录,TypeScript 才会自动关联。
然后再TS文件中使用,就能获得完整的类型支持
1 | // app.ts |
@types文件夹中的包的工作原理也是如此。
再来说第二个用途:共享 TS 类型 / 扩展全局环境。这类 .d.ts 文件不对应任何 .js 文件。在这类 .d.ts 文件中可以有2中写法:declare 和 export。
使用declare声明的类型是全局类型, 在任何 .ts 文件中直接使用:
1 | // types/global-types.d.ts |
因为.d.ts 文件中的 declare ,会将类型注入全局命名空间,TS 自动可见。
第二种情况是使用export导出类型
1 | // types/user.d.ts |
在其他ts文件中必须导入使用,不能直接使用,否则会报错
1 | // app.ts |
项目重构
将一个JS项目重构为TS项目,可以手动编写 .d.ts 文件来重构, 也可以直接将 .js 文件重命名为 .ts ,并逐步添加类型,即直接改造源码文件,让 TS 编译它。
对于Vue项目,使用Vue重构,最好的方式是新建一个项目,并勾选TS支持。Vue 官方脚手架(Vite / Vue CLI)对 TS 支持非常成熟;这么做可以避免手动配置出错,能获得最佳实践的默认配置。还能保留原有JS项目的代码。
ref的类型
Vue 的 ref() 能自动推断类型(比如 ref(0) → Ref<number>),但在以下情况显式指定类型更安全、更清晰,
1 | import { ref } from 'vue' |
其他常用实践:
如果ref用来捕获一个组件实例,那么它的类型应该这样定义:
1 | import Cropper from "@/components/cropper.vue"; |
当我们将一个Ref的类型限制为一个接口类型,但是又不想初始化全部属性,如果我们写成
1 | const userinfo = ref<UserInfo | object>({}); |
后续代码就很可能报错
1 | // 使用时报错: |
此时我们就可以借助null来解决这个问题,写成
1 | const userinfo = ref<UserInfo | null>(null); |
后续我们只需判断userinfo.value是否存在即可
inject的类型
在TS+Vue3项目中,我们通过inject注入的数据,也需要指定类型,方法和给ref数据指定类型一样,使用的是泛型
1 | //先定义用户信息的接口 |
这告诉 TypeScript:我注入的这个值要么是 UserInfo 类型,要么是 null(默认值)。
但是如果注入的是一个RefImpl,则需要通过Ref<UserInfo >的方式来校验,,还要导入Ref类型,感觉比较麻烦。
1 | import type { Ref } from "vue" |
如果注入的是一个计算属性,方法也是一样的,只需将Ref替换为ComputedRef
1 | import type { ComputedRef } from 'vue'; |
1 | const is_myself = computed(() => { |
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 | declare module '*.vue' { |
在TS项目中,我们也可以使用TS,来限制组件通过props接收的数据类型
1 | interface 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检查
