TypeScript类型注释

1.类型注释

TypeScript类型种类如下

TypeScript基本类型:

number,string,boolean,bigint,symbol,null,undefined,never,void,any,unknown,值类型

对象类型:

class,object,Array,interface,JavaScript构造函数类型

高级类型:

enum,tuple,type,联合类型,交叉类型

1.1 TypeScript基本类型

1.1.1 TypeScrip和JavaScript基本类型区别

  • 大小写区别:

    TypeScript类型和JavaScript类型大小写不同。

  • 性能区别:

    JavaScript中这些类型是作为构造函数或工厂函数使用,TypeScript这些类型就是纯粹的类型,没有这些功能。虽然使用两者进行注释是等效的,但是使用TypeScript中这些类型会有更好的性能和内存管理能力(参考ChatGPT)。

1.1.2 void和never

never和void是语义相近的类型,never比void更加严格。

  • never表示不能是任何类型:

    // 使用后文提到的“类型别名”和“交叉类型”来获得never类型
    type variableA = string 
    type variableB = number
    //得到的variableC是never类型,因为string和number类型不可能取交集
    type variableC = variableA & variableB 
    
    // 指定一个函数返回值为never类型,这个函数永远不能被赋值,因为函数默认返回undefined
    let funcEmpty: () => never
    
  • void表示值只能是undefined:

    // 指定一个函数为空函数
    let funcEmpty: () => void = () => {}
    

1.1.3 any和unknown

  • any表示任何类型:

    any表示任何类型,不推荐使用,使用后效果和JavaScript相同。

  • unknown表示未知类型,操作需要检查:

    unknown表示任何类型,含义和any相同,但是比any严格,可以使用。在调用变量属性方法,或进行其它操作时不经过检查都是不合法的。

    let variable: unknown = 'hello'
    console.log(variable.length) // 报错,不能对unknown类型变量进行操作
    
    if(typeof variable === 'string')
        console.log(variable.length) // 合法,提前检测了类型
    

1.1.4 值类型

当声明时指定了具体值之后,变量赋值只能赋为该值,否则会发生错误

let variableA: 2
variableA = 2

1.1.5 其它基本类型

直接进行类型注释即可,较为简单

let variable: string = 'hello'

1.2 对象类型

1.2.1 object和Object

  • 不要使用object和Object进行注释:

    在1.1.1中提到了用object和Object进行类型注释是等效的。但是实际上不推荐使用object和Object进行类型注释。使用object和Object进行对象类型注释,TypeScript会认为对象不可修改,必须保持和空对象一致。

    let obj: object = {
        name: "Danny",
        age: 21
    }
    
    console.log(obj.name) // 报错,不允许访问name属性
    obj.gender = "male" // 报错,不允许修改对象
    console.log(obj.toString) // 合法,空对象原型上有toString方法
    
  • 使用对象字面量进行注释:

    如果要类型注释对象类型,推荐使用对象字面量。不过要注意的是,TypeScript是“静态类型检查语言”(参考编译原理),对于类型检查是严格的,在变量初始化时指定了要和对象字面量形式一致,因此下面添加或删除对象属性都是非法的。

    let obj: { name: string, age: number } = {
        name: "Danny",
        age: 21
    }
    
    console.log(obj.name) // 合法
    obj.gender = "male" // 不合法
    console.log(obj.toString) // 合法
    
  • 其它推荐的对象注释方式:

    在实际应用时使用对象字面量注释不具有复用性,通常使用“接口”,“类型别名”,“类”进行注释

1.2.2 Array

TypeScript中数组类型注释较为简单。string[]类型注释是泛型类型注释Array的简写,效果相同。

在使用Array时要另外注意一点Array类型在类型推断时会被推断为数组元素个数不确定,详情参考“2.2.2数组推断”或“TypeScript泛型”博文。

// 类型注释一维数组
let ar1Dim: string[] = ['1', '2', '3']
// 类型注释二维数组
let ar2Dim: string[][] = [['11', '12', '13'], ['21', '22', '23']]

// 还可以使用内置Array泛型来声明,但是声明高维数组较麻烦
const ar1Dim: Array<string> = ['1', '2', '3']
const ar2Dim: Array<Array<string>> = [['11', '12', '13'], ['21', '22', '23']]

1.2.3 Function

1.2.3.1 声明函数
  • “函数表达式”声明函数:

    function func(arg1: string, arg2: number): void {}
    
  • “赋值语句”声明函数:

    const func = function(arg1: string, arg2: number): void {}
    
  • “箭头函数”声明函数:

    const func = (arg1: string, arg2: number): void => {} 
    
  • “接口或类型别名”声明函数:

    // 接口
    interface FuncInterface {
        (arg1: string, arg2: number): void // 语法糖:对象字面量中定义函数返回值可以用“:返回值”替代
    }
    const funcI: FuncInterface = (arg1: string, arg2: number) => {}
    
    // 类型别名
    type FuncAlias = (arg1: string, arg2: number) => void // 语法糖:描述箭头函数类型
    const funcA = (arg1: string, arg2: number) => {}
    
  • “函数签名”声明函数:

    // 函数签名可以由“接口”,“类型别名”实现
    interface Func {
        description: string;
        (arg1: string, arg2: number): void
    }
    
    function func(arg1: string, arg2: number): void {}
    
    func.description = "this is a function"
    
    const funcS: Func = func
    
1.2.3.2 注释函数
  • 不要使用Function:

    不要使用Function进行类型注释:在最后两种声明方法中和“object和Object”一样,在这里也不推荐用Function进行注释。Function太过于宽泛,没有指明参数类型和返回值类型。

  • 使用throw可以不用考虑返回值:

    如果函数中使用throw,说明函数无法正常返回值,当然默认返回undefined也是不生效的。在这里已经抛出了错误,所以不需要考虑和函数返回值的类型注释保持一致。

    function test(): string {
        throw new TypeError('test') // 合法
    }
    test() // 合法
    

1.2.4 Interface

1.2.4.1 描述接口

在描述接口换行时可以使用逗号或分号

interface STU {
    id: string,
    name: string,
    gender: string,
    age: number,
    getDetailInfo(id: string): { grade: number, location: string }
}
1.2.4.2 扩展接口
  • 使用extends关键字扩展接口:

    interface A {
        name: string
    }
    interface B extends A { // 此时实现接口B也必须含有接口A中的name
        age: number
    }
    
  • 使用覆写方式扩展接口:

    interface A {
        name: string
    }
    interface A {
        age: number // 此时效果和extends效果相同
    }
    
  • 使用交叉类型扩展接口:

    interface A {
    	name: string
    }
    interface B {
        age: number
    }
    type C = A & B
    
1.2.4.3 实现接口
1.2.4.3.1 类实现接口
  • 使用implements实现接口无法进行类型推断:

    后文要提到的“类型推断”是指在不进行类型注释时TypeScript会推测变量类型是哪一种,这类似于JavaScript的V8引擎中的“JIT”优化中的热处理(详情参考博客“JavaScript编译器和解释器”)。由于这里只有implements关键字,TypeScript无法推测类实现的接口中的方法的参数类型。

    class SDUer implements STU { // STU接口在上文已经实现
        constructor(public id: string, public name: string, public gender: string, public age: number) {}
        getDetailInfo(id) {
            return {
                grade: 100,
                location: '6'
            }
        }
    }
    
    let sduer = new SDUer('123', 'Danny', 'male', 21) 
    sduer.getDetailInfo(1234) // 虽然接口中指明了该方法接收的参数应为字符串,但是编译器不会报错
    
  • 解决类实现接口可能出现的潜在错误:

    由于implements导致无法进行类型推断,因此函数参数默认为any,这样会影响静态类型检查,可能会出现错误。

    可行的解决方案:

    • 配置tsconfig.json,开启严格模式,这样不允许any类型出现

    • 实现接口方法时主动添加参数类型

      getDetailInfo(id: string) {
          return {
              grade: 100,
              location: '6'
          }
      }
      
    • 声明对象时指定对象就是接口的实现

      let sduer: STU = new SDUer('123', 'Danny', 'male', 21) 
      
  • 类不能实现接口描述的构造函数:

    接口或类型别名可能使用这样的语法糖来描述构造函数:

    interface Constructor {
    	new (arg1: string, arg2: number): { name: string, age: number }
    }
    
    type Constructor = {
    	new (arg1: string, arg2: number): { name: string, age: number }
    }
    

    但是类不能实现这样的接口,因为构造函数属于类的独有成员,不应该由接口描述,不论尝试怎样实现,编译器都会报错。

    接口描述构造函数的场景应用较少,比较常见的一个场景就是“使用工厂函数创建对象

    // 通过接口来描述构造函数
    interface Constructor {
        new (name: string, age: number): { name: string, age: number }
    }
    
    // 构造构造函数所在的类
    class Person {
        constructor(public name: string, public age: number) {}
    }
    
    // 工厂函数
    function factory(constructor: Constructor, name: string, age: number): Person {
        return new constructor(name, age)
    }
    
    // 构造函数通过类名调用,所以将Person类传递过去
    console.log(factory(Person, 'Danny', 21))
    
1.2.4.3.2 对象实现接口
const sduStu: STU = {
    id: '1234',
    name: 'Danny',
    gender: 'male',
    age: 21,
    getDetailInfo(id: string) { // id可以不写类型注释,因为通过sdustu: STU可以推断出这里的id必须是string类型
        return {
            grade: 100,
            location: '6'
        }
    }
}
1.2.4.3.3 函数实现接口

函数实现接口的方法起始已经委婉地在上1.2.3中提到,就是“接口声明函数”和“函数签名声明函数”两种方式。

// 用接口声明函数
interface Cal {
    (a: number, b: number): number
}

const cal: Cal = (a, b) => a + b

1.2.5 其它构造函数类型

直接进行注释即可,较为简单

// JavaScript构造函数类型
let str: String = 'hello'

// 自定义class类型
class STU {
    public name: string
    public age: number
    constructor(name: string, age: number) {
        this.name = name
        this.age = age
    }
}
let stu: STU = new STU("Danny", 21)

1.3 高级类型

1.3.1 enum

使用enum声明一个枚举,枚举中又声明了若干枚举值,每一个枚举值又会有一个关联值。

1.3.1.1 默认枚举

默认从第一个枚举值开始关联0,往后关联1,2,3

enum Fruit {
    Apple,
    Orange,
    Melon
}

typescript的默认枚举在实现上实际是一个对象,对象中进行了反向映射

let Fruit = {
    Apple: 0,
    Orange: 1,
    Melon: 2,
    '0': 'Apple',
    '1': 'Orange',
    '2': 'Melon'
}

Fruit.AppleFruit[0]这两种访问形式都有应用场景

1.3.1.2 数值枚举

数值枚举要求给枚举值赋值为数值

enum Fruit {
    Apple = 2,
    Orange,
    Melon
}

反向映射结果为

let Fruit = {
    Apple: 2,
    Orange: 3,
    Melon: 4,
    '2': 'Apple',
    '3': 'Orange',
    '4': 'Melon'
}

也可以给全部枚举值指定关联值

enum Fruit {
    Apple = 2,
    Orange = 1,
    Melon = 4
}

反向映射结果为

let Fruit = {
    Apple: 2,
    Orange: 1,
    Melon: 4,
    '2': 'Apple',
    '1': 'Orange',
    '4': 'Melon'
}
1.3.1.3 字符串枚举

字符串枚举要求每个枚举值必须赋一个字符串关联值

enum Fruit {
    Apple = 'Apple',
    Orange = 'Orange',
    Melon = 'Melon'
}

反向映射结果为

let Fruit = {
    Apple: 'Apple',
    Orange: 'Orange',
    Melon: 'Melon',
}
1.3.1.4 混合枚举

混合枚举要求每个枚举值必须赋一个字符串或数值关联值。不推荐使用混合枚举,一般枚举值都有相关性,混合枚举破坏了相关的联系。

enum Fruit {
    Apple = 0,
    Orange = 'Orange',
    Melon = 'Melon'
}

反向映射结果为

let Fruit = {
    Apple: '0',
    0: 'Apple',
    Orange: 'Orange',
    Melon: 'Melon'
}
1.3.1.5 常量枚举

常量枚举在编译时会被删除,不产生枚举对应的对象形式,效率较高。因此声明常量枚举后直接访问枚举是非法的,只能访问枚举值。

const enum Fruit {
    Apple,
    Orange,
    Melon
}

console.log(Fruit) // 非法的
console.log(Fruit.Apple) // 合理的

因为不会生成对象形式,因此最后结果只会输出0

1.3.1.6 使用枚举的时机

在typescript中枚举,数组,对象相互之间的功能比较相似,需要在特定场景进行分析。

  • 语意明确时用枚举:

    当常量具有清晰明确的语意时,应该使用枚举类型。枚举类型为常量提供了明确的命名和类型,可以使代码更加可读和维护。

    enum DaysOfWeek {
      Monday,
      Tuesday,
      Wednesday,
      Thursday,
      Friday,
      Saturday,
      Sunday
    }
    
    function isWeekend(day: DaysOfWeek): boolean {
      return day === DaysOfWeek.Saturday || day === DaysOfWeek.Sunday;
    }
    
    isWeekend(DaysOfWeek.Saturday); // true
    
  • 值不需要变动用对象或数组:

    如果固定的值不会发生变化,可以考虑使用常量数组或对象。

    const weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"];
    const maxFileSize = { txt: 1024, jpg: 2048, gif: 3072 };
    
  • 值需要从动态来源获得用对象或数组

    如果值需要从动态来源获取,例如从后端API中获取,则应该使用对象或数组类型,并根据需要进行类型定义。

    interface User {
      id: number;
      name: string;
      role: "admin" | "member";
    }
    
    function hasAdmin(users: User[]) {
      return users.some(user => user.role === "admin");
    }
    
    // 从API中获取用户信息
    fetch("/users")
      .then(response => response.json())
      .then(data => {
        if (hasAdmin(data)) {
          // do something
        }
      });
    
1.3.1.7 枚举类型注释方法

枚举和枚举值都可以作为类型来进行注释。

  • (常用)注释为枚举类型:

    将变量注释为枚举类型,指的是变量的取值范围固定在枚举值的关联值当中。

    let variableEnum: Fruit = 0
    if(variableEnum === Fruit.Apple)
        console.log('Apple')
    else if(variableEnum === Fruit.Orange)
        console.log('Orange')
    else if(variableEnum === Fruit.Melon)
        console.log('Melon')
    else
        console.log('None')
    
  • 注释为枚举值类型:

    let variableA: Fruit.Apple
    variableA = 0
    

1.3.2 tuple

  • 元组确定性:

    元组确定了数组每个元素的类型以及数组的长度。数组只能规定每个元素的类型,不能确定数组长度。

    const tuple: [number, number] = [1, 2]
    const ar: Array<number> = [1, 2]
    const add = (a: number, b: number) => a + b
    add(...tuple) // 合法,元组长度固定,展开后add接收两个参数
    add(...ar) // 不合法,数组长度不确定,展开后接收未知个参数
    
  • 元组可选参数:

    在进行元组类型的注释时可以使用?表示元素可以不存在,但是只能在最后使用。

    type AnyTypeArray = [number, boolean?, string?]
    const ar: AnyTypeArray = [1] // 合法
    const ar: AnyTypeArray = [1, true] // 合法
    

    如果没有使用元组的可选元素,那么默认为undefined。元组的长度会包括类型注释时的可选参数。

    type AddParameters = [number, number, number?]
    const ar: AddParameters = [1, 2]
    
    const add = (a: number, b: number) => a + b
    add(...ar) // 报错,元组长度实际为3
    
  • 元组剩余参数:

    在进行元组类型注释时可以在非尾部使用剩余参数。

    type Tuple = [number, ...boolean[], string]
    const ar: Tuple = [1, 'hello']
    const arr: Tuple = [1, true, false, 'hello']
    

    ☆☆这种做法还可以实现JavaScript无法做到的函数形参声明。相当于可以在非末尾声明剩余形参。

    function handleUserInfo(...args: [number, ...string[], boolean]) {
        const [age, name, gender, location, isMarried] = args
        console.log(age, name, gender, location, isMarried)
    }
    
    // 使用效果相当炸裂
    handleUserInfo(21, 'Danny', 'male', 'WeiHai', true)
    
  • 只读元组:

    只读元组可以使用TypeScript内置的Readonly别名来声明

    const tuple: Readonly<[number, string]> = [21, 'Danny']
    const [userAge, userName] = tuple
    

    只读元组可以使用readonly关键字来声明

    const tuple: readonly [number, string] = [21, 'Danny']
    const [userAge, userName] = tuple
    

1.3.3 type

1.3.3.1 描述类型别名

使用类型别名指定已经存在的类型

// 使用类型别名指定为对象字面量类型
type School = {
    name: String,
    age: Number
}
let variable: School

// 使用类型别名指定为TypeScript基本类型
type str = string
1.3.3.2 扩展类型别名

类型别名一旦声明就只能通过&运算符进行扩展

type A = {
    name: string
}
type B = {
    age: number
}
type C = A & B // 合法
B.name: string // 非法

1.3.4 联合类型和交叉类型

1.3.4.1 联合类型

联合类型指的是用|来表示变量可以取不同的类型

let variableA: string | number | boolean
1.3.4.2 交叉类型

交叉类型指的是用&来表示变量取多种类型中的交集。

  • 对于接口,对象字面量类型:

    使用&交叉会合并属性,因为可以做到同时满足。

    interface A {
        name: string
    }
    interface B {
        gender: string
    }
    type C = A & B
    let obj: C = {
        name: 'Danny',
        gender: 'male'
    }
    
  • 对于普通类型:

    使用&交叉会直接取交集,因为无法同时满足。

    type TypeA = string | number
    type TypeB = number | boolean
    type TypeC = TypeA & TypeB
    let variable: TypeC = 123 // variable只能取数值类型
    

2.类型推断

合理利用类型推断可以使代码更简洁。不过要注意,尽可能不要被推断为any,在typescript严格模式下是不允许的。

2.1 类型推断方式

2.1.1 根据已有类型注释进行类型推断

下面函数的返回值一定是number类型,这是根据已有类型注释进行推断。

下面对象字面量实现接口,因为接口中已经显示指定类型且对象指定为接口的实现,那么实现的函数的参数将会被自动进行类型推断。

function cal(a: number, b: number) {
    return a + b
}

interface Test {
    work(a: number, b: number): void
}

const TestMaker: Test = {
    work: (a, b) => {}
}

2.1.2 根据变量值进行类型推断

下面会根据“hello”推测str为字符串类型。注意:除非显式指定变量为JavaScript值类型,否则不会被推测为JavaScript值类型。即下面的str不会被推测为“hello”类型。

let str = "hello"

2.2 类型推断特例

2.2.1 不进行类型推断

  • 类使用implements实现接口:

    例如使用类来实现一个接口中的check方法。注意:(tsc 5.1.3)由于没有类型注释,编译器无法进行类型推断。在实现接口方法时会认为参数均为any类型。

    interface CheckAble {
        check(name: string): boolean
    }
    
    class NameChecker implements CheckAble {
        check(s) {
            return true
        }
    }
    
    let nc = new NameChecker
    nc.check(123) // 编译不会报错
    

    添加类型注释,更规范地实现接口。(1)可以在声明变量时添加类型注释,指明是实现接口的变量(2)可以在类实现check方法时对参数进行类型注释

    interface CheckAble {
        check(name: string): boolean
    }
    
    class NameChecker implements CheckAble {
        check(s) {
            return true
        }
    }
    
    let nc: CheckAble = new NameChecker
    nc.check(123) // 编译会报错
    
  • 回调函数传参:

    该内容会在“TypeScript函数”博文中详细介绍,指的是回调函数实参个数可以少于形参个数。

    const filter = <T>(ar: Array<T>, callback: (el: T, idx: number, arr: Array<T>) => boolean): Array<T> => ar.filter(callback)
    
    console.log(filter([1, 2, 3, 4], el => !!(el % 2))) // 只给callback传了第一个参数,少于形参要求的三个参数
    

2.2.2 数组推断

  • 字面量创建数组将被推测为数组:

    在不进行类型注释时ar不会被推断为元组

    const ar = [1, 2, 3]
    
  • 数组长度推断:

    在对数组进行推断时,默认认为数组类型大小是不固定的(参考“TypeScript对象”博文)。这也是为什么我们特别地提出元组中元素个数是确定的的原因。

    const add = (a: number, b: number) => a + b
    
    const ar = [1, 2]
    console.log(add(...ar)) // 报错,推测ar为number数组类型且个数不确定,展开后不是确定的两个元素,与函数形参要求不匹配。
    
    const ar = [1, 2] as const
    console.log(add(...ar)) // 合法,使用类型断言将数组推断为元组,此时数组元素个数确定
    

2.2.3 值类型推断

值类型推断优先度最低,如果不显示地进行类型注释,则一定不会被推断为值类型

let str = "hello" // 推断为string类型而不是“hello”类型
let str2: "hello" = "hello" // 显示指定str2为“hello”类型

3.类型断言

类型断言用在表达式之后,是一种显示类型转化的方法

3.1 类型推断断言

  • 类型注释优先级大于类型推断断言:

    一旦进行了类型注释,再进行类型断言不会影响变量最终类型的推理。下面的例子虽然断言num是any,但是已经确认了num是number类型,所以最终num是number类型,访问length属性是非法的。

    const num: number = 123 as any
    num.length // 报错,num是number类型
    

    下面的例子没有进行类型注释,此时使用类型断言会影响类型推断过程的走向。此时num被推测为any类型,访问length合法。

    const num = 123 as any
    num.length // 合法
    

    ☆☆下面的例子很有代表性,默认字面声明数组类型会被推断为数组,但是会认为数组长度未知,在使用展开符展开后作为实参传给函数,函数会认为你传递了未知数量的参数。这时候就需要类型断言,使用特殊的类型断言as const说明对象是只读的,因此每个元素的类型固定下来,长度也固定下来,这时候数组会被推断为元组。这样就可以展开后作为剩余实参传给函数。

    const ar = [1, 2] as const // as断言使得ar被推断为元组
    const add = (a: number, b: number) => a + b
    add(...ar) // 合法
    

    ☆☆下面的例子也有代表性。在理解了上面的例子后可以知道,一旦进行了类型注释,那么son就是Father类型。

    interface Father {
        name: string,
        age: number
    }
    
    interface Son extends Father {
        gender: string,
        getInfo: () => void
    }
    
    const son: Father = { 
       name: 'Danny',
       age: 21
    } as Son
    
    son.getInfo() // 报错,son是Father类型,没有getInfo方法
    son.gender // 报错,son是Father类型,没有gender属性
    
  • 类型推断断言不能用于无交集的类型:

    类型断言可以将123推断为any,也可以将any推断为123。即可以在具象和非具象之间断言,但是不能在无关类型间断言。

    下面的例子说明了字符串不可能被推断为数值。

    const variable = 'hello' as number // 报错,我们希望将variable推断为number类型,但是这一定不可能发生
    

    下面的例子说明了数组不可能被推断为元组。

    const ar: Array<number> = [1, 2] as const // 报错,我们希望将ar推断为元组,但是已经指定了ar为数组,数组类型长度在TypeScript中被认为是不确定的,所以ar一定不可能被推断为元组
    
    const arr: ReadonlyArray<number> = [1, 2] as const // 报错,理由同上(如果不了解as的机理,这种结果对于初学者可能难以理解)
    
  • 显示类型转化:

    除去JavaScript中的显示类型转化,TypeScript中显示类型转化有两种:(1)使用类型断言(2)在变量前使用<>改变其类型

    const ar = [1, 2] as const
    const ar = <const>[1, 2]
    
    const ar: ReadonlyArray<number> = [1, 2, 3]
    const arr: Array<number> = [4, 5, 6]
    

3.2 非空赋值断言

  • 类型注释优先级大于非空赋值断言:

    还是和类型推断断言as一样,类型注释一旦确定,我们无法再影响类型推断

    const str: null = null ! 
    console.log(str.length) // 报错,// 我们希望str被推断为非空,但是类型注释已经说明str为空,因此str一定不会被推断为非空
    

    下面这个例子说明非空赋值断言的应用场景

    const str = Math.random() + 0.5 > 0.5 ? 'hello' : null !
    
    console.log(str.length) // 合法,Math.random() + 0.5一定大于0.5,我们希望str被推断为非空。否则str会被推断为联合类型
    

    下面这个例子说明了非空赋值断言可以被类型推断断言替代

    const str = (Math.random() + 0.5 > 0.5 ? 'hello' : null) as string
    
    console.log(str.length) // 合法
    

4.类型操作符

4.1 keyof操作符

keyof操作符用于获取对象的键的类型

  • keyof用法:

    keyof操作符用于操作一个类型,返回该类型的所有键对应的类型。

    interface User {
    	name: string,
        age: number
    }
    type UserKey = keyof User
    const user: UserKey = 'name' // 合法
    const user2: UserKey = 'age' // 合法
    const user3: UserKey = '123' // 非法,UserKey的类型就是"name" | "age",其它任何值都不能取
    
  • keyof操作对象:

    注意类型操作符keyof操作的是类型,不能操作值

    const obj = {
        name: "Danny",
        age: 21
    }
    type ObjAlias = keyof obj // 报错,obj是值不是类型
    
  • keyof操作结果:

    因为对象的键是确定的,因此操作结果只可能是“数值类型”或“字符串值类型”或“符号值类型”。

    const alias = Symbol('alias')
    
    interface User {
        name: string,
        age: number,
        [alias]: symbol,
        [index: number]: string
    }
    
    type Type = keyof User
    
    // 操作结果是符号,是值类型Symbol('alias')
    const user: Type = alias
    // 操作结果是字符串,是值类型“name”
    const user2: Type = "name"
    // 操作结果是number
    const user3: Type = 123
    

4.2 typeof操作符

typeof操作符用于获取值的类型

  • typeof用法:

    typeof操作符与keyof操作符恰好相反,typeof操作符作用于值类型。

    在TypeScript中修改了typeof操作符的功能,不再是像JavaScript中只返回字符串,而是会返回类型。

    const num: number = 123
    type Num = typeof num
    const myNum: Num = 456
    
    // typeof作用于对象不会返回object或Object
    const user = {
        name: 'Danny',
        age: 21,
        gender: 'male'
    }
    type User = typeof user
    const myUser: User = {
        name: 'Cathy',
        age: 21,
        gender: 'male'
    }
    

4.1 索引签名

索引签名指定了对象的键和值分别应该为什么类型

  • 键只能指定为数值或字符串类型:

    interface User {
        [index: number]: string // 合法
    }
    interface User {
        [index: string]: any // 合法
    }
    interface User {
        [index: symbol]: string // 非法,索引签名中键的类型只能被指定为number或string
    }
    
  • 索引签名引起的冲突:

    • 索引签名只应存在一个:

      interface User {
          [index: number]: string // 合法
          [index: string]: string // 合法,但没有意义。number键名会默认转化为string类型
      }
      
      interface User {
          [index: number]: string // 合法
          [index: string]: number // 报错,索引签名不能同时满足值既是string又是number
      }
      
    • 索引签名和已有属性冲突:

      interface User {
      	age: number, // 报错,下面的索引签名要求了所有键对应的值都应该为string类型
      	[index: string]: string
      }
      

4.4 索引访问操作符

索引访问操作符用于获取对象值的类型

  • 索引访问操作符用法:

    索引访问操作符是[],其中接收某一种类型。通过这种类型去对象(接口或别名)中寻找其对应的值。

    这里容易理解错误的是形如User["name"],你可能认为这里的“name”只是字符串,但是实际上它是“name”值类型。这也是解释了为什么在处理符号类型的键时要使用typeof alias的原因,因为[]操作符中只能接收类型而不是值。

    const alias = Symbol.for('alias')
    interface User {
        name: string,
        age: number,
        [alias]: boolean,
    }
    
    type UserAliasType = User[typeof alias] // 注意这里不是alias,是symbol.for('alias')类型,因为[]接收类型而不是值
    type UserNameType = User["name"] // 这里的“name”不是字符串,而是“name”值类型
    type UserAgeType = User["age"] // 这里的“age”同理也是“age”值类型
    
    const myAlias: UserAliasType = true
    const myName: UserNameType = 'Danny'
    const myAge: UserAgeType = 21
    
  • 索引访问操作符访问索引签名:

    访问索引签名键对应的值时,可以通过number,string或具体的number值类型来进行访问

    // 设定索引键为string
    interface User {
        name: string
        [index: string]: string
    }
    type UserSignType = User[string] // 合法,通过string类型访问
    const mySign: UserSignType = 'hello'
    
    // 设定索引键为number
    interface User {
        name: string
        [index: number]: string
    }
    type UserSignType = User[number] // 合法,通过number类型访问
    type UserDetailType = User[1] // 合法,通过number值类型访问
    const mySign: UserDetailType = 'hello'
    

    复杂的类型提取示例(源自TypeScript官网)

    const MyArray = [
      { name: "Alice", age: 15 },
      { name: "Bob", age: 23 },
      { name: "Eve", age: 38 },
    ];
    
    type Age = typeof MyArray[number]["age"]; // 先通过typeof得到Array类型,再通过[number]访问索引类型得到{ name: string, age: number}类型,再通过["age"]索引访问得到number类型
    

你可能感兴趣的:(TypeScript,typescript,javascript)