【TypeScript入坑】TypeScript 的复杂类型「Interface 接口、class类、Enum枚举、Generics泛型、类型断言」

TypeScript入坑


Interface 接口

🔔
interfacetype 相类似,但不完全一致,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

在你的枚举中,Up0Down1,接下来你定义了 Left = '西'Right = '东'。由于 LeftRight 是字符串枚举成员,不会参与数字递增,因此:

  • 枚举中不存在 Direction[2],所以 Direction[2] 返回 undefined
    🏆 解决该问题的关键点:

  • 数字枚举支持反向映射,即可以通过数字查找成员名称。

  • 字符串枚举不支持反向映射,只能通过枚举名来访问值。

常量枚举

你可以使用 constenum 关键字组合,声明一个常量枚举。 常量枚举中不允许使用计算值(变量或表达式)

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" 

约束泛型

假设有这样一个场景: 有时想操作某类型的一组值,并且我们知道这组值具有什么样的属性。

下例中,我们想访问 arglength 属性,但是编译器并不能推断每次传入的参数都有 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); // 报错

使用注意点

  • 尽量少用泛型。

  • 类型参数越少越好。

  • 类型参数需要出现两次。

    typescript 复制代码
    function greet<Str extends string>(s: Str) {
      console.log("Hello, " + s);
    }
    // 等价于
    function greet(s: string) {
      console.log("Hello, " + s);
    }
  • 泛型可以嵌套。

    typescript 复制代码
    type 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是类型断言,它们必须满足下面的条件:exprT的子类型,或者Texpr的子类型。

也就是说,类型断言要求实际的类型与断言的类型兼容,实际类型可以断言为一个更加宽泛的类型(父类型),也可以断言为一个更加精确的类型(子类型),但不能断言为一个完全无关的类型。

as const断言

如果没有声明变量类型,let 命令声明的变量,会被类型推断为 TypeScript 内置的基本类型之一const 命令声明的变量,则被推断为值类型常量

typescript 复制代码
// 类型推断为基本类型 string
let s1 = "JavaScript";

// 类型推断为字符串 "JavaScript"
const s2 = "JavaScript";

非空断言

对于那些可能为空的变量(即可能等于undefinednull),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");
}

assertsis 都是关键词,这里的意思是该函数用来断言参数value的类型是string。


namespace

namespace 是一种将相关代码组织在一起的方式,中文译为"命名空间"。

它出现在 ES 模块诞生之前,作为 TypeScript 自己的模块格式而发明的。但是,自从有了 ES 模块,官方已经不推荐使用 namespace 了。

相关推荐
四岁半儿1 小时前
常用css
前端·css
你的人类朋友2 小时前
说说git的变基
前端·git·后端
姑苏洛言2 小时前
网页作品惊艳亮相!这个浪浪山小妖怪网站太治愈了!
前端
字节逆旅2 小时前
nvm 安装pnpm的异常解决
前端·npm
Jerry2 小时前
Compose 从 View 系统迁移
前端
四维碎片2 小时前
【Qt】线程池与全局信号实现异步协作
开发语言·qt·ui·visual studio
IT码农-爱吃辣条2 小时前
Three.js 初级教程大全
开发语言·javascript·three.js
GIS之路2 小时前
2025年 两院院士 增选有效候选人名单公布
前端
四岁半儿2 小时前
vue,H5车牌弹框定制键盘包括新能源车牌
前端·vue.js
烛阴3 小时前
告别繁琐的类型注解:TypeScript 类型推断完全指南
前端·javascript·typescript