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<string>的简写,效果相同。

在使用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类型