Skip to content
当前页导航

Typescript

typescript 的数据类型有哪些

  • boolean(布尔类型)
  • number(数字类型)
  • string(字符串类型)
  • array(数组类型) let arr:string[] = ['12', '23'];let arr:Array<number> = [1, 2];
  • tuple(元组类型):允许表示一个已知元素数量和类型的数组,let tupleArr:[number, string, boolean] = [12, '34', true];
  • enum(枚举类型)enum Color {Red, Green, Blue}; let c: Color = Color.Green;
  • any(任意类型)
  • null 和 undefined 类型
  • void 类型:用于标识方法返回值的类型,表示该方法没有返回值。
  • unknown(描述不确定的变量):可以将任意类型的值赋值给 unknown,但 unknown 类型的值只能赋值给 unknown 或 any
  • never 类型:never 是其他类型 (包括 null 和 undefined)的子类型,可以赋值给任何类型。但是没有类型是 never 的子类型,这意味着声明 never 的变量只能被 never 类型所赋值。
  • object 对象类型

never 和 void 有什么区别?

void:

  • void 表示函数没有返回值,或者说函数返回的是 undefined。
  • 当一个函数没有显式指定返回值类型时,它的返回类型默认为 void。
  • 不能对 void 类型的变量赋予除 undefined 以外的值。

never:

  • never 表示函数永远不会正常返回,或者说函数会抛出异常或无限循环。
  • 通常 never 类型用于表示永远不会执行完的函数或抛出异常的函数,或者在类型系统中表示不可能发生的情况。
  • 可以将 never 类型赋值给任何其他类型,但是反过来不行。

说说你对 TypeScript 中枚举类型的理解?应用场景?

什么是枚举

枚举是一个被命名的常量集合,类似于 js 对象, 枚举的使用是通过 enum 关键字进行定义。多处定义相同的枚举是可以进行合并操作

它分为:

  • 数字枚举
  • 字符串枚举
  • 异构枚举

数字枚举

当我们声明一个枚举类型是,虽然没有给它们赋值,但是它们的值其实是默认的数字类型,而且默认从 0 开始依次累加:

ts
enum Direction {
  Up, // 值默认为 0
  Down, // 值默认为 1
  Left, // 值默认为 2
  Right, // 值默认为 3
}

console.log(Direction.Up === 0) // true
console.log(Direction.Down === 1) // true
console.log(Direction.Left === 2) // true
console.log(Direction.Right === 3) // true

如果我们将第一个值进行赋值后,后面的值也会根据前一个值进行累加 1:

ts
enum Direction {
  Up = 10,
  Down, // 11
  Left, // 12
  Right, // 13
}

这种自增的特点会导致一个问题的出现,如下:

ts
enum Direction {
  Up, // 0
  Down, // 1
  Left, // 2
  Right = 2,
}

由上可以看出,Direction.LeftDirection.Right都等于 2,这就会导致在使用它们做判断时,两个的条件都一样

字符串枚举

ts
enum Direction {
  Up = 'Up',
  Down = 'Down',
  Left = 'Left',
  Right = 'Right',
}

如果设定了一个变量为字符串之后,后续的字段也需要赋值字符串,否则报错:

ts
enum Direction {
  Up = 'UP',
  Down, // error TS1061: Enum member must have initializer
  Left, // error TS1061: Enum member must have initializer
  Right, // error TS1061: Enum member must have initializer
}

异构枚举

即把数字枚举和字符串枚举结合起来混合起来使用

ts
enum Direction {
  No = 0,
  Yes = 'YES',
}

this 的问题

在 ts 中,必须显示的指定 this 的类型,不然就会报错。

ts
function say() {
  console.log(this.name) // ts(2683) “this”隐式具有类型“any”,因为它没有类型注释
}
say()

// 在上述代码中,如果我们直接调用 say 函数,this 应该指向全局 window 或 global(Node 中)。
// 但是,在 strict 模式下的 TypeScript 中,它会提示 this 的类型是 any,此时就需要我们手动显式指定类型了。

在 TypeScript 中,我们只需要在函数的第一个参数中声明 this 指代的对象(即函数被调用的方式)即可,比如最简单的作为对象的方法的 this 指向。

显式注解函数中的 this 类型,它表面上占据了第一个形参的位置,但并不意味着函数真的多了一个参数,因为 TypeScript 转译为 JavaScript 后,“伪形参” this 会被抹掉。

ts
function say(this: Window, name: string) {
  console.log(this.name)
}

// say('sky') // “void”类型的“this”上下文不可分配给“Window”类型的方法“this”,后面会解释
window.say = say
window.say('name')

const obj = { say, name: 'bb' }
obj.say('dd')
// 报错 The 'this' context of type '{ say: (this: Window, name: string) => void; name: string; }' is not assignable to method's 'this' of type 'Window'
ts
interface Person {
  name: string
  say(this: Person): void
}

const person: Person = {
  name: 'captain',
  say() {
    console.log(this.name)
  },
}

const fn = person.say

fn() // 报错 The 'this' context of type 'void' is not assignable to method's 'this' of type 'Person'
ts
class Component {
  onClick(this: Component) {}
}

const component = new Component()

interface UI {
  addClickListener(onClick: (this: void) => void): void
}

const ui: UI = {
  addClickListener() {},
}

ui.addClickListener(component.onClick) // 报错
// 上面示例中,我们定义的 Component 类的 onClick 函数属性(方法)显式指定了 this 类型是 Component,
// 在第 15 行作为入参传递给 ui 的 addClickListener 方法中,它指定的 this 类型是 void,两个 this 类型不匹配。

函数重载

针对同一个函数,根据函数的不同参数,返回的结果不一样。ts 的函数重载 匹配的规则是从上到下。

ts
interface P1 {
  name: string
}

interface P2 extends P1 {
  age: number
}

function convert(x: P1): number

function convert(x: P2): string

function convert(x: P1 | P2): any {}

const x1 = convert({ name: '' } as P1) // => number

const x2 = convert({ name: '', age: 18 } as P2) // number
// 因为 P2 继承自 P1,所以类型为 P2 的参数会和类型为 P1 的参数一样匹配到第一个函数重载,此时 x1、x2 的返回值都是 number。

// 改进,只需要把 P2 提前
function convert(x: P2): string

function convert(x: P1): number

function convert(x: P1 | P2): any {}

const x1 = convert({ name: '' } as P1) // => number

const x2 = convert({ name: '', age: 18 } as P2) // => string

类型谓词(is)

在添加返回值类型的地方,通过“参数名 + is + 类型”的格式明确表明了参数的类型,进而引起类型缩小。

ts
function isString(s: unknown): s is string {
  // 类型谓词
  return typeof s === 'string'
}

function isNumber(n: number) {
  return typeof n === 'number'
}

function operator(x: unknown) {
  if (isString(x)) {
    // ok x 类型缩小为 string
  }

  if (isNumber(x)) {
    // ts(2345) unknown 不能赋值给 number
  }
}

说说 TypeScript 中的接口

简单来讲,一个接口所描述的是一个对象相关的属性和方法。可以重复定义,它们的属性会叠加

只读属性

我们可以通过在属性名前面加 readonly 修饰符来标注。

需要注意的是:加上 readonly 只是在检测层面会报错,实际编译过后还是可以更改的。

ts
interface ReadOnlyProgramLanguage {
  readonly name: string
}

let ReadOnlyTypeScript: ReadOnlyProgramLanguage = {
  name: 'TypeScript',
}

/** ts(2540)错误,name 只读 */
ReadOnlyTypeScript.name = 'JavaScript'

可选属性

ts
interface OptionalProgramLanguage {
  name: string
  age?: () => number
}

let OptionalTypeScript: OptionalProgramLanguage = {
  name: 'TypeScript',
} // ok

接口的索引签名

我们可以对接口的索引进行类型约束。索引名称的类型分为 string 和 number 两种,通过如下两个接口,我们可以用来描述索引是任意数字或任意字符串的对象。

ts
// 有两个接口
interface StringInterface {
  [name: string]: string
}

interface NumberInterface {
  [name: number]: number
}

let str: StringInterface = {
  age: 'fdf',
  sex: 'fd',
  2: 'fd', // ok
}

let num: NumberInterface = {
  1: 3,
  2: 5,
  0: 3,
  age: 3, // 报错
}

// 注意:在上述示例中,数字作为索引时,它的类型既可以与数字兼容,也可以与字符串兼容,
// 这与 JavaScript 的行为一致。因此,使用 0 或 '0' 索引对象时,这两者等价。

注意:虽然属性可以与索引签名进行混用,但是属性的类型必须是对应的数字索引或字符串索引的类型的子集,否则会出现错误提示。

ts
interface StringInterface {
  [name: string]: string
  sex: string // ok // age: number // 报错,因为它必须是跟前面的索引类型 [name: string] : string 同样的,只能是 string 或 string 的子集(比如 any、null、undefined、never)
}

interface NumberInterface {
  name: string
  age: number
  [name: number]: number
}

let str: StringInterface = {
  age: 'd',
  sex: 'fd',
  2: 'fd',
}

let num: NumberInterface = {
  name: 'f',
  2: 5,
  0: 3,
  age: 3,
}

// 还需要注意的是   我们不能约束数字索引属性与字符串索引属性拥有截然不同的类型,具体示例如下:
interface NumberInterface {
  [name: number]: number // 报错
  [rank: string]: string
}
需改成如下同种类型
interface NumberInterface {
  [name: number]: number
  [rank: string]: number
}

接口的继承

ts
interface Animal {
  type: string
}

interface Dog {
  name: string
}

// 继承一个
interface Cat extends Animal {
  age: number
}

// 继承多个
interface Cat extends Animal, Dog {
  age: number
}

说说 TypeScript 中的类型别名

它不能重复定义同一个类型别名

ts
type LanguageType = {
  name: string
  age: () => number
}
type fn = () => string
type ag = string | number

针对接口类型无法覆盖的场景,比如组合类型、交叉类型,我们只能使用类型别名

ts
interface Animal {
  type: string
}

/** 联合 */
type MixedType = string | number

/** 交叉 */
type IntersectionType = { id: number; name: string } & {
  age: number
  name: string
}

/** 提取接口属性类型 */
type AgeType = Animal['type']

type UA = 'px' | 'em' | 'rem'
type UB = 'vh' | 'em' | 'rem'

type Union = UA & UB
const a: Union = 'em'

联合操作符 | 和 交叉操作符的优先级 & ,& 操作符的优先级更高,不管出现的顺序,可以通过 ()进行调整优先级。

Interface 与 Type 的相同点与区别

相同点:

  • 都可以描述一个对象和函数类型
  • 都可以扩展,不过实现方式不一样, interface 是用 extends , type 是用 & 符号(其实就是交叉类型),它们两个可以互相扩展

不同点:

  • type 还可以声明基本类型、联合类型、元组类型等。
  • type 支持映射。
  • type 可以使用 typeof 返回的值。
  • interface 可以合并重复声明, type 不可以。

class 类

公共、私有与受保护的修饰符

  • public 修饰的是在任何地方可见、公有的属性或方法
  • private 修饰的是仅在同一类中可见、私有的属性或方法
  • protected 修饰的是仅在类自身及子类中可见、受保护的属性或方法
  • readonly 只读修饰符
ts
class Animal {
  public type = 'animal'
  private name = 'sky'
  protected age = 23
  public readonly sex = 'man'
}

类的存取器

通过存取器 getter 和 setter 截取对类成员的读写访问。

ts
class Animal {
  type = 'animal'
  get myName() {
    return this.type
  }

  set myName(name: string) {
    this.type = name
  }
}

const ani = new Animal()
console.log(ani.myName) // animal
ani.myName = 'sky'
console.log(ani.myName) // sky

类的继承

ts
class Animal {
  type = 'animal'
}

class Dog extends Animal {
  name: string
  constructor(name: string) {
    super() // 如果不调用,会报错:派生类的构造函数必须包含 "super" 调用
    this.name = name
  }
}

抽象类

它是一种不能被实例化仅能被子类继承的特殊类。通过 abstract 关键字去声明一个抽象类,以及抽象类中的抽象属性、抽象方法。

它跟接口有点类似,可以使用抽象类定义派生类需要实现的属性和方法,有区别的一点为,可以定义派生类的默认属性和方法。接口则不行。

ts
abstract class Adder {
  abstract x: number
  abstract y: number
  abstract add(): number

  displayName = 'Adder'
  addTwice(): number {
    return (this.x + this.y) * 2
  }
}
// 通过 abstract 声明的属性和方法,在派生类 NumAdder 中,都需要实现。

class NumAdder extends Adder {
  x: number
  y: number

  constructor(x: number, y: number) {
    super()
    this.x = x
    this.y = y
  }

  add(): number {
    return this.x + this.y
  }
}

const numAdder = new NumAdder(1, 2)

console.log(numAdder.displayName) // => "Adder"

console.log(numAdder.add()) // => 3

console.log(numAdder.addTwice()) // => 6

使用接口定义类

ts
interface IAdder {
  x: number
  y: number
  add: () => number
}

class NumAdder implements IAdder {
  x: number
  y: number
  constructor(x: number, y: number) {
    this.x = x
    this.y = y
  }

  add() {
    return this.x + this.y
  }

  addTwice() {
    return (this.x + this.y) * 2
  }
}

泛型

泛型指的是类型参数化,即将原来具体的类型进行参数化,根据传入的类型来明确。

设计泛型的目的在于有效约束类型成员之间的关系,比如函数参数和返回值、类或者接口成员和方法之间的关系。

函数泛型

ts
function returnItem<T>(para: T): T {
  return para
}

// 定义泛型的时候,可以一次定义多个类型参数,比如我们可以同时定义泛型 T 和 泛型 U:
function swap<T, U>(tuple: [T, U]): [U, T] {
  return [tuple[1], tuple[0]]
}

swap([7, 'seven']) // ['seven', 7]

接口泛型

ts
interface ReturnItemFn<T> {
  (para: T): T
}

const returnItem: ReturnItemFn<number> = (para) => para

类泛型

在类的定义中,我们还可以使用泛型用来约束构造函数、属性、方法的类型

ts
class Memory<S> {
  store: S
  constructor(store: S) {
    this.store = store
  }
  set(store: S) {
    this.store = store
  }
  get() {
    return this.store
  }
}
const numMemory = new Memory<number>(1) // <number> 可缺省
const getNumMemory = numMemory.get() // 类型是 number
numMemory.set(2) // 只能写入 number 类型
const strMemory = new Memory('') // 缺省 <string>
const getStrMemory = strMemory.get() // 类型是 string
strMemory.set('string') // 只能写入 string 类型

实用工具类型

TypeScript 提供了几种实用工具类型来促进常见的类型转换。这些实用程序是全局可用的。

  • Partial<Type> 。将传入的 type 类型的所有属性设置为可选的类型。并返回一个新类型。
  • Required<Type> 。将传入的 type 类型的所有属性设置为必选的类型。并返回一个新类型。跟 Partial 正好相反
  • Readonly<Type> 。将传入的 type 类型的所有属性设置为只读,并返回一个新类型。
  • Pick<Type, Keys> 。从 type 中选取部分属性 keys(字符串字面值或字符串字面值的联合)来构造类型,返回新的类型。跟 Omit 相反。
  • Omit<Type, Keys> 。从 type 中删除部分属性 keys(字符串字面值或字符串字面值的联合)来构造类型,返回新的类型。
  • Record<Keys, Type> 。构造一个对象类型,它的属性键是 keys,属性值是 type 类型。可用于将一种类型的属性映射到另一种类型。Keys 可以为固定的值,也可以是一个联合类型,但是当为联合类型的时候,必须全部有其中的属性。
ts
/**
 * Partial<Type>
 * 将传入的 type 的所有属性设置为可选的类型。并返回一个新类型
 * 源码 type Partial<T> = { [P in keyof T]?: T[P] | undefined; }
 */
interface OldPart {
  id: number
  age: number
}
const todo1: Partial<OldPart> = {
  id: 3,
}

/**
 * Required<Type>
 * 将传入的 type 的所有属性设置为必选的类型。并返回一个新类型。跟 Partial 正好相反
 * 源码 type Required<T> = { [P in keyof T]-?: T[P]; }
 */
type OldRequired = {
  id?: number
  age?: number
}
const todo2: Required<OldRequired> = {
  id: 2,
  age: 3,
}

/**
 * Readonly<Type>
 * 将传入的 type 的所有属性设置为只读,并返回一个新类型。
 * 源码 type Readonly<T> = { readonly [P in keyof T]: T[P]; }
 */
interface OldRead {
  id: number
}
const todo3: Readonly<OldRead> = {
  id: 3,
}

// todo3.id = 5 // 无法分配到 "id" ,因为它是只读属性。ts(2540)

/**
 * Record<Keys, Type>
 * 构造一个对象类型,它的属性键是 keys,属性值是 type 类型。可用于将一种类型的属性映射到另一种类型。
 * Keys 可以为固定的值,也可以是一个联合类型,但是当为联合类型的时候,必须全部有其中的属性
 * 源码 type Record<K extends string | number | symbol, T> = { [P in K]: T; }
 */
interface OldRecord {
  id: number
  age: number
}
type CatName = 'miffy' | 'boris' | 'mordred'
const todo4: Record<'name', OldRecord> = {
  name: { id: 3, age: 5 },
}

// 为联合类型的时候,必须全部有其中的属性
const todo5: Record<CatName, OldRecord> = {
  miffy: { id: 3, age: 5 },
  boris: { id: 3, age: 5 },
  mordred: { id: 3, age: 5 },
}

/**
 * Pick<Type, Keys>
 * 通过从 type 中选取部分属性keys(字符串字面值或字符串字面值的联合)来构造类型,返回新的类型。跟 Omit 相反
 * 源码 type Pick<T, K extends keyof T> = { [P in K]: T[P]; }
 */
interface OldPick {
  id: number
  age: number
  desc: string
}
const todo6: Pick<OldPick, 'age'> = {
  age: 3,
}
const todo7: Pick<OldPick, 'age' | 'desc'> = {
  age: 3,
  desc: 'dd',
}

/**
 * Omit<Type, Keys>
 * 从 type 中删除部分属性keys(字符串字面值或字符串字面值的联合)来构造类型,返回新的类型。
 * 源码 type Omit<T, K extends string | number | symbol> = { [P in Exclude<keyof T, K>]: T[P]; }
 */
interface OldOmit {
  id: number
  age: number
  desc: string
}
const todo8: Omit<OldOmit, 'age'> = {
  id: 3,
  desc: 'dd',
}
const todo9: Omit<OldOmit, 'age' | 'desc'> = {
  id: 3,
}

TypeScript 命名空间

命名空间一个最明确的目的就是解决重名问题,TypeScript 中命名空间使用 namespace 来定义。

命名空间定义了标识符的可见范围,一个标识符可在多个名字空间中定义,它在不同名字空间中的含义是互不相干的

ts
namespace SomeNameSpaceName {
  export interface ISomeInterfaceName {}
  export class SomeClassName {}
}

namespace Letter {
  export let a = 1
  export let b = 2
  export let c = 3
  export let z = 26
}

// 使用方式
SomeNameSpaceName.SomeClassName

TypeScript 装饰器

装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上,是一种在不改变原类和使用继承的情况下,动态地扩展对象功能

类装饰

例如声明一个函数 addAge 去给 Class 的属性 age 添加年龄.

ts
function addAge(constructor: Function) {
  constructor.prototype.age = 18
}

@addAge
class Person {
  name: string
  age!: number
  constructor() {
    this.name = 'huihui'
  }
}

let person = new Person()

console.log(person.age) // 18

// 上述代码,实际等同于以下形式:
Person = addAge(function Person() { ... });

方法/属性装饰

装饰器可以用于修饰类的方法,这时候装饰器函数接收的参数变成了:

  • target:对象的原型
  • propertyKey:方法的名称
  • descriptor:方法的属性描述符
ts
// 声明装饰器修饰方法/属性
function method(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  console.log(target)
  console.log('prop ' + propertyKey)
  console.log('desc ' + JSON.stringify(descriptor) + '\n\n')
  descriptor.writable = false
}

function property(target: any, propertyKey: string) {
  console.log('target', target)
  console.log('propertyKey', propertyKey)
}

class Person {
  @property
  name: string
  constructor() {
    this.name = 'huihui'
  }

  @method
  say() {
    return 'instance method'
  }

  @method
  static run() {
    return 'static method'
  }
}

const xmz = new Person()

// 修改实例方法say
xmz.say = function () {
  return 'edit'
}

参数装饰

接收 3 个参数,分别是:

  • target :当前对象的原型
  • propertyKey :参数的名称
  • index:参数数组中的位置
ts
function logParameter(target: Object, propertyName: string, index: number) {
  console.log(target)
  console.log(propertyName)
  console.log(index)
}

class Employee {
  greet(@logParameter message: string): string {
    return `hello ${message}`
  }
}
const emp = new Employee()
emp.greet('hello')

装饰器工厂

如果想要传递参数,使装饰器变成类似工厂函数,只需要在装饰器函数内部再函数一个函数即可

ts
function addAge(age: number) {
  return function (constructor: Function) {
    constructor.prototype.age = age
  }
}

@addAge(10)
class Person {
  name: string
  age!: number
  constructor() {
    this.name = 'huihui'
  }
}

let person = new Person()

当多个装饰器应用于一个声明上,将由上至下依次对装饰器表达式求值,求值的结果会被当作函数,由下至上依次调用

ts
function f() {
  console.log('f(): evaluated')
  return function (
    target,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    console.log('f(): called')
  }
}

function g() {
  console.log('g(): evaluated')
  return function (
    target,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    console.log('g(): called')
  }
}

class C {
  @f()
  @g()
  method() {}
}

// 输出
f() // evaluated
g() // evaluated
g() // called
f() // called