前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >类型别名与字面量类型_TypeScript笔记10

类型别名与字面量类型_TypeScript笔记10

作者头像
ayqy贾杰
发布2019-06-12 15:11:10
1.1K0
发布2019-06-12 15:11:10
举报
文章被收录于专栏:黯羽轻扬黯羽轻扬

一.类型别名

代码语言:javascript
复制
type PersonName = string;
type PhoneNumber = string;
type PhoneBookItem = [PersonName, PhoneNumber];
type PhoneBook = PhoneBookItem[];

let book: PhoneBook = [
  ['Lily', '1234'],
  ['Jean', '1234']
];

type关键字能为现有类型创建一个别名,从而增强其可读性

接口与类型别名

类型形式上与接口有些类似,都支持类型参数,且可以引用自身,例如:

代码语言:javascript
复制
type Tree<T> = {
    value: T;
    left: Tree<T>;
    right: Tree<T>;
}

interface ITree<T> { 
  value: T;
  left: ITree<T>;
  right: ITree<T>;
}

但存在一些本质差异:

  • 类型别名并不会创建新类型,而接口会定义一个新类型
  • 允许给任意类型起别名,但无法给任意类型定义与之等价的接口(比如基础类型)
  • 无法继承或实现类型别名(也不能扩展或实现其它类型),但接口可以
  • 类型别名能将多个类型组合成一个具名类型,而接口无法描述这种组合(交叉、联合等)
代码语言:javascript
复制
// 类型组合,接口无法表达这种类型
type LinkedList<T> = T & { next: LinkedList<T> };

interface Person {
  name: string;
}
function findSomeone(people: LinkedList<Person>, name: string) {
  people.name;
  people.next.name;
  people.next.next.name;
  people.next.next.next.name;
}

应用场景上,二者区别如下:

  • 接口:OOP场景(因为能被继承和实现,维持着类型层级关系)
  • 类型别名:追求可读性的场景、接口无法描述的场景(基础类型、交叉类型、联合类型等)

二.字面量类型

存在两种字面量类型:字符串字面量类型与数值字面量类型

字符串

字符串字面量也具有类型含义,例如:

代码语言:javascript
复制
let x: 'string';
// 错误 Type '"a"' is not assignable to type '"string"'.
x = 'a';
// 正确
x = 'string';

可以用来模拟枚举的效果:

代码语言:javascript
复制
type Easing = 'ease-in' | 'ease-out' | 'ease-in-out';
class UIElement {
  animate(dx: number, dy: number, easing: Easing) {
    if (easing === 'ease-in') {}
    else if (easing === 'ease-out') {}
    else {
      // 自动缩窄到"ease-in-out"类型
    }
  }
}

// 错误 Argument of type '"linear"' is not assignable to parameter of type 'Easing'.
new UIElement().animate(0, 0, 'linear');

不同的字符串字面量属于不同的具体类型,因此,(如果必要的话)可以这样重载:

代码语言:javascript
复制
function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
function createElement(tagName: string): Element {
  return document.createElement(tagName);
}

数值

数值字面量同样具有类型含义:

代码语言:javascript
复制
// 返回骰子的6个点数
function rollDice(): 1 | 2 | 3 | 4 | 5 | 6 {
  // ...
}

看起来只是个匿名数值枚举,似乎没什么存在必要

存在意义

实际上,字面量类型的意义在于编译时能够结合类型信息“推理”,例如:

代码语言:javascript
复制
function foo(x: number) {
  // 错误 This condition will always return 'true' since the types '1' and '2' have no overlap.
  if (x !== 1 || x !== 2) { }
}

function bar(x: string) {
  // 错误 This condition will always return 'false' since the types '"1"' and '"2"' have no overlap.
  if (x === '1' && x === '2') { 
    //...
  }
}

这种类型完整性补充让TypeScript能够更细致地“理解”(静态分析)代码含义,进而发现一些不那么直接的潜在问题

Nevertheless, by pairing a type with it’s unique inhabitant, singleton types bridge the gap between types and values.

三.枚举与字面量类型

我们知道有一种特殊的枚举叫联合枚举,其成员也具有类型含义,例如:

代码语言:javascript
复制
// 联合枚举
enum E {
  Foo,
  Bar,
}

// 枚举的类型含义
function f(x: E) {
  // 错误 This condition will always return 'true' since the types 'E.Foo' and 'E.Bar' have no overlap.
  if (x !== E.Foo || x !== E.Bar) {
    //...
  }
}

这与字面量类型中的例子非常相似:

代码语言:javascript
复制
function f(x: 'Foo' | 'Bar') {
  // 错误 This condition will always return 'true' since the types '"Foo"' and '"Bar"' have no overlap.
  if (x !== 'Foo' || x !== 'Bar') {
    //...
  }
}

P.S.类比起见,这里用字符串字面量联合类型('Foo' | 'Bar')模拟枚举E,实际上枚举E等价于数值字面量联合类型(0 | 1),具体见二.数值枚举

从类型角度来看,联合枚举就是由数值/字符串字面量构成的枚举,因此其成员也具有类型含义。名称上也表达了这种联系:联合枚举,即数值/字符串联合

P.S.枚举成员类型与数值/字符串字面量类型也叫单例类型(singleton types)

Singleton types, types which have a unique inhabitant.

也就是说,一个单例类型下只有一个值,例如字符串字面量类型'Foo'只能取值字符串'Foo'

四.可区分联合

结合单例类型、联合类型、类型保护和类型别名可以建立一种模式,称为可区分联合(discriminated unions)

P.S.可区分联合也叫标签联合(tagged unions)或代数数据类型(algebraic data types),即可运算、可进行逻辑推理的类型

具体地,可区分联合一般包括3部分:

  • 一些具有公共单例类型属性的类型——公共单例属性即可区分的特征(或者叫标签)
  • 一个指向这些类型构成的联合的类型别名——即联合
  • 针对公共属性的类型保护

通过区分公共单例属性的类型来缩窄父类型,例如:

代码语言:javascript
复制
// 1.一些具有公共单例属性(kind)的类型
interface Square {
    kind: "square";
    size: number;
}
interface Circle {
    kind: "circle";
    radius: number;
}
// 2.定义联合类型,并起个别名
type Shape = Square | Circle;
// 3.具体使用(类型保护)
function area(s: Shape) {
  switch (s.kind) {
    // 自动缩窄到Square
    case "square": return s.size * s.size;
    // 自动缩窄到Circle
    case "circle": return Math.PI * s.radius ** 2;
  }
}

算是对instanceof类型保护的一种补充,都用于检测复杂类型的兼容关系,区别如下:

  • instanceof类型保护:适用于有明确继承关系的父子类型
  • 可区分联合类型保护:适用于没有明确继承关系(运行时通过instanceof检测不出继承关系)的父子类型

完整性检查

有些时候可能想要完整覆盖联合类型的所有组成类型,例如:

代码语言:javascript
复制
type Shape = Square | Circle;
function area(s: Shape) {
  switch (s.kind) {
    case "square": return s.size * s.size;
    // 潜在问题:漏掉了"circle"
  }
}

可以通过never类型来实现这种保障(Never类型为数不多的应用场景之一):

代码语言:javascript
复制
function assertNever(x: never) {
  throw new Error("Unexpected object: " + x);
}
function area(s: Shape) {
  switch (s.kind) {
    case "square": return s.size * s.size;
    // case "circle": return s.radius * s.radius;
    // 错误 Argument of type 'Circle' is not assignable to parameter of type 'never'.
    default: return assertNever(s);
  }
}

如果没有完整覆盖,就会走到default分支把s: Shape传递给x: never引发类型错误(完整覆盖了的话,default就是不可达分支,不会引发never错误)。能够满足完整性覆盖要求,但需要额外定义一个assertNever函数

P.S.关于Never类型的更多信息,见基本类型_TypeScript笔记2

此外,还有一种不那么准确,但也有助于检查完整性的方法:开启--strictNullChecks选项,并标明函数返回值。利用默认返回undefined来保证完整性,例如:

代码语言:javascript
复制
// 错误 Function lacks ending return statement and return type does not include 'undefined'.
function area(s: Shape): number {
  switch (s.kind) {
    case "square": return s.size * s.size;
  }
}

实质上是非空返回值检测,不像assertNever是精确到switch粒度的,相对脆弱(有默认返回值,或有多个switch都会破坏完整性检查)

参考资料

  • Advanced Types
  • SIP-23 – Literal-based singleton types
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-03-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端向后 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一.类型别名
    • 接口与类型别名
    • 二.字面量类型
      • 字符串
        • 数值
          • 存在意义
          • 三.枚举与字面量类型
          • 四.可区分联合
            • 完整性检查
              • 参考资料
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档