TypeScript入坑
Interface 接口
🔔
interface
和type
相类似,但不完全一致,type
可以校验基础类型,而Interface
不支持基础类型的校验。
简介
interface
是对象的模板,可以看作是一种类型约定,中文译为"接口"。使用了某个模板的对象,就拥有了指定的类型结构。
TS 里,能使用 Interface 的话就使用 Interface。
- 接口只是 TS 帮助我们校验的工具,并不会变成 JS
- 属性前加上
?
,代表该变量可有可无 - 属性前加上
readonly
,代表只读不可修改
typescript
interface Pereson {
name: string
age?: number
readonly test: string
}
const getPersonName = (person: Pereson) => {
console.log(person.name)
}
const setPersonName = (person: Pereson, name: string): void => {
person.name = name
}
const person = {
name: 'simon',
age: 18,
sex: 'male'
}
getPersonName(person)
setPersonName(person, 'lin')
接口合并
多个同名接口会合并成一个接口。
typescript
interface Box {
height: number;
width: number;
}
interface Box {
length: number;
}
TS 强校验
如果以字面量的形式传给函数,TS 会进行强校验。例如:
typescript
interface Pereson {
name: string
age?: number
}
const getPersonName = (person: Pereson) => {
console.log(person.name)
}
const setPersonName = (person: Pereson, name: string): void => {
person.name = name
}
const person = {
name: 'simon',
age: 18,
sex: 'male'
}
getPersonName({
name: 'simon',
age: 18,
sex: 'male'
}) // 会报错,sex不在 Person 种
setPersonName(person, 'lin')
// 修改 Interface 解决(最后一行代表的是,可以是任何字符串类型的键,任何值)
interface Pereson {
name: string
age?: number
[propName: string]: any
}
Interface 里支持方法的写入
typescript
interface Pereson {
name: string
age?: number
say(): string
}
const person = {
name: 'simon',
say() {
return 'say hello'
}
}
class 类应用接口
typescript
interface Pereson {
name: string
age?: number
say(): string
}
// 语法 implements
class User implements Pereson {
name = 'simon'
say() {
return 'say hello'
}
}
接口之间互相继承
typescript
interface Shape {
name: string;
}
// 关键字 extends
interface Circle extends Shape {
radius: number;
}
interface
允许多重继承,可以继承 type
的对象类型,也可以继承 class
。
接口定义函数
typescript
interface SayHi {
(word: string): string
}
const say: SayHi = (word: string) => {
return word
}
interface 与 type 的异同
type
能够表示非对象类型 ,而interface
只能表示对象类型(包括数组、函数等)interface
可以继承 其他类型,type
不支持继承(可以通过交叉类型实现继承)- 同名
interface
会自动合并 ,同名type
则会报错 interface
不能包含属性映射 (mapping),type
可以this
关键字 只能用于interface
type
可以扩展原始数据类型 ,interface
不行interface
无法表达某些复杂类型(比如交叉类型和联合类型)
综上所述,如果有复杂的类型运算,那么没有其他选择只能使用
type
;一般情况下,
interface
灵活性比较高,便于扩充类型或自动合并,建议优先使用。
小案例
公用属性使用 Interface 进行扩展:
typescript
interface Person {
name: string
}
interface Teacher extends Person {}
interface Student extends Person {
age: number
}
const teacher = {
name: 'simon'
}
const student = {
name: 'jon',
age: 18
}
const getUserInfo = (user: Person) => {
console.log(user.name)
}
getUserInfo(teacher) // simon
getUserInfo(student) // jon
class 类
类的定义与继承
typescript
// 类里写属性与方法
class Person {
name = 'simon'
getName() {
return this.name
}
}
const person = new Person()
console.log(person.getName()) // simon
// 继承类,继承类属于字类,被继承的属于父类
class Teacher extends Person {
getTeacherName() {
return 'simon Teacher'
}
// 子类可以重写父类的属性与方法
getName() {
// super 关键字指向了父类,可以直接调用父类。不会受到类重写的影响
return super.getName() + 'TTT'
}
}
const teacher = new Teacher()
console.log(teacher.getName()) // simonTTT
console.log(teacher.getTeacherName()) // simon Teacher
类的访问类型
访回类型:
private
:允许在类内使用protected
:允许在类内及继承的子类中使用public
:允许在类的内外调用(默认)
自带方法:
readonly
:只读属性static
:将方法挂载到类上而不是实例上
🔔
直接写在类里的属性或函数,相当于前面加了
public
typescript
class Person {
protected name: string = 'simon'
private age: number = 10
public sayHi() {
console.log('hi' + this.age)
}
}
class Teacher extends Person {
public sayBye() {
return this.name
}
}
const person = new Person()
person.sayHi() // hi 10
const teacher = new Teacher()
console.log(teacher.sayBye()) // simon
构造器 constructor
-
constructor
会在new
实例的时候自动执行typescript// 以下两段代码相同, constructor 里, 参数前加上public代表在之前已经声明过这个变量了 // 传统写法 class Person { public name: string constructor(name: string) { this.name = name } } const person = new Person('simon') // 简化写法 class Person { // public name: string constructor(public name: string) { // this.name = name } } const person = new Person('simon') console.log(person.name)
-
字类集成父类并使用
constructor
的话,必须先调用父类的constructor
,并按照父类的参数规则进行typescript// super()代表调用父类的 constructor // 如果父类没有使用constructor 字类需要调用一个空的super() class Person { constructor(public name: string) {} } class Teacher extends Person { constructor(public age: number) { super('zws') } } const teacher = new Teacher(28)
静态属性,Setter 和 Getter
- Getter:读
- Setter:写
typescript
// 可以通过getter访问私有属性,通过setter更改私有属性
// 一般用于对数据的加密
class Person {
constructor(private _name: string) {}
get name() {
return this._name + ' has'
}
set name(name: string) {
const realName = name.split(' ')[0]
this._name = realName
}
}
const person = new Person('simon')
console.log(person.name) // simon has
person.name = 'simon a has'
console.log(person.name) // simon a has
做个小案例
🙋 通过 TS 创建一个 Demo 类,这个类只能被调用一次
🤔 思路:
- 不能在外部以 new Demo 的形式创建一个实例(将 constructor 设置为私有属性)
- 使用 static (将方法挂载到类上而不是实例上)来实现
- 使用 instance 方法来保存传入的值,并判断
typescript
class Demo {
private constructor(public name: string) {}
private static instance: Demo
static getInstance(name: string) {
if (!this.instance) {
this.instance = new Demo(name)
}
return this.instance
}
}
const demo1 = Demo.getInstance('simon')
const demo2 = Demo.getInstance('sim')
console.log(demo1.name) // simon
console.log(demo2.name) // simon
抽象类
- 只能被继承,不能实例化
- 抽象类里的抽象方法,不能够写具体实现
typescript
abstract class Gemo {
width: number
getType() {
return 'Gemo'
}
abstract getArea(): number
}
class Cricle extends Gemo {
// 子类继承了抽象类,里面的抽象方法必须实现一下
getArea() {
return 123
}
}
class Square {}
class Triangle {}
Enum 枚举
简介
实际开发中,经常需要定义一组相关的常量。TypeScript 就设计了 Enum 结构,用来将相关常量放在一个容器里面,方便使用。
你可以使用常量 const
描述某种不会改变的值。但某些值是在一定范围内的一系列常量,如星期(周一~周日)、三原色(红黄蓝)、方位(东南西北)等,这种类型的值称为枚举。
Enum作为类型有一个缺点就是输入任何数值都不报错。
Enum成员的值
Enum每个成员的值都可以显式赋值,可以是任意数值,但不能是大整数(Bigint
)。
typescript
enum Color {
Red = 90,
Green = 0.5,
Blue = 7n, // 报错
}
Enum成员值不能重新赋值 ,通常我们会在 enum
关键字前加上 const
修饰、加上 const
后,经过编译后Enum成员会被替换为对应的值。
同名Enum的合并
- 多个同名的 Enum 结构会自动合并。
- Enum 结构合并时,只允许其中一个的首成员省略初始值,否则报错。
- 同名 Enum 合并时,不能有同名成员,否则报错。
- 同名 Enum 合并的另一个限制是,所有定义必须同为
const
枚举或者非const
枚举,不允许混合使用。
数值枚举
使用关键字 enum
来声明一个枚举类型数据。
枚举成员默认为数值类型
typescript
enum Direction {
Up,
Down,
Left,
Right
}
console.log(Direction.Up) // 0
console.log(Direction[0]) // "Up"
未赋初始值的枚举项会接着上个项的值进行递增。
使用 【🔧 tsc工具】 将上述代码编译为 js 后,可以发现 ts 使用 Direction[(Direction["Up"] = 0)] = "Up"
这样的内部实现对对象成员进行了双向的赋值。
由此可以看出,enum 具有双向映射的特点。
typescript
// ts 代码编译为 js 后
"use strict";
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 = {}));
console.log(Direction.Up); // 0
console.log(Direction[0]); // "Up"
字符串枚举
枚举成员为字符串时,其之后的成员也必须是字符串。
typescript
enum Direction {
Up, // 未赋值,默认为0
Down = '南',
Left = '西',
Right = '东'
}
🫵 小示例(猜猜结果&原因)
typescript
enum Direction {
Up,
Down,
Left = '西',
Right = '东'
}
console.log(Direction.Left) // "西"
console.log(Direction[2]) // 结果?undefined
🙋 为什么 Direction[1]
打印出来是 undefined
呢?
enum
的工作方式会根据枚举成员是否有显式的值赋予而有所不同。让我们详细分析上面的Direction
枚举:
枚举成员解释:
- Up 和 Down:
- 它们是数字枚举,没有显式赋值,因此会被自动分配递增的数字值:
- Up 默认值为 0。
- Down 自动递增为 1。
- Left 和 Right:
- 它们是字符串枚举,因为被显式赋值为 '西' 和 '东'。
在 TypeScript 中,数字枚举支持反向映射 ,即你可以通过索引(数字)访问枚举名称。例如,Direction[0]
会返回"Up"
,Direction[1]
会返回"Down"
。但是,字符串枚举不支持反向映射。当你在枚举中混合使用数字枚举和字符串枚举时,只有数字枚举支持这种通过数字索引进行反向查找的特性。
🤔 那 为什么Direction[2]
为undefined
?在你的枚举中,
Up
为0
,Down
为1
,接下来你定义了Left = '西'
和Right = '东'
。由于Left
和Right
是字符串枚举成员,不会参与数字递增,因此:
枚举中不存在
Direction[2]
,所以Direction[2]
返回undefined
。
🏆 解决该问题的关键点:数字枚举支持反向映射,即可以通过数字查找成员名称。
字符串枚举不支持反向映射,只能通过枚举名来访问值。
常量枚举
你可以使用 const
和 enum
关键字组合,声明一个常量枚举。 常量枚举中不允许使用计算值(变量或表达式)
typescript
let a: number = 123;
const enum Direction {
Up = a, // ✋ 报错:const enum member initializers must be constant expressions.(2474)
// const enum成员初始化器必须是常数表达。
// ⚠️ 报错原因:常量枚举中不允许使用计算值
// 枚举成员为字符串时,其之后的成员也必须是字符串
Down = '南',
Left = '西',
Right = '东'
}
keyof 运算符
keyof
运算符可以取出 Enum 结构的所有成员名,作为联合类型返回
typescript
enum MyEnum {
A = "a",
B = "b",
}
// 'A'|'B'
type Foo = keyof typeof MyEnum;
Generics 泛型
在 TypeScript 中,泛型(Generics)是一种在编写代码时不指定具体类型,而是在使用时再确定类型的机制。这使得你可以编写更加通用、灵活且可重用的代码,同时保持类型安全。
泛型的核心思想是参数化类型。通过在函数、类或接口中使用泛型,你可以编写与具体类型无关的代码,而让使用者在调用时提供具体的类型信息。这样一来,你的代码就可以适应多种类型的数据,而不必为每种类型都编写重复的逻辑。
typescript
// 定义Map时为接收两个类型参数的函数,在调用时指定类型
const nodePorts = new Map<string, string[]>([
['stm', ['stm-entry', 'stm-exit']],
]);
简介
泛型指的是,在定义函数、接口或类的时候不预先指定数据类型,而在使用时再指定类型的特性。
泛型可以提升应用的可重用性,如使用其创建组件,则可以使组件可以支持多种数据类型。
假如需要一个函数,返回传入它的内容 不使用泛型时,它会是这样的:
typescript
function echo(arg: any) {
return arg
}
但这样就丢失了一些信息:传入的类型与返回的类型应该是相同的。如果我们传入一个数字,我们只知道任何类型的值都有可能被返回。于是此时需要一种方法使返回值的类型与传入参数的类型是相同的。
类型变量
这是一种特殊的变量,只用于表示类型而不是值。 使用泛型改写上述函数:
typescript
// 类型变量也遵循标识符定义规范,写为T只是习惯上这么做
function echo<T>(arg: T): T {
return arg
}
这样,传入实参时,会同时将实参的类型传递给类型变量 T,同时返回值也是 T。
场景:交换两个数组元素
不使用泛型会丢失数据类型:
typescript
function swap(tuple) {
return [tuple[1], tuple[0]]
}
使用泛型后,不仅会保有类型推断,还可以直接调用实例的方法:
typescript
function swapGeneric<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]]
}
const result2 = swapGeneric(['string', 0.123])
// ts的类型推断系统能够明确得知第一个元素会是数值,而第二个元素会是字符串
result2[0].toFixed(2) // "0.12"
result2[1].toLocaleUpperCase() // "STRING"
约束泛型
假设有这样一个场景: 有时想操作某类型的一组值,并且我们知道这组值具有什么样的属性。
下例中,我们想访问 arg
的 length
属性,但是编译器并不能推断每次传入的参数都有 length
属性,于是就报错了。
typescript
function echoWithArray<T>(arg: T): T {
console.log(arg.length) // Error: T doesn't have .length
return arg
}
经过不充分的考虑后,我们尝试将类型变量指定为数组:
typescript
function echoWithArray<T>(arg: T[]): T[] {
console.log(arg.length)
return arg
}
此时你也许会发现,这样只能传入数组,不能传入同样具有 length
属性的字符串、类数组对象等。
我们可以定义一个接口来描述约束条件,然后由类型变量继承此接口实现泛型约束:
typescript
// 一些编程规范约定接口以大写 I 作为前缀
interface IWithLength {
length: number
}
function echoWithLength<T extends IWithLength>(arg: T): T {
console.log(arg.length)
return arg
}
// 只要包含 length 属性且为数值 均不会报错
const str1 = echoWithLength('str')
const obj = echoWithLength({ length: 10, width: 10 })
const arr2 = echoWithLength([])
泛型类
场景:定义一个类,能实现被 push 入的队列元素与 pop 出的元素的类型一致。
typescript
class Queue<T> {
private data = []
push(item: T) {
return this.data.push(item)
}
pop(): T {
return this.data.pop()
}
}
// 泛型类实例化时要指定具体的类型
const queue = new Queue<number>()
queue.push(1)
queue.push('str') // Error: 类型"string"的参数不能赋给类型"number"的参数。
泛型接口
万能的泛型同样可以用来描述接口:
typescript
interface KeyPair<T, U> {
key: T
value: U
}
// 泛型接口描述的对象,同样需要满足类型要求
let kp1: KeyPair<number, string> = { key: 123, value: 'str' }
let kp2: KeyPair<string, number> = { key: 'test', value: 123 }
// 描述函数的泛型接口
interface IPlus<T> {
// 函数应具有两个形参,和一个返回值,它们的类型相同
(a: T, b: T): T
}
function plus(a: number, b: number): number {
return a + b
}
function concat(a: string, b: string): string {
return a + b
}
参数类型不兼容:
类型参数的约束条件
TypeScript 提供了一种语法,允许在类型参数上面写明约束条件,如果不满足条件,编译时就会报错。这样也可以有良好的语义,对类型参数进行说明。
typescript
function comp<T extends { length: number }>(a: T, b: T) {
if (a.length >= b.length) {
return a;
}
return b;
}
上面实例中,T exitends { length: number }
就是约束条件,表示类型参数T
必须满足 { length: number }
,否则会报错。
typescript
comp([1, 2], [1, 2, 3]); // 正确
comp("ab", "abc"); // 正确
comp(1, 2); // 报错
使用注意点
-
尽量少用泛型。
-
类型参数越少越好。
-
类型参数需要出现两次。
typescriptfunction greet<Str extends string>(s: Str) { console.log("Hello, " + s); } // 等价于 function greet(s: string) { console.log("Hello, " + s); }
-
泛型可以嵌套。
typescripttype OrNull<Type> = Type | null; type OneOrMany<Type> = Type | Type[]; type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>;
类型断言
简介
对于没有类型声明的值,TypeScript会进行类型推断,很多时候得到的结果,未必是开发者想要的。
typescript
type T = "a" | "b" | "c";
let foo = "a";
let bar: T = foo; // 报错
TypeScript 提供了"类型断言 "这样一种手段,允许开发者在代码中"断言"某个值的类型,告诉编译器此处的值是什么类型。TypeScript 一旦发现存在类型断言,就不再对该值进行类型推断,而是直接采用断言给出的类型。
typescript
type T = "a" | "b" | "c";
let foo = "a";
let bar: T = foo as T; // 正确
类型断言有两种语法。
typescript
// 语法一:<类型>值
<Type>value;
// 语法二:值 as 类型
value as Type;
语法一因为跟JSX语法冲突,使用时必须关闭TypeScript的React支持,否则无法识别,由于这个原因,一般使用语法二。
类型断言的条件
类型断言的使用前提是,值的实际类型与断言的类型必须满足一个条件。
typescript
expr as T;
上面代码中,
expr
是实际的值,T
是类型断言,它们必须满足下面的条件:expr
是T
的子类型,或者T
是expr
的子类型。
也就是说,类型断言要求实际的类型与断言的类型兼容,实际类型可以断言为一个更加宽泛的类型(父类型),也可以断言为一个更加精确的类型(子类型),但不能断言为一个完全无关的类型。
as const断言
如果没有声明变量类型,let
命令声明的变量,会被类型推断为 TypeScript 内置的基本类型之一 ;const
命令声明的变量,则被推断为值类型常量。
typescript
// 类型推断为基本类型 string
let s1 = "JavaScript";
// 类型推断为字符串 "JavaScript"
const s2 = "JavaScript";
非空断言
对于那些可能为空的变量(即可能等于undefined
或null
),TypeScript 提供了非空断言,保证这些变量不会为空 ,写法是在变量名后面加上感叹号!
。
typescript
const root = document.getElementById("root")!;
上面示例中,
getElementById()
方法加上后缀!
,表示这个方法肯定返回非空结果。
断言函数
断言函数是一种特殊函数,用于保证函数参数符合某种类型。如果函数参数达不到要求,就会抛出错误,中断程序执行;如果达到要求,就不进行任何操作,让代码按照正常流程运行。
typescript
function isString(value) {
if (typeof value !== "string") throw new Error("Not a string");
}
为了更清晰地表达断言函数,TypeScript 3.7 引入了新的类型写法。
typescript
function isString(value: unknown): asserts value is string {
if (typeof value !== "string") throw new Error("Not a string");
}
asserts
、is
都是关键词,这里的意思是该函数用来断言参数value的类型是string。
namespace
namespace
是一种将相关代码组织在一起的方式,中文译为"命名空间"。
它出现在 ES 模块诞生之前,作为 TypeScript 自己的模块格式而发明的。但是,自从有了 ES 模块,官方已经不推荐使用 namespace 了。