乐趣区

再学-TypeScript-2-高级类型

前言

在之前已经重新的去了解了一下基础类型的一些知识,现在就继续向前,进一步了解下 TypeScript 中的高级类型以及一些用法。

一、字面量类型

  • 字符串字面量类型

    有时候可以直接定义字符串字面量类型简单方便的指定变量的固定值,实现类似枚举类型的字符串:

    type direction = 'up' | 'down' | 'left' | 'right' | 'static'
    
    function move(type: number): direction {switch(type) {
        case 0:
          return 'up'
        case 1:
          return 'down'
        case 2:
          return 'left'
        case 3:
          return 'right'
        default:
          return 'static'
      }
    }
  • 数字字面量类型

    TypeScript 还具有数字字面量类型:

    type nums = 0 | 1 | 2

二、索引类型

当我们需要获取某个对象中的一些属性值时,通常会是这样:

const person = {
  name: 'tom',
  age: 11
}

function getProps(obj: any, keys: string[]) {return keys.map(key => obj[key])
}

getProps(person, ['name', 'age']) // ['tom', 11]

// 当指定的 key 不是对象的 key 时,编译器并没有提示错误
getProps(person, ['sex'])  // [undefined] 

这时候,可以使用索引类型,编译器就能够检查使用了动态属性名的代码:

interface Person {
  name: string;
  age: number;
}

function getProps<T, K extends keyof T>(person: T, keys: K[]): T[K][] {return keys.map(key => person[key])
}

const person = {
  name: 'tom',
  age: 11
}

getProps(person, ['name'])  // ['tom']
getProps(person, ['sex'])  // 报错 不能将类型“"sex"”分配给类型“"name" | "age"”

可以发现,在定义方法 getProps 时,使用了几个操作符,理解下这些操作符的作用

  • 索引类型查询操作符 keyof T

    对于任何类型 Tkeyof T 的结果为 T 上已知的公共属性名的联合类型。例如:

    let personProps: keyof Person // 'name' | 'age'
    // 也就是等同于下面定义
    let personProps: 'name' | 'age'
  • 索引访问操作符 T[K]

    在这里,类型语法反映了表达式语法。这意味着 person['name'] 具有类型 Person['name']。然而,就像索引类型查询一样,你可以在普通的上下文里使用 T[K],这正是它的强大所在。你只要确保类型变量 K extends keyof T 就可以了。例如下面 getProperty 函数的例子:

    function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {return o[name]
    }
    
    // 当你返回 T[K]的结果,编译器会实例化键的真实类型
    // 因此 getProperty 的返回值类型会随着你需要的属性改变
    
    const name = getProperty(person, 'name')  // string 类型
    const age = getProperty(person, 'age')  // number 类型
  • 继承 extends

    K extends keyof T 是泛型约束,泛型变量通过继承某些类型获取某些属性,表示泛型 K 继承 keyof T 的属性名

索引类型和字符串索引签名

keyofT[K] 与字符串索引签名进行交互。如果你有一个带有字符串索引签名的类型,那么 keyof T 会是 string。并且 T[string] 为索引签名的类型:

interface Obj<T> {[key: string]: T
}

let keys: keyof Obj<number> // string 类型
let value: Obj<number>['foo'] // number 类型

// 可以利用索引类型定义属性值为某种类型的对象
const datesObj: Obj<number> = {  
  yesterday: 8,
  today: 9,
  tomorrow: 10
}

// 同样 数组也是适用的
interface Arr<T> {[index: number]: T
}
const datesArr: Arr<number> = [8, 9, 10]

三、映射类型

映射类型是 TypeScript 提供了从旧类型中创建新类型的一种方式,在映射类型里,新类型以相同的形式去转换旧类型里每个属性。

当需要某个类型但是对属性要求不一样时,就可以使用映射类型来指定:

interface Person {
  name: string
  age: number
}

type partial<T> = {[P in keyof T]?: T[P]  // 创建一个映射类型 依赖 Person 类型 但属性是可选的
}

const Jack: partial<Person> = {name: 'jack'}

观察上面示例代码,在定义类型的 key 时,用到了前面的 索引类型查询操作符(keyof,用来遍历每个属性索引,通过 in 操作符指向 P,可以理解成 for ... in

type Keys = 'opt1' | 'opt2'
type Flags = {[K in Keys]: boolean
}
// 类型变量 K,它会依次绑定到每个属性
// 字符串字面量联合的 Keys,它包含了要迭代的属性名的集合
// 属性的结果类型 Flags
// 这个映射类型等同于:type Flags = {
  opt1: boolean
  opt2: boolean
}

四、交叉类型与联合类型

  • 交叉类型:

    交叉类型是将多个类型合并为一个类型。这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。

  • 联合类型:

    联合类型表示一个值可以是几种类型之一。

从这两种类型的定义中,可以知道,交叉类型(A & B)同时具有类型 A 和类型 B 两者的所有属性,可以访问所有子类型的成员;联合类型(X | Y)则是表示值是其中一种类型,只能访问子类型的共有的成员。

interface A {
  name: string
  age: number
  getName: () => string}

interface B {
  name: string
  age: number
  getAge: () => number}

function getIntersection (): A & B {}
function getUnion(): A | B {}

// 交叉类型 具有所有子类型的属性
const x = getVariable()
x.getName()
x.getAge()

// 联合类型 表示值的类型是几种类型之一
// 能确定的就是共有的属性,所以可以直接访问
// 不能访问某个子类型单独具有的属性
const y = getUnion()
y.getName()  // 不存在该属性
y.getAge()  // 不存在该属性
y.name  // 可以访问

在某些情况下,当我们确定一个联合类型的变量是其中哪种类型或者需要在某种类型时会执行类型下的一些方法,如果不是公共成员会报错,这时候就需要用上 类型断言 了:

(<B>y).getName()

在实际开发中,交叉类型用的比较少,更多情况下,用的是联合类型。

五、类型注解、类型推断和类型断言以及类型保护

类型注解

所谓 类型注解,就是人为为一个变量指定类型,例如:

const count: number = 0

在定义变量的时候可以手动给变量添加类型,不过实际上是不需要的,因为 TypeScript 会根据变量值自动推断出变量的类型,如果无法推断就默认为 any 类型,这就是 类型推断。因此,在大部分情况下,我们不需要去写类型注解;但是在某些无法推断的情况下就需要类型注解了,例如函数的参数:

// 参数的类型无法推断 默认其类型为 any
// 所以 x、y 和 sum 都被推断成了 any 类型
function add(x, y) {return x + y}
const sum = add(1, 1)

// 这时就用上类型注解了
// 指定参数的类型是 number ts 会推断出 sum 也是 number 类型
function add(x: number, y: number) {return x + y}
const sum = add(1, 1)

还有一种情况就是变量声明时未赋值,也无法推断:

let z;  // any 类型
z = 9

类型断言

当我们确定某个变量的类型,比定义时推断或注解的更准确,可以通过 类型断言 来手动指定变量的类型。它只是编译阶段起作用,检查代码,并不会实际的进行类型的转换。

const str: any = 'something'
const len1 = <string>str.length  // 尖括号语法
const len2 = (str as string).length  // as 语法

在一些无法确定其具体类型的情况下,在函数实现中,通常需要区分出具体类型:

interface Bird {fly()
  layEggs()}
interface Fish {swim()
  layEggs()}
function getSmallPet(): Fish | Bird {// ...}
let pet = getSmallPet()

pet.fly()  // 错误 类型“Bird | Fish”上不存在属性“fly”pet.swin()  // 错误 类型“Bird | Fish”上不存在属性“swin”// 使用类型断言
if ((<Fish>pet).swim) {(<Fish>pet).swim()} else {(<Bird>pet).fly()}

类型保护

上面例子中,为了通过类型检查,需要多次使用类型断言,明显这不是一种优雅的办法,最好的办法就是一旦检查过类型就记录下来,不需要多次的指定类型。这种就是 类型保护。类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。

  • 自定义类型保护

    要定义一个类型保护,我们只要简单地定义一个函数,它的返回值是一个 类型谓词

    function isFish(pet: Fish | Bird): pet is Fish {return (<Fish>pet).swim !== undefined
    }

    pet is Fish 就是类型谓词,谓词为 parameterName is Type 这种形式,parameterName 必须是来自于当前函数的一个参数名。每当使用一些变量调用 isFish 时,TypeScript 会将变量缩减为那个具体的类型,只要这个类型与变量的原始类型是兼容的。

    if (isFish(pet)) {pet.swim()
    } else {pet.fly()
    }
  • typeof 类型保护

    在 JavaScript 中,通常使用 typeof 来确定基本类型,TypeScript 能识别 typeof 作为类型保护,可以直接在代码里检查类型了。

    function getAttributeValue(attribute: number | string) {if(typeof attribute === 'string') {// ...} else {// ...}
    }

    typeof 类型保护只有两种形式能被识别:typeof v === "typename"typeof v !== "typename"

    "typename"必须是 "number""string""boolean""symbol"

  • instanceof 类型保护

    instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

    类似于typeof 检测基本类型,instanceof 用来检测实例与类的所属关系,也是一种类型保护,是通过构造函数来细化类型的一种方式。

    class NumberValue {constructor(private value: number) { }
      public getValue() {return (this.value * 100).toString()}
    }
    class StringValue {constructor(private value: string) { }
      public getValue() {return (Number(this.value) * 100).toString()}
    }
    function getValue() {return Math.random() < 0.5 ? new NumberValue(1) : new StringValue('2')
    }
    
    const value = getValue()
    if(value instanceof NumberValue) {// ...} else {// ...}
  • in 类型保护

    如果指定的属性在指定的对象或其原型链中,则 in 运算符返回true

    in 操作符可以安全的检查一个对象上是否存在一个属性,它通常也被做为类型保护使用:

    interface X {x: number}
    interface Y {y: number}
    
    function do(arg: X | Y) {if('x' in X) {// ...} else {// ...}
    }
  • 字面量类型保护

    在联合类型里使用字面量类型时,可以直接通过判断联合类型中的公共属性:

    type X {
        name: 'x'
        do: number
    }
    type Y {
        name: 'y'
        do: number
    }
    
    function do(arg: X | Y) {if(arg.name === 'x') {// ...} else {// ...}
    }

六、类型别名 type

TypeScript 使用 type 关键字声明类型别名,类型别名并不会新建一个类型,只是创建一个名字来引用一些类型。

type Name = string
type Container<T> = {value: T}  // 类型别名也可以是泛型
type Tree<T> = {  // 可以使用类型别名来在属性里引用自己
    value: T
    left: Tree<T>
    right: Tree<T>
}

typeinterface

  • 这两者都是类型约束的主要形式,可以限定对象的类型,检查类型。一般在一些简单的类型定义,可以一样使用
  • type 并没有 interface 那么多的使用场景,interface 可以被被 extendsimplements,但是 type 不行。
type TPersion = {
  name: string
  age: number
}

interface IPersion {
  name: string
  age: number
}

const jack: TPersion = {
  name: 'jack',
  age: 20
}

const tom: IPersion = {
  name: 'tom',
  age: 21
}    

interface IMan extends IPersion {log: () => void
}

class Man implements IMan {
  name: string
  age: number
  log() {console.log(this.name)
  }
}

至此,已经简单的梳理了一些高级类型的知识点,这篇其实早就快写完了,前段时间因为工作太忙,然后又自己犯懒鸽了好久,也是一直没搞学习,一眨眼就一个月多过去了。现在空闲点了,再重新搞起。

退出移动版