TypeScript中联合类型的类型保护

在TypseScript中引入Redux给action加类型验证的时候我们考虑对于同一个请求的三个action的type是不是可以直接将类型设置为string。
例如:

export interface ActionType {
  type: string;
}

export interface RequestManufacturerSuccess extends ActionType {
  payloadUsAs: Manufacturer[];
}

export interface RequestManufacturerFailed extends ActionType {
  payloadUsAs: any;
}

export type RequestManufacturerAction =
    ActionType | RequestManufacturerSuccess | RequestManufacturerFailed;

但是我们在reducer中发现如果给action的type直接限定类型为string时,reducer无法通过不同type找到对应的不同的actionType,代码如下:

const initState: ManufacturerState = { manufacturers: [], error: null, loading: false };

function reducer(state = initState, action: RequestManufacturerAction): ManufacturerState {
  switch (action.type) {
    case REQUEST_MANUFACTURERS:
      return { ...state, loading: true };
    case REQUEST_MANUFACTURERS_SUCCESS:
      // 会提示 property action.payload does not exist on type ActionType
      return { ...state, manufacturers: action.payload, loading: false };
    case REQUEST_MANUFACTURERS_FAILED:
      // 会提示 property action.payload does not exist on type ActionType
      return { ...state, error: action.payload, loading: false };
    default:
      return state;
  }
}

这里就涉及到了TypeScript union type的概念和特性了:

联合类型概念:

union type基本形式:使用 | 分隔不同类型

export type pad = number | string

表示如果一个变量或参数或函数是pad类型则它可以为number类型或者string类型;

export type RequestManufacturerAction =
    ActionType | RequestManufacturerSuccess | RequestManufacturerFailed;

同理,上面的代码表示如果一个变量或参数或函数是RequestManufacturerAction类型则它可以为ActionType类型或者RequestManufacturerSuccess类型或RequestManufacturerFailed类型。

但是在reducer中使用这个联合类型时,我们想通过传入的action的不同type让它自动识别对应到不同的actionType使得我们能够获取该类型的属性时,它无法做到这一点。

实际上,如果我们使用了union type,我们在使用该联合类型的参数时,只能使用联合类型中所有类型的公共部分。例如:

//types.ts
export interface Bird {
  type: string;
  fly: () => string;
  layEggs: () => string;
}

export interface Fish {
  type: string;
  swim: () => string;
  layEggs: () => string;
}

export type SmallPet = Bird | Fish;
//index.ts
import {Bird, Fish, SmallPet} from "./types";

function distinguishPet(smallPet: SmallPet) {
  switch (smallPet.type) {
    case 'bird':
        return smallPet.fly; //errors
    case 'fish':
        return smallPet.swim; //errors
    default:
      return smallPet.layEggs; //okay
  }
}

联合类型在这里看来似乎有点复杂,如果我们有A | B这样的类型,我们只能确定A和B共有的部分,在这个例子中,Bird 有一个fly成员,但是我们无法确定Bird | Fish类型中有fly成员。如果变量确实为Fish类型,那么在运行时smallPet.fly会失败。

Type Guard 和 Differentiating Types

我们可以通过Type Guard 和 Differentiating Types的方式去确保union type的各种情况,使其可以使用不同类型的属性:

类型断言

为了使上面的情况可以工作,我们可以使用类型断言的方式:

function distinguishPet(smallPet: SmallPet) {
  switch (smallPet.type) {
    case 'bird':
        return (smallPet as Bird).fly;
    case 'fish':
      if ("swim" in smallPet) {
        return (smallPet as Fish).swim;
      }
  }
}

自定义 Type Guards

Type Predicates

定义一个 type guard,我们只需要定义一个返回类型为type predicate的函数
例如:

function distinguishPet(smallPet: SmallPet) {
  switch (smallPet.type) {
    case 'bird':
      if (isBird(smallPet)) {
        return smallPet.fly;
      }
    case 'fish':
      if (isFish(smallPet)) {
        return smallPet.swim;
      }
    default:
      return smallPet.layEggs;
  }
}

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

function isBird(pet: Fish | Bird): pet is Bird {
  return (pet as Bird).fly !== undefined;
}

不论何时isFish被调用,TypeScript会进一步缩小变量类型的范围到一个比较精确的类型。

'in' 操作符

in 操作符在这里作为缩小类型的表达式

function distinguishPet(smallPet: SmallPet) {
  switch (smallPet.type) {
    case 'bird':
      if ("fly" in smallPet) {
        return smallPet.fly;
      }
    case 'fish':
      if ("swim" in smallPet) {
        return smallPet.swim;
      }
    default:
      return smallPet.layEggs;
  }
}

typeof type guards

const BIRD = 'BIRD';
const FISH = 'FISH';

export interface Bird {
  type: typeof BIRD;
  fly: () => string;
  layEggs: () => string;
}

export interface Fish {
  type: typeof FISH;
  swim: () => string;
  layEggs: () => string;
}

export type SmallPet = Bird | Fish;
import {SmallPet} from "./types";

function distinguishPet(smallPet: SmallPet) {
  switch (smallPet.type) {
    case 'BIRD':
        return smallPet.fly;
    case 'FISH':
        return smallPet.swim;
    default:
      const {layEggs} = smallPet;
      return layEggs;
  }
}

在这里TypeScript会把typeof识别为它自己的一个type guards.

instanceof type guards

export interface Tree{
  getTreeNumber(): string;
}
import {Tree} from "./types";

export class TreesIncrement implements Tree{
  constructor(private treeNumber: number){}
  getTreeNumber(){
    return Array(this.treeNumber+1).join(" ");
  }
}

export class TreesNumber implements Tree{
  constructor(private treeNumber: string){}
  getTreeNumber(){
    return this.treeNumber;
  }
}

function getRandomTreeNumber() {
  return Math.random() < 0.5 ? new TreesIncrement(4) : new TreesNumber("3");
}

let tree: Tree = getRandomTreeNumber();

if(tree instanceof TreesIncrement){
  console.log(tree);
}

if(tree instanceof TreesNumber){
  console.log(tree);
}

instanceof 右边需要是一个contructor函数。instanceof type guards 是一个使用其contructor函数实现精确type的方法。

参考链接:https://www.typescriptlang.org/docs/handbook/advanced-types.html#union-types

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