Skip to content

常用类型

any

any的含义是任意类型, 一旦将变量限制为any, 那就意味着放弃了对该变量的类型限制

注意

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

ts
// 显式的any: 明确声明 a 是 any
let a: any
a = 100
a = 'Halo'
a = false

// 隐式的any: 没有明确的表示 b 的类型是 any, TS 主动推断出的 any
let b

b = 100
b = 'Halo'
b = false

// 注意: any 类型的变量可以赋值给任意类型的变量
let x: string

x = a // 无警告
console.log(x) // false

unknown

unknown的含义是未知类型

  • unknown可以理解为一个类型安全的any, 适用于不确定数据的具体类型
ts
// 设置 a 的类型为 unknown
let a: unknown

// 以下对 a 赋值均正常
a = 100
a = 'Halo'
a = false

// 设置 x 的数据类型为 string
let x: string

x = a // 警告: 不能将类型“unknown”分配给类型“string”。

// 第一种方法: 判断类型
if (typeof a === 'string') {
  x = a
}

// 第二种方法: 类型断言
x = a as string
x = <string>a
  • unknown会强制开发者在使用之前进行类型检查, 从而提供更强的类型安全性
ts
let strA: string
strA = 'Halo'
strA.toUpperCase() // 无警告

let strB: any
strB = 'Halo'
strB.toUpperCase() // 无警告

let strC: unknown
strC = 'Halo'
strC.toUpperCase() // 警告: “strC”的类型为“未知”

// 使用类型断言强制指定 strC 的类型为 string
(strC as string).toUpperCase() // 无警告

never

never的含义是任何值都不是, 简而言之就是不能有值, undefined, null, '', false, 0都不行

  • 几乎不用never去直接限制变量, 因为没有意义
ts
// 指定 a 的类型为 never, 那就意味着 a 以后不能赋值任何数据
let a: never

// 以下对 a 的所有赋值都会有警告
a = 1
a = false
a = ''
a = undefined
a = null
  • never一般是TS主动推断出来的
ts
// 指定 x 的类型为 string
let x: string
x = 'Halo'

if (typeof x === 'string') {
  console.log(x.toUpperCase())
} else {
  console.log(x) // TS 会推断出此处的 x 是 never, 因为没有任何一个值符合此处的逻辑
}
  • never可用于限制函数的返回值
ts
// 限制 throwError 函数不需要用任何返回值, 任何值都不行
function throwError(str: string): never {
  throw new Error(`程序异常退出: ${str}`)
}

void

  • void通常用于函数返回值, 含义是表示函数不返回任何值, 调用者也不应依赖其返回值进行任何操作

注意

如果函数中没有return去指定函数的返回值, 那么函数是没有显式返回值的, 但会有一个隐式返回值, 就是undefined

虽然logMessage函数的返回类型为void, 但也可以接受undefined

即: undefinedvoid可以接受的一种空值

ts
function logMessage(msg: string): void {
  console.log(msg)
}
logMessage('Halo')
  • 以下写法均符合规范
ts
// 无警告
function logMessageA(msg: string): void {
  console.log(msg)
}

// 无警告
function logMessageB(msg: string): void {
  console.log(msg)
  return
}

// 无警告
function logMessageC(msg: string): void {
  console.log(msg)
  return undefined
}
  • 限制函数返回值voidundefined的区别
ts
function demoA(): void {
  console.log('@')
}
let resA = demoA()
if (resA) { // 警告: 无法测试 "void" 类型的表达式的真实性

}

function demoB(): undefined {
  console.log('@')
}
let resB = demoB()
if (resB) { // 无警告

}

void 和 undefined

  • void是一个广泛的概念, 用来表达空值, 而undefined则是这种空值的具体实现之一, 因此可以说undefinedvoid能接受的状态的一种具体形式
  • void包含undefined, 但void表达的语义超越了单纯的undefined, 它是一种意图上的约定, 而不仅仅是特定值的限制

void 总结

若函数返回类型为void

  • 从语义上讲, 函数是可以返回undefined的, 至于显式返回还是隐式返回无所谓
  • 函数调用者不应关心函数的返回值, 也不应依赖返回值进行任何操作, 即使返回了undefined

object

关于小写object和大写Object, 实际开发中使用相对较少, 因为范围太大了

小写 object

object的含义是: 所有非原始类型, 可存储对象, 函数, 数组等, 由于限制的范围比较宽泛, 在实际开发中使用相对较少

ts
// a 能存储的类型是 非原始类型
let a: object;

// 以下代码将 非原始类型 赋值给 a, 所以均符合要求
a = {};
a = { name: "张三" };
a = [1, 2, 3];
a = function () {};
a = new String("123");
class Person {}
a = new Person();

// 以下代码将 原始类型 赋值给 a, 有警告
a = 1; // 不能将类型“number”分配给类型“object”
a = true; // 不能将类型“boolean”分配给类型“object”
a = "Halo"; // 不能将类型“string”分配给类型“object”
a = null; // 不能将类型“null”分配给类型“object”
a = undefined; // 不能将类型“undefined”分配给类型“object”

大写 Object

  • 官方描述: 所有可以调用Object方法的类型
  • 简单记忆: 除了undefinednull的任何值
  • 由于限制的范围太大了, 所以实际开发中使用频率较低
ts
// b 能存储的类型是可以调用到 Object 方法的类型
let b: Object

// 以下代码将 Object 的实例对象赋值给 a, 均无警告
b = {};
b = { name: "张三" };
b = [1, 2, 3];
b = function () {};
b = new String("123");
class PersonB {}
b = new PersonB();
b = 1;
b = true;
b = "Halo";

// null 和 undefined 不是 Object 的实例对象, 会有警告
b = null // 不能将类型“null”分配给类型“Object”
b = undefined // 不能将类型“undefined”分配给类型“Object”

声明对象类型

  • 实际开发中限制一般对象, 通常使用以下形式
ts
// 声明对象类型 person 必须有 name 和 age 属性, gender 为可选属性
let personA: { name: string, age: number, gender?: string }

personA = { name: '张三', age: 18 }

// 含义同上, 可以用 ; 分号分隔
let personB: { name: string; age: number; gender?: string }

// 含义同上, 可以用换行分隔
let personC: {
  name: string
  age: number
  gender?: string
}
  • 索引签名: 允许定义对象可以具有任意数量的属性, 这些属性的键和类型是可变的, 常用于描述类型不确定的属性, 具有动态属性的对象
ts
// 声明对象类型 person 必须有 name 和 age 属性, gender 为可选属性, 同时可以有任意数量, 任意类型
let personD: {
  name: string
  age: number
  gender?: string
  [key: string]: any // 索引签名, 可以将 key 替换为其他单词
}

personD = {
  name: '李四',
  age: 20,
  gender: '男',
  score: 60,
  email: '123@xx.com'
}

声明函数类型

  • TS中的=>在函数类型声明时表示函数类型, 描述其参数类型返回类型
  • JS中的=>是一种定义函数的语法, 是具体的函数实现
  • 函数类型声明还可以使用接口, 自定义类型等方式
ts
let count: (a: number, b: number) => number

count = function(x, y) {
  return x + y
}

声明数组类型

ts
let arrA: string[]
arrA = ['a', 'b', 'c']

let arrB: Array<number> // <number> 泛型指定类型
arrB = [1, 2, 3]

tuple

元组tuple是一种特殊的数组类型, 可以存储固定数量的元素, 并且每个元素的类型是已知的且可以不同 元组用于精确描述一组值的类型, ?表示可选元素

ts
// 第一个元素必须是 string 类型, 第二个元素必须是 number 类型
let arrA: [string, number]
// 第一个元素必须是 number 类型, 第二个元素是可选的, 如果有则必须是 boolean 类型
let arrB: [number, boolean?]
// 第一个元素必须是 number 类型, 后面的元素可以是任意数量的 string 类型
let arrC: [number, ...string[]]

// 赋值
arrA = ['123', 321]
arrB = [100]
arrB = [200, false]
arrC = [10, '1', '2', '3']
arrC = [20]

// 不可以赋值, arrA 声明是两个元素, 赋值是三个
arrA = ['Halo', 123, false] // 不能将类型“[string, number, boolean]”分配给类型“[string, number]”

enum

枚举enum可以定义一组命名常量, 枚举可以增强代码的可读性, 让代码更好维护

ts
/**
 * 根据调用 walk 函数时传入的不同参数,执行不同的逻辑,存在的问题是调用 walk 传参时没有任何提示,开发者很容易写错字符串内容
 * 并且用于判断逻辑的 up down left right 是连续且相关的一组值,此时就特别适合使用枚举
 */

function walk(str: string) {
  switch (str) {
    case "up":
      console.log("向上");
      break;
    case "down":
      console.log("向下");
      break;
    case "left":
      console.log("向左");
      break;
    case "right":
      console.log("向右");
      break;
    default:
      console.log('未知方向');
      break;
  }
}

walk('up')
walk('down')
walk('left')
walk('right')

数字枚举

数字枚举是最常见的枚举类型, 其成员的值会自动递增, 且数字枚举还具备反向映射的特点, 可以通过值来获取对应的枚举成员名称

ts
// 定义枚举
enum Direction {
  UP,
  DOWN,
  LEFT,
  RIGHT
}

// 打印信息
console.log(Direction)
/*
  {
    0: 'UP',
    1: 'DOWN',
    2: 'LEFT',
    3: 'RIGHT',
    'UP': 0,
    'DOWN': 1,
    'LEFT': 2,
    'RIGHT': 3
  }
*/

// 反向映射
console.log(Direction.UP) // 0
console.log(Direction[0]) // UP

// 无法为“UP”赋值,因为它是只读属性
Direction.UP = 'UPUP'
  • 可以指定枚举成员的初始值, 其后的成员值会自动递增
ts
enum Direction {
  UP = 6,
  DOWN,
  LEFT,
  RIGHT
}

console.log(Direction.UP) // 6
console.log(Direction.DOWN) // 7
  • 使用枚举优化上例walk函数, 代码更加直观易读, 而且类型安全, 同时也更易于维护
ts
enum Direction {
  UP,
  DOWN,
  LEFT,
  RIGHT
}

function walk(str: Direction) {
  switch (str) {
    case Direction.UP:
      console.log("向上");
      break;
    case Direction.DOWN:
      console.log("向下");
      break;
    case Direction.LEFT:
      console.log("向左");
      break;
    case Direction.RIGHT:
      console.log("向右");
      break;
    default:
      console.log('未知方向');
      break;
  }
}

字符串枚举

枚举成员的值是字符串

注意

字符串枚举会丢失反向映射

ts
// 字符串枚举
enum DirectionString {
  UP = 'UP',
  DOWN = 'DOWN',
  LEFT = 'LEFT',
  RIGHT = 'RIGHT'
}

let dir: DirectionString = DirectionString.UP
console.log(dir) // UP

常量枚举

官方描述: 常量枚举是一种特殊枚举类型, 它使用const关键字定义, 在编译时会被内联, 避免生成一些额外的代码

何为编译时内联?

所谓内联就是TS在编译时, 会将枚举成员引用替换为它们的实际值, 而不是生成额外的枚举对象, 这样可以减少生成的JS代码量, 并且提高运行时的性能

ts
// 常量枚举 编译为 js 时会少很多代码
const enum DirectionConst {
  UP,
  DOWN,
  LEFT,
  RIGHT
}

console.log(DirectionConst.UP)

type

type可以为任意类型创建别名, 让代码更简洁, 可读性更强, 同时能更方便的进行类型复用和扩展

type 基本用法

类型别名使用type关键字定义, type后跟类型名称, 如下代码中num是类型别名

ts
type num = number

let age: num
age = 18

type 联合类型

联合类型是一种高级类型, 表示一个值可以是几种不同类型之一, 类似逻辑或

ts
// 联合类型
type Status = number | string
let a: Status = 100
let b: Status = '200'

type Gender = '男' | '女'
let x: Gender = '男'
let y: Gender = '女'

type 交叉类型

交叉类型Intersection Types允许将多个类型合并为一个类型, 合并后的类型将拥有所有被合并类型的成员, 交叉类型通常用于对象类型, 类似逻辑且

ts
// 交叉类型
// 面积
type Area = {
  width: number; // 宽
  height: number; // 高
}

// 地址
type Address = {
  num: number // 楼号
  cell: number // 单元号
  room: string // 房间号
}

type House = Area & Address

const house: House = {
  width: 30,
  height: 3,
  num: 1,
  cell: 2,
  room: '3'
}

一种特殊情况

先观察如下两段代码

  • 代码段1(正常): 在函数定义时, 限制函数返回值为void, 那么函数的返回值就必须是空
ts
function demo(): void {
  // 返回 undefined 合法
  return undefined

  // 以下返回均不合法
  return 100
  return false
  return null
  return []
}
  • 代码段2(特殊): 使用类型声明限制函数返回值为void时, TS并不会严格要求函数返回空
ts
type LogFunc = () => void
const f1: LogFunc = () => {
  return 100 // 允许返回非空值
}

const f2: LogFunc = () => 200 // 允许返回非空值

const f3: LogFunc = function() {
  return 300 // 允许返回非空值
}

let res = f1()
if (res) { // 警告: 无法测试 "void" 类型的表达式的真实性

}

为什么会这样?

是为了确保如下代码成立, Array.prototype.push方法的返回是一个数字, 而Array.prototype.forEach方法期望其回调的返回类型是void

为了兼容JS箭头函数简写时会默认return的语法格式

ts
const src = [1,2,3]
const dst = [0]
src.forEach(el => dst.push(el))

类相关知识

ts
class Person {
  // 属性声明
  name: string
  age: number
  // 构造器
  constructor(name: string, age: number){
    this.name = name
    this.age = age
  }
  // 方法
  speak() {
    console.log(`我叫${this.name}, 今年${this.age}岁`)
  }
}

const man = new Person('张三', 18)
console.log(man)
man.speak()

类的继承

ts
class Student extends Person {
  grade: string
  constructor(name: string, age: number, grade: string){
    // 调用父类构造器
    super(name, age)
    this.grade = grade
  }
  // 方法
  study() {
    console.log(`${this.name}正在努力学习中......`)
  }
  // 重写父类方法
  override speak() {
    console.log(`我是学生, 我叫${this.name}, 今年${this.age}岁, 在读${this.grade}年级`)
  }
}

const stu = new Student('李四', 12, '初三')
console.log(stu)
stu.speak()
stu.study()

属性修饰符

修饰符含义具体规则
public公开的类内部, 子类, 类外部可以访问
protected受保护的类内部, 子类可以访问
private私有的类内部可以访问
readonly只读属性属性无法修改

public 修饰符

ts
class Person {
  // 属性声明
  public name: string
  public age: number
  constructor(name: string, age: number){
    this.name = name
    this.age = age
  }
  public speak() {
    // 类内部访问
    console.log(`我叫${this.name}, 今年${this.age}岁`)
  }
}

class Student extends Person {
  study() {
    // 子类访问
    console.log(`${this.name}正在努力学习中......`)
  }
}

const man = new Person('张三', 18)
// 类外部访问
console.log(man.name)

属性的简写形式

注意

简写必须要写修饰符

ts
class Person {
  /* 简写前
  public name: string
  public age: number
  constructor(name: string, age: number){
    this.name = name
    this.age = age
  }
  */

  // 简写后
  constructor(public name: string, public age: number) {
    
  }
}

protected 修饰符

ts
class Person {
  constructor(
    protected name: string,
    protected age: number
  ) {}
  protected getDetail() {
    // 类内部可以访问
    return `我叫${this.name}, 今年${this.age}岁`
  }
  introduce() {
    console.log(this.getDetail())
  }
}

const man = new Person('Tom', 18)

// 类外部无法访问
man.name
man.age
man.getDetail()

class Student extends Person {
  study() {
    this.introduce()
    // 子类可以访问
    console.log(`${this.name}正在学习...`)
  }
}

private 修饰符

ts
class Person {
  constructor(
    public name: string,
    public age: number,
    private IDCard: string
  ) {}
  getInfo() {
    return `我叫${this.name}, 今年${this.age}岁`
  }
  private getPrivateInfo() {
    // 类内部可以访问
    return `身份证号码: ${this.IDCard}`
  }
  getFullInfo() {
    return `${this.getInfo()}, ${this.getPrivateInfo()}`
  }
}

const man = new Person('Tom', 18, '110114567544323454')
// 类外部无法访问
man.IDCard
man.getPrivateInfo()

class Student extends Person {
  study() {
    // 子类无法访问
    console.log(this.IDCard)
    this.getPrivateInfo()
  }
}

readonly 修饰符

ts
class Person {
  constructor(
    public name: string,
    readonly age: number
  ) {}
}

const man = new Person('张三', 18)
man.name = '李四'
man.age = 20 // 无法为“age”赋值,因为它是只读属性

抽象类

概述: 抽象类是一种无法被实例化的类, 专门用来定义类的结构和行为, 类中可以写抽象方法, 也可以写具体实现, 抽象类主要用来为其派生类提供一个基础结构, 要求其派生类必须实现其中的抽象方法

简记: 抽象类不能实例化, 其意义是可以被继承, 抽象类里可以用普通方法, 也可以有抽象方法

通过以下场景理解抽象类

  • 定义一个抽象类Package, 表示所有包裹的基本结构, 任何包裹都有重量属性weight, 包裹都需要计算运费, 但是不同类型的包裹(标准, 特快)有不同的运费计算方式, 因此用于计算运费的caleculate方法是一个抽象方法, 必须由具体的子类来实现
ts
// 声明抽象类
abstract class Package {
  // 构造器
  constructor(public weight: number) {}
  // 声明抽象方法
  abstract calculate(): number
  // 具体方法
  printPackage() {
    console.log(`包裹重量为: ${this.weight}kg, 运费为: ${this.calculate()}元`)
  }
}
  • 标准StandardPackage类继承了Package, 实现caleculate方法
ts
// 标准
class StandardPackage extends Package {
  constructor(
    weight: number,
    public unitPrice: number
  ) {
    super(weight)
  }
  calculate(): number {
    return this.weight * this.unitPrice
  }
}

const sp = new StandardPackage(10, 5)
sp.printPackage()
  • 特快ExpressPackage类继承了Package, 实现caleculate方法
ts
// 特快
class ExpressPackage extends Package {
  constructor(
    weight: number,
    public unitPrice: number,
    public additional: number,
  ) {
    super(weight)
  }
  calculate(): number {
    if (this.weight > 10) {
      return 10 * this.unitPrice + (this.weight - 10) * this.additional
    }
    return this.weight * this.unitPrice
  }
}

const ep = new ExpressPackage(16, 10, 2)
ep.printPackage()

抽象类总结

何时使用抽象类?

  1. 定义通用接口: 为一组相关的类定义通用的行为, 如方法或属性
  2. 提供基础实现: 在抽象类中提供某些方法或为其提供基础实现, 这样派生类就可以继承这些实现
  3. 确保关键实现: 强制派生类实现一些关键行为
  4. 共享代码和逻辑: 当多个类需要共享部分代码时, 抽象类可以避免代码重复

接口

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

定义类结构

ts
// PersonInterface 接口
interface PersonInterface {
  name: string
  age: number
  speak(n: number): string
}

class Person implements PersonInterface {
  constructor(
    public name: string,
    public age: number
  ) {}

  speak(n: number): string {
    console.log(n)
    return `我叫${this.name}, 今年${this.age}岁`
  }
}

const man = new Person('Tom', 18)
man.speak(3)

定义对象结构

ts
// UserInterface 接口
interface UserInterface {
  name: string
  age: number
  gender?: string
  readonly IDCard: string
  run: (n: number) => void
}

const user: UserInterface = {
  name: 'Tom',
  age: 18,
  gender: '男',
  IDCard: '110123341111',
  run(n) {
    for (let i = 0; i < n; i++) {
      console.log(`跑`)
    }
  }
}

定义函数结构

ts
// CountInterface 接口
interface CountInterface {
  (a: number, b: number): number
}

const count: CountInterface = (x, y) => {
  return x + y
}

count(3, 9)

接口之间的继承

ts
// PersonInterface 接口
interface PersonInterface {
  name: string,
  age: number
}

// StudentInterface 接口继承 PersonInterface 接口
interface StudentInterface extends PersonInterface {
  grade: string
}

const stu: StudentInterface = {
  name: '张三',
  age: 18,
  grade: '3'
}

接口自动合并(可重复定义)

ts
// PersonInterface 接口
interface PersonInterface {
  name: string,
  age: number
}

// 自动跟上面的合并
interface PersonInterface {
  gender: string
}

const p: PersonInterface = {
  name: '李四',
  age: 18,
  gender: '男'
}

接口总结

何时使用接口?

  1. 定义对象的格式: 描述数据模型, API响应格式, 配置对象等, 是实际开发中使用较多的场景
  2. 类的契约: 规定一个类需要实现哪些属性和方法
  3. 自动合并: 一般用于扩展第三方库的类型, 这种特性在大型项目中可能会用到

一些相似概念的区别

interface 和 type 的区别

  • 相同点: interfacetype都可以用于定义对象结构, 两者在许多场景中是可以互换的
  • 不同点:
    1. interface更专注于定义对象和类的结构, 支持继承, 合并
    2. type可以定义类型别名, 联合类型, 交叉类型, 但不支持继承和合并

interface 和抽象类的区别

  • 相同点: 都用于定义一个类的格式, 应该遵循的契约
  • 不同点:
    1. 接口: 只能描述结构, 不能有任何实现代码, 一个类可以实现多个接口
    2. 抽象类: 既可以包含抽象方法, 也可以包含具体方法, 一个类只能继承一个抽象类