如何在不知道T类型的情况下为通用类型List <T>编写“ isX”类型防护?

时间:2019-08-26 02:29:45

标签: typescript typescript-generics typeguards

我正在用TypeScript编写类型化List实现,这是一个普通对象,其结构使用type表达式来确保。我选择将其编写为普通对象而不是类,因为我认为普通对象将更容易使我确保其不变性。该对象的类型定义如下:

type List<T> = Readonly<{
  head: T;
  tail: List<T> | void;
}>;

我想为此结构编写一个类型保护,以便可以在其他代码中确保对List对象进行操作。我当前的类型防护如下所示:

export function isList(list: any): list is List {
  if (isNil(list)) {
    return false;
  }

  return ("head" in list && "tail" in list);
}

此实现在第一行list is List处有一个错误:TypeScript抱怨类型List<T>是通用的,因此,我必须在对其的引用中提供一个类型。

此函数的预期输出将遵循以下内容:

import {getListSomehow, isList} from "list";

const list = getListSomehow("hello", "world", "etc...");
const arr = ["hello", "world", "etc..."];

console.log(isList(list)); // true
console.log(isList(arr)); // false
console.log(isList("a value")); // false

有什么方法可以编写这种类型的防护,而不必知道T的类型吗?如果不是,是否有办法以某种方式检索此类型,同时仍允许该函数取任何值?

1 个答案:

答案 0 :(得分:1)

这个问题暴露了TypeScript的一些缺点。简短的答案是:您可能无法做您想做的事情,至少不能以您尝试做的方式做。 (我们在对该问题的评论中对此进行了一些讨论。)

出于类型安全的考虑,TypeScript通常依赖于编译时类型检查。与JavaScript不同,TypeScript标识符具有类型。有时这是明确给出的,而其他时候是由编译器推断的。通常,如果您尝试将标识符视为不同于其已知类型的类型,则编译器会抱怨。

这给与现有JavaScript库进行接口带来了问题。 JavaScript中的标识符没有类型。此外,不可能在运行时可靠地检查值的类型。

这导致了类型保护器的出现。可以在TypeScript中编写一个函数,其目的是告诉编译器,如果该函数返回true,则传递给它的参数之一被称为特定类型。这使您可以实现自己的鸭子类型,作为JavaScript和TypeScript之间的一种联系。类型保护功能看起来像这样:

const isDuck = (x: any): x is Duck => looksLikeADuck(x) && quacksLikeADuck(x);

这不是一个很好的解决方案,但是只要您仔细检查类型,它就可以工作,并且实际上没有其他选择。

但是,类型保护不适用于泛型类型。请记住,类型保护的目的是接受any输入并确定它是否为特定类型。我们可以通过泛型类型来达到目标​​:

function isList(list: any): list is List<any> {
    if (isNil(list)) {
        return false;
    }

    return "head" in list && "tail" in list;
}

但是,这仍然不是理想的。现在,我们可以测试某个事物是否为List<any>,但是我们无法测试某个更具体的事物,例如List<number> -如果没有它,结果可能对我们没有特别的帮助,因为我们所有封装的值仍然是未知类型。

我们真正想要的是可以检查某物是否为List<T>的东西。有点棘手:

function isList<T>(list: any): list is List<T> {
    if (isNil(list)) {
        return false;
    }

    if (!("head" in list && "tail" in list)) {
        return false;
    }

    return isT<T>(list.head) && (isNil(list.tail) || isList<T>(list.tail));
}

现在我们必须定义isT<T>

function isT<T>(x: any): x is T {
    // What goes here?
}

但是我们不能那样做。我们无法在运行时检查值是否为任意类型。我们可以通过以下方式来破解:

function isList<T>(list: any, isT: (any) => x is T): list is List<T> {
    if (isNil(list)) {
        return false;
    }

    if (!("head" in list && "tail" in list)) {
        return false;
    }

    return isT(list.head) && (isNil(list.tail) || isList<T>(list.tail, isT));
}

现在是呼叫者的问题:

function isListOfNumbers(list: any): list is List<number> {
    return isList<number>(list, (x): x is number => typeof x === "number");
}

这都不是理想的。如果可以避免,应该;而是依靠TypeScript的严格类型检查。我提供了示例,但首先,我们需要调整List<T>的定义:

type List<T> = Readonly<null | {
    head: T;
    tail: List<T>;
}>;

现在,有了这个定义,而不是:

function sum(list: any) {
    if (!isList<number>(list, (x): x is number => typeof x === "number")) {
        throw "Panic!";
    }

    // list is now a List<number>--unless we wrote our isList or isT implementations incorrectly.

    let result = 0;

    for (let x = list; x !== null; x = x.tail) {
        result += list.head;
    }

    return result;
}

使用:

function sum(list: List<number>) {
    // list is a List<number>--unless someone called this function directly from JavaScript.

    let result = 0;

    for (let x = list; x !== null; x = x.tail) {
        result += list.head;
    }

    return result;
}

当然,如果您正在编写库或使用纯JavaScript,则可能没有处处都有严格的类型检查,但是您应该尽可能地依靠它。