Class

--strictPropertyInitialization

控制类的属性是否必须在构造函数中初始化(true if strict; false otherwise)

ts
class A {
  name: string // Error: Property 'name' has no initializer and is not definitely assigned in the constructor.
  constructor() {}
}

如果想要关闭这个检查,可以使用明确赋值断言运算符 ! 来告诉 TypeScript 该属性已经被初始化了

ts
class A {
  name!: string
  constructor() {}
}

只读属性

ts
class A {
  // 只读属性:只能在声明时或构造函数中初始化
  readonly name: string = 'world'
  constructor(otherName?: string) {
    if (otherName !== undefined) {
      this.name = otherName // 可以在构造函数中修改
    }
  }
}

构造函数

  • 可重载

    ts
    class Point {
      x: number = 0
      y: number = 0
    
      constructor(x: number, y: number)
      constructor(xy: string)
      constructor(x: number | string, y: number = 0) {}
    }
  • 没有参数类型 Parameters<>

  • 没有返回类型 ReturnType<>

  • 可使用参数属性

    ts
    class Point {
      constructor(readonly x: number, public y: number) {}
    }

方法

方法里面获取类的其他属性或者其他方法,需要使用 this

ts
let x: number = 0

class C {
  x: string = 'hello'

  m() {
    x = 'world' // Type 'string' is not assignable to type 'number'. 指的是外层的 x
  }
}

访问器属性 Getters / Setters

  • 如果只有 getter,则属性 readonly

  • 如果 setter 的参数类型没有指定,会自动从 getter 的返回值类型推断

  • setter 参数类型不匹配,也可以(from 4.3)

    ts
    class Thing {
      _size = 0
    
      get size(): number {
        return this._size
      }
    
      set size(value: boolean | number | string) {
        const num = Number(value)
        if (!Number.isFinite(num)) {
          this._size = 0
          return
        }
        this._size = num
      }
    }

索引类型

ts
class A {
  [s: string]: ((s: string) => boolean) | boolean

  check(s: string) {
    return this[s] as boolean
  }
}

成员可见性 public / private / protected

public 默认

可以在类的内部和外部访问,包括子类。可以被继承,可以被重写,可以省略 public 关键字

protected

可以在类的内部和子类内部访问,但是不能在外部访问。

可以被继承,可以被重写。重写时可以改变可见性,将属性或方法从 protected 改为其他,重新暴露出来

ts
class Greeter {
  public greet() {
    console.log(`Hello, ${this.getName()}`) // ok
  }

  protected getName() {
    return 'hi'
  }
}
class SpecialGreeter extends Greeter {
  public howdy() {
    console.log(`Howdy, ${this.getName()}`) // ok
  }
}
const g = new SpecialGreeter()
g.greet() // ok
g.getName() // error

跨层级保护访问(Cross-hierarchy protected access)

ts
class Base {
  protected x: number = 1
}
class Derived1 extends Base {
  protected x: number = 5
}
class Derived2 extends Base {
  f1(other: Derived2) {
    other.x = 10
  }

  f2(other: Derived1) {
    other.x = 10 // error 属性“x”受保护,只能在类“Derived1”及其子类中访问
  }
}

private

protected 更严格,只能在类的内部访问,包括子类内部。不能被继承,不能被重写

ts
class Base {
  private x = 0
}
class Derived extends Base {
  x = 1 // error 类“Derived”错误扩展基类“Base”。属性“x”在类型“Base”中是私有属性,但在类型“Derived”中不是。
  private x = 0 // error 类“Derived”错误扩展基类“Base”。类型具有私有属性“x”的单独声明。
}

"不能被重写" 这一点与js不同, es6 中可以重写私有属性

js
class Base {
  #x = 0
  #getX() {
    return this.x
  }
}
class Derived extends Base {
  #x = 1
  #getX() {
    return this.#x
  }
}

静态成员

  • 可以通过类构造函数对象本身访问

  • 静态成员也有 publicprotectedprivate 可见性

  • 静态成员不能有关键字 namelengthcall

静态块

原理同 static block ES2022

泛型类

ts
class Box<Type> {
  contents: Type
  constructor(value: Type) {
    this.contents = value
  }
}

const b = new Box('hello!') // b: Box<string>

静态成员不可以是泛型的

ts
class Box<Type> {
  static defaultValue: Type // error 静态成员不能引用类类型参数
}

静态成员本来就是属于类的,而不是属于实例的,所以不应该依赖于实例的类型参数

关于 this

箭头函数的 this

ts
class MyClass {
  name = 'MyClass'
  getName = () => {
    return this.name
  }
}
const c = new MyClass()
const g = c.getName
console.log(g()) // MyClass,箭头函数的 this 指向定义时的 this,如果 getName 是普通函数,则 this 为 undefined

上面的箭头函数虽然解决了this的问题,但是也有弊端

  • 会使用更多的内存,因为每个实例都会保存一份getName方法的拷贝

  • 不能在子类中使用 super.getName()

    ts
    class MyClass {
      name = 'MyClass'
      getName() {
        return this.name
      }
    }
    class MySubClass extends MyClass {
      name = 'MySubClass'
      getName = () => {
        return `${super.getName()}Sub`
      }
    }
    new MySubClass().getName() // MyClassSub

    改成箭头函数

    ts
    class MyClass {
      name = 'MyClass'
      // 此时可以把 getName 看成一个属性,而不是方法
      getName = () => {
        return this.name
      }
    }
    class MySubClass extends MyClass {
      name = 'MySubClass'
      getName = () => {
        return `${super.getName()}Sub` // error 父类定义的类字段“getName”无法通过 super 在子类中访问
      }
    }
    new MySubClass().getName()

this 参数

在方法或函数定义中,名为 this 的初始参数在 TypeScript 中具有特殊含义。这些参数在编译期间被删除:

ts
function fn(this: SomeType, x: number) {
  /* ... */
}
// JavaScript output
function fn(x) {
  /* ... */
}

此时我们不使用箭头函数,而是使用 this 参数,可以解决意外错误调用的问题

ts
class MyClass {
  name = 'MyClass'
  getName(this: MyClass) {
    return this.name
  }
}
const c = new MyClass()
const g = c.getName
console.log(g()) // error 类型为“void”的 "this" 上下文不能分配给类型为“MyClass”的方法的 "this"

this 类型

ts
class Box {
  contents: string = ''
  // 1. 这里参数里面出现了一个this类型,它表示Box类的实例类型
  //    如果被其他类继承,那么this类型会变成继承的类的实例类型
  sameAs(other: this) {
    return other.contents === this.contents
  }

  // 2. 这里出现了一个this类型,它表示Box类的实例类型
  set(value: string) { // (method) Box.set(value: string): this
    this.contents = value
    return this
  }
}
class DerivedBox extends Box {
  otherContent: string = '?'
}

const base = new Box()
const derived = new DerivedBox()
derived.sameAs(base) // error 类型“Box”的参数不能赋给类型“DerivedBox”的参数
ts
class Box {
  static contents: string = ''
  static set(value: string) { // 类类型 (method) Box.set(value: string): typeof Box
    this.contents = value
    return this
  }
}

基于类型的守卫

您可以在类和接口方法的返回位置使用 this is Type。当与类型缩小(如 if 语句)混合使用时,目标对象的类型将缩小为指定的 Type

ts
interface Networked {
  host: string
}

class FileSystemObject {
  constructor(
    public path: string,
    private networked: boolean,
  ) {}

  isDirectory(): this is Directory {
    return this instanceof Directory
  }

  isFile(): this is FileRep {
    return this instanceof FileRep
  }

  isNetworked(): this is Networked & this {
    return this.networked
  }
}
class Directory extends FileSystemObject {
  children!: FileSystemObject[]
}

class FileRep extends FileSystemObject {
  constructor(
    path: string,
    public content: string,
  ) {
    super(path, false)
  }
}

const fso: FileSystemObject = new FileRep('foo/bar.txt', 'foo')

let a: FileSystemObject[] | string | string | undefined
if (fso.isFile()) {
  a = fso.content // const fso: FileRep
} else if (fso.isDirectory()) {
  a = fso.children // const fso: Directory
} else if (fso.isNetworked()) {
  a = fso.host // const fso: Networked & FileSystemObject
}
console.log(a)

与泛型一起使用

ts
class Box<T> {
  value?: T

  hasValue(): this is { value: T } {
    return this.value !== undefined
  }
}

const box = new Box<string>()

const a = box.value // const a: string | undefined

if (box.hasValue()) {
  const b = box.value // const b: string
}

抽象类和抽象成员

  • 抽象类是不能被实例化的,只能被继承

  • 抽象属性和抽象方法只能存在于抽象类中,没有提供实现但必须在派生类中实现,且必须实现所有的

ts
abstract class Base {
  abstract a: number
  abstract getName(): string

  printName() {
    console.log(`Hello, ${this.getName()}`)
  }
}

class Derived extends Base {
  a = 1
  getName() {
    return 'world'
  }
}

抽象构造函数

接上例

ts
function greet(Ctor: typeof Base) {
  const instance = new Ctor() // error 无法创建抽象类的实例
  instance.printName()
}

上科技

ts
function greet(Ctor: new () => Base) {
  const instance = new Ctor()
  instance.printName()
}
greet(Base) // 类型“typeof Base”的参数不能赋给类型“new () => Base”的参数。无法将抽象构造函数类型分配给非抽象构造函数类型
greet(Derived) // ok