【问题标题】:Why is this extra property allowed on my Typescript object?为什么我的 Typescript 对象允许这个额外的属性?
【发布时间】:2019-08-03 21:25:24
【问题描述】:

我们最近开始在我们的网络平台项目中使用 typescript。

一个巨大的优势应该是强大的类型系统,它允许在编译时检查各种正确性(假设我们努力正确地建模和声明我们的类型)。

目前,我似乎发现了类型系统所能达到的极限,但似乎不一致,我也可能只是使用了错误的语法。

我正在尝试对我们的应用将从后端接收的对象类型进行建模,并使用类型系统让编译器检查应用中的所有位置:

  1. 结构,即 TS 编译器只允许对某个类型的对象使用现有(枚举)属性
  2. 属性类型检查,即 TS 编译器知道每个属性的类型

这是我的方法的最小化版本(或采取direct link to TS playground

interface DataObject<T extends string> {
    fields: {
        [key in T]: any   // Restrict property keys to finite set of strings
    }
}

// Enumerate type's DB field names, shall be used as constants everywhere
// Advantage: Bad DB names because of legacy project can thus be hidden in our app :))
namespace Vehicle {
    export enum Fields {
        Model = "S_MODEL",
        Size = "SIZE2"
    }
}

// CORRECT ERROR: Property "SIZE2" is missing
interface Vehicle extends DataObject<Vehicle.Fields> {
    fields: {
        [Vehicle.Fields.Model]: string,
    }
}

// CORRECT ERROR: Property "extra" is not assignable
interface Vehicle2 extends DataObject<Vehicle.Fields> {
    fields: {
        extra: string
    }
}

// NO ERROR: Property extra is now accepted!
interface Vehicle3 extends DataObject<Vehicle.Fields> {
    fields: {
        [Vehicle.Fields.Model]: string,
        [Vehicle.Fields.Size]: number,
        extra: string  // Should be disallowed!
    }
}

为什么第三个接口声明没有抛出错误,而编译器似乎完全能够在第二种情况下禁止无效的属性名称?

【问题讨论】:

    标签: typescript types typescript-generics


    【解决方案1】:

    这是预期的行为。基本接口仅指定field 的最低要求是什么,打字稿中没有要求实现类字段和接口字段之间的精确匹配。您在Vehicle2 上收到错误的原因不是extra 的存在,而是缺少其他字段。 (底部错误为Property 'S_MODEL' is missing in type '{ extra: string; }'.

    如果使用条件类型存在这些额外的属性,您可以使用一些类型技巧来获取错误:

    interface DataObject<T extends string, TImplementation extends { fields: any }> {
        fields: Exclude<keyof TImplementation["fields"], T> extends never ? {
            [key in T]: any   // Restrict property keys to finite set of strings
        }: "Extra fields detected in fields implementation:" & Exclude<keyof TImplementation["fields"], T>
    }
    
    // Enumerate type's DB field names, shall be used as constants everywhere
    // Advantage: Bad DB names because of legacy project can thus be hidden in our app :))
    namespace Vehicle {
        export enum Fields {
            Model = "S_MODEL",
            Size = "SIZE2"
        }
    }
    
    // Type '{ extra: string; [Vehicle.Fields.Model]: string; [Vehicle.Fields.Size]: number; }' is not assignable to type '"Extra fields detected in fields implementation:" & "extra"'.
    interface Vehicle3 extends DataObject<Vehicle.Fields, Vehicle3> {
        fields: {
            [Vehicle.Fields.Model]: string,
            [Vehicle.Fields.Size]: number,
            extra: string // 
        }
    }
    

    【讨论】:

    • 非常酷的解决方案 IMO。对于非类型系统的书呆子来说,这有点 hacky 并且不容易理解:D 请问我可以在哪里了解更多关于条件类型检查的信息?我想我在 TS 手册中没有看到任何内容。
    • @MaxAxeHax 嗯 .. 不确定。我阅读了 PR 并虔诚地关注 GitHub 项目。我也喜欢做很多实验,在这里回答问题可以帮助我积累知识。有官方文档,但它们的解释非常狭窄,更多的是对语言功能的简要描述,它们没有涉及你可以用它做什么有趣的事情。
    【解决方案2】:

    如果你想象fields 是这样一个接口:

    interface Fields {
        Model: string;
        Size: number;
    }
    

    (它是匿名完成的,但由于您的[key in Vehicle.Fields]: any,它确实与此接口匹配)

    然后这会失败,因为它匹配那个接口 - 它没有 ModelSize 属性:

    fields: {
        extra: string
    }
    

    但是,这通过了:

    fields: {
        Model: string;
        Size: number;
        extra: string
    }
    

    因为匿名接口存在Fields 接口的扩展。它看起来像这样:

    interface ExtendedFields extends Fields {
        extra: string;
    }
    

    这一切都是通过 TypeScript 编译器匿名完成的,但是您可以向接口添加属性并使其仍然与接口匹配,就像扩展类仍然是基类的实例一样

    【讨论】:

    • 感谢您的解释,它为我解决了一些问题。我想我对创建类型化对象文字的情况感到困惑,其中编译器(当然)不允许对象添加不属于声明类型的属性。
    猜你喜欢
    • 2014-10-17
    • 2017-10-24
    • 1970-01-01
    • 1970-01-01
    • 2020-08-10
    • 1970-01-01
    • 1970-01-01
    • 2014-09-22
    相关资源
    最近更新 更多