TypeScript
简介
TypeScript
由微软开发,是基于JavaScript
的一个扩展语言.
TypeScript
包含了JavaScript
的所有内容,即TypeScript
是JavaScript
的超集
TypeScript
增加了: 静态类型检查,接口,泛型等很多现代开发特性,因此更加适合大型项目的开发.
TypeScript
需要编译为JavaScript
,然后才能在浏览器或其他JavaScript
运行环境运行.
为何需要TypeScript
JavaScript
当年诞生的时候定位是浏览器的脚本语言,用于在网页中嵌入一些简单的逻辑
JavaScript
的困扰
- 不清不楚的数据类型
1 | let welcome = 'hello' |
- 有漏洞的逻辑
1 | const str = Date.now() % 2 ? '奇数' : '偶数' |
- 访问不存在的属性
1 | const obj = { width: 10 }; |
- 低级的拼写错误
1 | const message = 'hello world'; |
编译TypeScript
浏览器不能直接运行TypeScript
代码,需要编译为JavaScript
再交给浏览器解析器执行.
命令行编译
1 | pnpm add typescript -g #全局安装TypeScript |
自动化编译
1 | # 初始化配置文件 |
工程中会生成一个tsconfig.json
配置文件,其中包含着很多编译时的配置.
1 | # 监视目录中的.ts文件变化 |
类型声明
使用:
来对变量或函数形参,进行类型声明
1 | let a: string // 变量a只能存储字符串 |
类型推断
类型总览
JavaScript的数据类型
8个大类型,其中7个基本类型,1个对象类型
- string
- number
boolean
- undefined
- null
bigInt
- symbol
- object
TypeScript中的数据类型
- 上述所有JavaScript类型
- 六个新的类型
any
unknown
never
void
tuple
enum
- 两个用于自定义类型的方式
type
interface
注意类型区别
1
2
3
4
5
6
7 let str1: string; // TS官方推荐的写法
str1 = 'hello';
str1 = new String('hello'); // 不能将类型“String”分配给类型“string”。“string”是基元,但“String”是包装器对象。如可能首选使用“string”。
let str2: String
str2 = 'hello'
str2 = new String('hello') // 没问题,不报错.在
JavaScript
中的这些内置构造函数:Number
,String
,Boolean
,他们用于创建对应的包装对象,在日常开发中很少使用,在TypeScript
中也是同理,所以在TypeScript
中进行类型声明时,通常都使用小写的number
,string
,boolean
- 原始类型 vs 包装对象
- 原始类型:如
number
,string
,boolean
,在JavaScript
中是简单数据类型,他们在内存中占用空间少,处理速度快.- 包装对象:如
Number
对象,String
对象,Boolean
对象,是复杂类型,在内存中占用更多空间,在日常开发中很少由开发人员创建自己的包装对象
- 自动装箱: JavaScript在必要时会自动将原始类型包装成对象,以便调用方法或访问属性.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 >let str = 'hello'; // 原始类型字符串
>// 当访问str.length时,JavaScript引擎做了以下工作
>let size = (function() {
// 1. 自动装箱: 创建一个临时的String对象包装原始字符串
let tempStringOjbect = new String(str);
// 2. 访问String对象的length属性
let lengthValue = tempStringObject.length;
// 3. 销毁临时对象,返回长度值
// (JavaScript引擎自动处理对象销毁,开发者无感知)
return lengthValue;
>})();
>console.log(size); // 输出: 5
常用类型和语法
any
any
的含义是: 任意类型,一旦将变量类型设置为any
,就意味着放弃了对该变量的类型检查.
1 | // 明确的表示a的类型是 any [显示声明为any] |
注意点: any
类型的变量,可以赋值给任意类型的变量
1 | let c: any |
unknown
unknown
的含义是: 未知类型
unknown
可以理解为一个类型安全的any
,适用于: 不确定数据的具体类型.
1 | // 设置a的类型为unknown |
unknown
会强制开发者在使用之前进行类型检查,从而提供更强的类型安全性.
1 | // 设置a的类型为unknown |
读取any
类型数据的任何属性都不会报错,而unknown
正好与之相反.
1 | let str1: string; |
never
never
的含义是:任何值都不是,简言之就是不能有值,undefined
,null
,''
,0
都不行.
- 几乎不用
never
去直接限制变量,因为没有意义,例如
1 | // 指定a的类型为never, 那就意味着a以后不能存任何数据了 |
never
一般是TypeScript
主动推断出来的,例如:
1 | // 指定a的类型为string |
never
也可以用与限制函数的返回值
1 | // 限制throwError函数不需要任何返回值,任何值都不行,包括没有显示地返回,默认返回的undefined也不行 |
void
void
通常用于函数返回值的声明,含义:函数返回值为空,调用者也不应该依赖其返回值进行任何操作
1 | function logMessage(msg: string): void { |
注意: 编码者没有编写
return
去指定函数的返回值,所以logMessage
函数是没有显式返回值的,但会有一个隐式返回值,就是undefined
,即:虽然函数返回类型为void
,但也是可以接受undefined
的,简单记:undefined
是void
可以接受的一种”空”.
- 以下写法均符合规范
1 | // 无警告 |
理解void和undefined
void
是一个广泛的概念,用来表达空,而undefined
则是这种”空”的具体实现之一.- 因此可以说
undefined
是void
能接受的”空”状态的一种具体形式.- 换句话说:
void
包含undefined
,但void
表达的语义超越了单纯的undefined
,它是一种意图上的约定,而不仅仅是特定值的限制.总结: 若函数的返回类型为
void
,那么:
- 从语法上讲: 函数是可以返回
undefined
的,至于显式返回还是隐式返回这无所谓.- 从语义上讲: 函数调用者不应关心函数返回的值,也不应依赖返回值进行任何操作!即便返回了
undefined
值.
object
关于
object
与Object
,直接说结论,实际开发中用的相对较少,因为范围太大了.
object(小写)
object
(小写)的含义是:所有非原始类型,可存储: 对象,函数,数组等,由于限制范围比较宽泛,在实际开发中使用的相对较少.
1 | let a:object; // a的值可以是任何非原始类型 |
Object
(大写)的含义是:
- 官方描述: 所有可以调用
Object
方法的类型 - 简单记忆: 除了
undefined
和null
的任何值 - 由于限制的范围实在太大了,所以实际开发中使用频率极低.
1 | let a:Object; // a的值可以是任何非原始类型 |
声明对象类型
- 实际开发中,限制一般对象,通常使用以下形式
1 | // 限制person1对象必须有name属性,age为可选属性 |
- 索引签名: 允许定义对象可以具有任意数量的属性,这些属性的键和类型是可变的,常用于描述类型不确定的属性(具有动态属性的对象);
1 | // 限制person对象必须有name属性,可选age属性,但值必须是数字,同事可以有任意数量,任意类型的其他属性 |
1 | // 限制person对象必须有name属性,可选age属性,但值必须是数字,同事可以有任意数量,任意类型的其他属性 |
声明函数类型
1 | let count: (a: number, b: number) => number; |
TypeScript
中的=>
在函数类型声明时表示函数类型,描述其参数类型和返回类型JavaScript
中的=>
是定义箭头函数的语法,是具体的函数实现.- 函数类型声明还可以使用: 接口,自定义类型等方式,下文中会详细讲解.
声明数组类型
1 | let arr1: string[]; |
Array<number>
是泛型,后面会详细讲解
tuple
元组(
Tuple
)是一种特殊的数组类型,可以存储固定数量的元素,并且每个元素的类型是已知的且可以不同.元素用于精确描述一组值的类型,
?
表示可选元素.
1 | // 第一个元素必须是string类型,第二个元素必须是number类型 |
enum
枚举(
enum
)可以定义一组命名常量,它能增强代码的可读性,也让代码更好维护.
如下代码的功能是:根据调用walk
时传入的不同参数,执行不同的逻辑,存在的问题是调用walk
是传参时没有任何提示,编码者很容易写错字符串内容;并且用于判断逻辑的up
,down
,left
,right
是连续且相关的一组值,那此时就特别适合用枚举(enum
)
1 | function walk(str: string) { |
- 数字枚举
数字枚举是一种最常见的枚举类型,其成员的值会自动递增,且数字枚举还具备反向映射的特点,在下面的代码的打印中,不难发现:可以通过值来获取对应的枚举成员名称
1 | enum Direction { |
也可以指定枚举成员的初始值,其后的成员值会自动递增
1 | enum Direction { |
- 字符串枚举
1 | enum Direction { |
- 常量枚举
官方描述: 常量枚举是一种特殊的枚举类型,它使用
const
关键字定义,在编译时会被内敛,避免生成一些额外的代码.何为编译时内联?
所谓”内联”,其实就是
TypeScript
在编译时,会将枚举成员引用替换为他们的实际值,而不是生成额外的枚举对象.这可以减少生成的JavaScript
代码量,并提高运行时的性能.
使用const
定义枚举类型后,
1 | const enum Direction { |
编译后的JavaScript
1 | ; |
type
type
可以为任意类型创建别名,让代码更简洁,可读性更强,同时能更加方便地进行类型复用和扩展.
- 基本用法
类型别名使用type
关键字定义,type
后面跟类型名称,例如下面代码中num
是类型别名.
1 | type num = Number; |
- 联合类型
联合类型是一种高级类型,它表示一个值可以是几种不同类型之一.
1 | type Status = number | string; |
- 交叉类型
交叉类型(Intersection Types) 允许将多个类型合并为一个类型.合并后的类型将拥有所有被合并类型的成员.交叉类型通常用于对象类型.
1 | // 面积 |
一个特殊情况
先来观察如下两段代码
代码段1(正常)
在函数定义时,限制函数返回值为void
,那么函数的返回值就必须是空.
1 | // 无警告 |
代码段2(特殊)
使用类型声明限制函数的返回值为void
时,TypeScript
并不会严格要求函数返回空.
1 | type LogFunc = () => void; |
为什么会这样
是为了确保如下代码成立,我们知道Array.prototype.push
的返回值是一个数字,而Array.prototype.forEach
方法期望其回调函数返回类型是void
1 | const src = [1, 2, 3]; |
官方文档的说明: Assignability of Functions
复习类的相关知识
1 | class Person { |
属性修饰符
修饰符 | 含义 | 具体规则 |
---|---|---|
public |
公开的 | 可以被: 类内部,子类,类外部访问 |
protected |
受保护的 | 可以被: 类内部,子类访问 |
private |
私有的 | 可以被: 类内部访问 |
readonly |
只读属性 | 属性无法修改 |
属性简写
1 | class Person1 { |
抽象类
- 概述: 抽象类是一种无法被实例化的类,专门用来定义类的结构和行为,类中可以写抽象方法,也可以写具体实现.抽象类主要用来为其派生类提供一个基础结构,要求派生类必须实现其中的抽象方法.
- 简记: 抽象类不能实例化,其意义是可以被继承,抽象类里可以有普通方法,也可以有抽象方法.
通过一下场景,理解抽象类
我们定义一个抽象类
Package
,表示所有包裹的基本结构.任何包裹都有重量属性weight
,包裹都需要计算运费.但不同类型的包裹(如: 标准速度,特快专递)都有不同的运费计算方式,因此用于计算运费的calculate
是一个抽象方法,必须由具体的子类来实现.
1 | abstract class Package { |
StandardPackage
继承了Package
,实现了calculate
方法
1 | abstract class Package { |
总结: 何时使用抽象类
- 定义通用接口: 为一组相关的类定义通用行为(方法或属性)时.
- 提供基础实现: 在抽象类中提供某些方法或为其提供基础实现,这样派生类就可以继承这些实现.
- 确保关键实现: 强制派生类实现一些关键行为.
- 共享代码和逻辑: 当多个类需要共享部分代码时,抽象类可以避免代码重复.
interface(接口)
interface
是一种定义结构的方式,主要作用是为类,对象,函数等规定一种契约,这样可以确保代码的一致性和类型安全.但要注意interface
只能定义格式,不能包含任何实现
定义类的结构
1 | interface IPerson { |
定义对象的结构
1 | interface User { |
定义函数结构
1 | interface CountInterface { |
接口之间的继承
1 | interface IPerson { |
接口自动合并(可重复定义)
1 | // 接口的自动合并 |
总结: 何时使用接口
- 定义对象的格式: 描述数据模型,API响应格式,配置对象等等,是开发中用的最多的场景.
- 类的契约: 规定一个类需要实现哪些属性和方法.
- 自动合并: 一般用于扩展第三方库的类型,这种特性在大型项目中可能会用到.
interface和type的区别
- 相同点:
interface
和type
都可以用于定义对象结构,两者在许多场景中是可以互换的.- 不同点:
interface
: 更加专注于定义对象和类的结构,支持继承,合并.type
: 可以定义类型别名,联合类型,交叉类型,但不支持继承和自动合并.
interface和抽象类的区别
- 相同点: 都用于定义一个类的格式(应该遵循的契约)
- 不同点:
- 接口: 只能描述结构,不能有任何实现代码,一个类可以实现多个接口
- 抽象类: 既可以包含抽象方法,也可以包含具体方法,一个类只能继承一个抽象类.
泛型
泛型允许我们在定义函数,类或接口时,使用类型参数来表示未指定的类型,这些参数在具体使用时,才被指定具体的类型,泛型能让同一段代码适应于多种类型,同时仍然保持类型的安全性.
举例: 如下代码中的<T>
就是泛型(不一定非叫T
),设置泛型后即可在函数中使用T
来表示该类型.
泛型函数
1 | function logData<T>(data: T): T { |
泛型可以有多个
1 | function logData<T, U>(data1: T, data2: U): T | U { |
泛型接口
1 | interface IPerson<T> { |
类型声明文件
类型声明文件是
TypeScript
中的一种特殊文件,通常以.d.ts
作为扩展名.它的主要作用是为现有的JavaScript代码提供类型信息,使得TypeScript能够在使用这些JavaScript库或模块时进行类型检查和提示.
装饰器
简介
- 装饰器的本质是一种特殊的函数,他可以对:类,属性,方法,参数进行扩展,同时能让代码更简洁.
- 装饰器自
2015
年在ECMAScript-6
中被提出到现在,已10年. - 截止目前,装饰器依然是实验性特性,需要开发者手动配置