检查打字稿中数组的类型

Check types of an array in typescript

显然,Typescript 似乎与 AST 配合得很好。如果我检查 x.type == "Abc",那么下一行,打字稿知道 x 是类型 Abc。请注意,我用它来检查带有 JSDOC 格式类型注释的 JS 文件。但我想这同样适用于纯打字稿文件

但是,我在测试元素数组时遇到问题。

第一个示例有效,因为我遍历了每个元素,并且仅在检查类型时才推送它。所以 typescript 正确地将类型 Property[] 推断为函数的 return 类型

/**
 * @param {ObjectExpression} objectAst
 */
function getPropertiesList(objectAst) {
    let propertiesList = []
    for (let p of objectAst.value.properties) {
        if (p.type == "Property")
            propertiesList.push(p)
        else
            throw new Error("Properties field has elements that aren't of type `Property`")
    }
    return propertiesList
}

然而,这个示例在功能上是相同的(但在我看来更清晰并且不创建新数组)不起作用。推断类型为 (SpreadElement|Property|ObjectMethod|ObjectProperty|SpreadProperty)[]。所以它没有考虑支票。

/**
 * @param {ObjectExpression} objectAst
 */
function getPropertiesList(objectAst) {
    let propertiesList = objectAst.value.properties
    if (!propertiesList.every(p => p.type == "Property"))
        throw new Error("Properties field has elements that aren't of type `Property`")
    return propertiesList
}

任何人都可以深入了解打字稿如何以不同的方式处理一种情况吗?

Typescript 可以使用检查来使特定类型更具体(如第一个示例所示),但显然它无法对数组执行这些检查。

这可以被认为是 typescript 编译器中的一个错误(因为这两段代码显然应该 return 相同的类型)?

编辑:为了提供一些上下文和可测试性,我从 recast 中导入了如下类型:

/**
 * @typedef { import('recast').types.namedTypes.ObjectExpression} ObjectExpression 
 * @typedef { import('recast').types.namedTypes.Property} Property 
*/

问题是编译器不理解 array.every() 在控制流传递给函数时可以用作 type guard on the type of array. Furthermore, the callback function p => p.type == "Property" is also not inferred to be a type guard on the type of p. The compiler is pretty good about analyzing inline code for potential type narrowing, but it pretty much gives up (see microsoft/TypeScript#9998)

如果您希望 TypeScript 理解调用 boolean 返回函数作为类型保护,您需要手动将此类函数注释为 user-defined type guard。像 foo(x: T): boolean 这样的函数可以更改为 foo(x: T): x is U,其中“x is U" 是一个 类型的谓词 。如果 foo(val) returns true,那么编译器会将 val 缩小为 U。否则不会。

对于回调,这需要将p => p.type == "Property"更改为(p): p is Property => type == "Property"。对于 array.every(),嗯,该方法是 declared in the standard library inside the Array<T> interface. Luckily, you are allowed to merge in extra method overloads to interfaces (beware that if your code is in a module you may have to specifically use global augmentation 添加到全局接口,如 Array<T>)。它看起来像这样:

interface Array<T> {
    every<U extends T>(cb: (x: T) => x is U): this is Array<U>;
}

现在编译器会发现,如果回调是类型保护函数,那么 every() 本身就充当类型保护。您的代码将按预期工作:

function getPropertiesList(objectAst: ObjectAST): Property[] {
    let propertiesList = objectAst.value.properties
    if (!propertiesList.every((p): p is Property => p.type == "Property"))
        throw new Error("Properties field has elements that aren't of type `Property`")
    return propertiesList
}

不过,单次使用 every() 可能工作量太大。实际上,您可能应该只使用 type assertion 并继续。类型断言适用于您比编译器更了解类型的情况;这是使用一个的合理时间:

function getPropertiesListAssert(objectAst: ObjectAST): Property[] {
    let propertiesList = objectAst.value.properties
    if (!propertiesList.every(p => p.type == "Property"))
        throw new Error("Properties field has elements that aren't of type `Property`")
    return propertiesList as Property[]; // assert
}

好的,希望对您有所帮助;祝你好运!

Playground Link to code