TS学习笔记(八):高级类型

交叉类型

交叉类型将多个类型合并为一个类型,相当于新类型具有这多个类型的所有特性,相当于是一种的操作,通常在使用混入(mixin)的场合使用交叉类型,交叉类型的形式如:

T & U

示例:

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

interface IMan {
  love: string;
  age: number;
}

let mixin: <T, U>(age: T, love: U) => T = function<T, U>(age: T, love: U): T & U {
  return Object.assign(age, love);
}

let me = mixin<IPerson, IMan>({name: 'funlee', age: 10}, { love: 'TS', age: 18});

console.log(me); // {name: "funlee", age: 18, love: 'TS}

联合类型

联合类型用于限制传入的值的类型只能是 | 分隔的每个类型,如:number | string | boolean 表示一个值的类型只能是 number、string、boolean 中的一种。
此外,如果一个值是联合类型,那么我们只能访问它们中共有的部分(共有的属性与方法),即相当于一种的关系,如:

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

interface IMan {
  love: string;
  age: number;
}

let me: IPerson | IMan;
me = {
  name: 'funlee',
  age: 18,
  love: 'TS'
}
console.log(me); // {name: "funlee", age: 18, love: "TS"}
console.log(me.name); // ERROR
console.log(me.age); // 18
console.log(me.love); // ERROR

类型保护与区分类型

联合类型可以让一个值可以为不同的类型,但随之带来的问题就是访问非共同方法时会报错。那么该如何区分值的具体类型,以及如何访问共有成员?

1.使用类型断言

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

interface IMan {
  love: string;
  age: number;
}

let me: IPerson | IMan;
me = {
  name: 'funlee',
  age: 18,
  love: 'TS'
}
console.log(me); // {name: "funlee", age: 18, love: "TS"}

if((me as IPerson).name) {
  console.log((me as IPerson).name); // funlee
}

if((me as IMan).love) {
  console.log((me as IMan).love); // TS
}

2.使用类型保护

为了避免像上例那样写一堆类型断言,我们可以使用类型保护,如写一个类型判断函数:

function isIinterface(obj: IPerson | IMan): obj is IPerson {
  return (obj as IPerson).name !== undefined;
}

这种 param is SomeType 的形式,就是类型保护,我们可以用它来明确一个联合类型变量的具体类型,在调用时 TypeScript 就会将变量缩减为该具体类型,如此一来以下调用就是合法的了:

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

interface IMan {
  love: string;
  age: number;
}

let me: IPerson | IMan;
me = {
  name: 'funlee',
  age: 18,
  love: 'TS'
}

function isIPerson(obj: IPerson | IMan): obj is IPerson {
  return (obj as IPerson).name !== undefined;
}
function isIMan(obj: IPerson | IMan): obj is IMan {
  return (obj as IMan).love !== undefined;
}
console.log(me); // {name: "funlee", age: 18, love: "TS"}
if(isIPerson(me)) {
  console.log(me.name); // funlee
}
if(isIMan(me)) {
  console.log(me.love); // TS
}

3.typeof 和 instanceof

当我们使用了 typeof 和 instanceof 后,TypeScript 就会自动限制类型为某一具体类型,从而我们可以安全地在语句体内使用具体类型的方法和属性。

function show(param: number | string) {
  if (typeof param === 'number') {
    console.log(`${param} is number`)
  } else {
    console.log(`${param} is string`)
  }
}

typeof 用于基本数据类型,instanceof 用于引用类型,对于类,我们则可以使用 instanceof,如:

class Person {
  name: string = 'funlee';
  age: number = 18;
}

class Man {
  age: number = 12;
  love: string = 'TS';
}

let me: Person | Man;
me = Math.random() < 0.5 ? new Person() : new Man();

if(me instanceof Person) {
  console.log(me.name);
}
if(me instanceof Man) {
  console.log(me.love);
}

null 的类型

null 和 undefined 可以赋给任何的类型,因为它们是所有其他类型的一个有效值,如:

let x1: number = null
let x2: string = null
let x3: boolean = null
let x4: undefined = null
let y1: number = undefined
let y2: string = undefined
let y3: boolean = undefined
let y4: null = undefined

在 TypeScript里,我们可以使用 --strictNullChecks 标记,开启这个标记后,当我们声明一个变量时,就不会自动包含 null 或 undefined,如:

// 开启`--strictNullChecks`后
// Type 'null' is not assignable to type 'number'.
let x1: number = null

// Type 'null' is not assignable to type 'string'.
let x2: string = null

// Type 'null' is not assignable to type 'boolean'.
let x3: boolean = null

// Type 'null' is not assignable to type 'undefined'.
let x4: undefined = null

// Type 'undefined' is not assignable to type 'number'.
let y1: number = undefined

// Type 'undefined' is not assignable to type 'string'.
let y2: string = undefined

// Type 'undefined' is not assignable to type 'boolean'.
let y3: boolean = undefined

// Type 'undefined' is not assignable to type 'null'.
let y4: null = undefined

但是我们可以手动使用联合类型来明确包含,如:

et x = 123
x = null // 报错
let y: number | null = 123
y = null // 允许
y = undefined // 报错,`undefined`不能赋值给`number | null`

当开启了 --strictNullChecks,可选参数/属性就会被自动地加上 | undefined,如:

function foo(x: number, y?: number) {
  return x + (y || 0)
}
foo(1, 2) // 允许
foo(1) // 允许
foo(1, undefined) // 允许
foo(1, null) // 报错,不允许将null赋值给`number | undefined`类型

类型别名

类型别名可以给现有的类型起个新名字,它和接口很像但又不一样,因为类型别名可以作用于原始值、联合类型、元组及其他任何需要手写的了类型,语法如:

type 新名字 = 已有类型

如:type Name = string
别名不会新建一个类型,它只会创建一个新的名字来引用现有类型。

泛型别名

别名支持泛型。

type Container<T> = {
  value: T
}

let name: Container<string> = {
  value: 'funlee'
}

但是类型别名不能出现在声明右侧的任何地方,如:

type Alias = Array<Alias> // 报错,别名Alias循环引用了自身

和接口的区别

  1. 错误信息、鼠标悬停时,不会使用别名,而是直接显示为所引用的类型
  2. 别名不能被 extends 和 implements

字符串字面量类型

字符串字面量类型允许我们定义一个别名,类型为别名的变量只能取固定的几个值,如:

type Easing = 'ease-in' | 'ease-out' | 'ease-in-out'
let x1: Easing = 'uneasy' // 报错: Type '"uneasy"' is not assignable to type 'Easing'
let x2: Easing = 'ease-in' // 允许

字符串字面量类型还能用于区分函数重载,如:

function createElement(tagName: 'img'): HTMLImageElement
function createElement(tagName: 'input'): HTMLInputElement
// ... 其他重载函数
function createElement(tagName: string): Element {
    // ...
}

可辨识联合

可以合并字符串字面量类型、联合类型、类型保护和类型别名来创建可辨识联合的高级模式(也称为标签联合或者代数数据类型),具有3个要素:

  1. 具有普通的字符串字面量属性——可辨识的特征
  2. 一个类型别名,用来包含了那些类型的联合——联合
  3. 此属性上的类型保护

创建一个可辨识联合类型,首先需要声明将要联合的接口,每个接口都要有一个可辨识的特征,如(kind属性):

interface Square {
  kind: 'square'
  size: number
}

interface Rectangle {
  kind: 'rectangle'
  width: number
  height: number
}

interface Circle {
  kind: 'circle'
  radius: number
}

现在,各个接口之间还是没有关联的,所以我们需要使用类型别名来联合这几个接口,如

type Shape = Square | Rectangle | Circle;

现在,使用可辨识联合,如:

function area(s: Shape) {
  switch (s.kind) {
    case 'square':
      return s.size * s.size
    case 'rectangle':
      return s.height * s.width
    case 'circle':
      return Math.PI * s.radius ** 2
  }
}

多态的 this

多态的 this 类型表示的是某个包含类或接口的子类型,例子如:

class BasicCalculator {
  public constructor(protected value: number = 0) {
  }
  public currentValue(): number {
    return this.value
  }
  public add(operand: number): this {
    this.value += operand
    return this
  }
  public multiply(operand: number): this {
    this.value *= operand
    return this
  }
}

let v = new BasicCalculator(2).multiply(5).add(1).currentValue() // 11

由于使用了 this 类型,当子类继承父类的时候,新的类就可以直接使用之前的方法,而不需要做任何的改变,如:

class ScientificCalculator extends BasicCalculator {
  public cconstructor(value = 0) {
    super(value)
  }
  public sin() {
    this.value = Math.sin(this.value)
    return this
  }
}
let v = new BasicCalculator(2).multiply(5).sin().add(1).currentValue();

如果没有 this 类型,那么 ScientificCalculator 就不能够在继承 BasicCalculator 的同时还保持接口的连贯性。因为m ultiply 方法会返回 BasicCalculator 类型,而BasicCalculator 没有 sin 方法。然而,使用 this 类型,multiply 就会返回 this,在这里就是 ScientificCalculator。

索引类型

索引类型能使编译器能够检查使用了动态属性名的代码,如:
我们想要完成一个函数,它可以选取对象中的部分元素的值,那么:

function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
  return names.map(n => o[n])
}

interface Person {
  name: string
  age: number
}

let p: Person = {
  name: 'funlee',
  age: 21
}

let res = pluck(p, ['name']) // 允许

以上代码解释如下:

  1. 首先,使用 keyof 关键字,它是索引类型查询操作符,它能够获得任何类型 T 上已知的公共属性名的联合。如例子中,keyof T 相当于 'name' | 'age'
  2. 然后,K extends keyof T 表明 K 的取值限制于 'name' | 'age'
  3. T[K] 则代表对象里相应 key 的元素的类型,所以在例子中,p 对象里的 name 属性,是 string 类型,所以此时 T[K] 相当于 Person[name],即相当于类型 string,所以返回的是 string[],所以 res 的类型为 string[]

所以,根据以上例子,举一反三有:

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]
}
let obj = {
  name: 'funlee',
  age: 21,
  male: true
}
let x1 = getProperty(obj, 'name') // 允许,x1的类型为string
let x2 = getProperty(obj, 'age') // 允许,x2的类型为number
let x3 = getProperty(obj, 'male') // 允许,x3的类型为boolean
let x4 = getProperty(obj, 'hobby') // 报错:Argument of type '"hobby"' is not assignable to parameter of type '"name" | "age" | "male"'.

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

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

interface Demo<T> {
  [key: string]: T
}
let keys: keyof Demo<boolean> // keys的类型为string
let value: Demo<number>['foo'] // value的类型为number

映射类型

我们可能会遇到这么一些需求:

  1. 将一个现有类型的每个属性都变为可选的,如:
interface IPerson {
  name: string
  age: number
}

可选版本为:

interface PersonPartial {
  name?: string
  age?: number
}
  1. 或者将每个属性都变为只读的,如:
interface IPersonReadonly {
  readonly name: string
  readonly age: number
}

而现在 typeScript 为我们提供了映射类型,能够使得这种转化更加方便,在映射类型里,新类型将以相同的形式去转换旧类型里每个属性,如以上例子可以改写为:

type Readonly<T> = {
  readonly [P in keyof T]: T[P]
}
type Partial<T> = {
  [P in keyof T]?: T[P]
}
type PersonReadonly = Readonly<Person>
type PersonPartial = Partial<Person>

我们还可以写出更多的通用映射类型,如:

// 可为空类型
type Nullable<T> {
  [P in keyof T]: T[P] | null
}

// 包装一个类型的属性
type Proxy<T> = {
  get(): T
  set(value: T): void
}
type Proxify<T> = {
  [P in keyof T]: Proxy<T[P]>
}
function proxify(o: T): Proxify<T> {
  // ...
}
let proxyProps = proxify(props)

由映射类型进行推断(拆包)

上面展示了如何包装一个类型,那么与之相反的就有拆包操作,示例如:

function unproxify<T>(t: Proxify<T>): T {
  let result = <T>{}
  for (const k in t) {
    result[k] = t[k].get()
  }
  return result
}
let originalProps = unproxify(proxyProps);
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容