为啥我的 Typescript 对象允许这个额外的属性?

Posted

技术标签:

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

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

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

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

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

    结构,即 TS 编译器只允许对某个类型的对象使用现有(枚举)属性 属性类型检查,即 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!
    


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

【问题讨论】:

【参考方案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 编译器匿名完成的,但是您可以向接口添加属性并使其仍然与接口匹配,就像扩展类仍然是基类的实例一样

【讨论】:

感谢您的解释,它为我解决了一些问题。我想我对创建类型化对象文字的情况感到困惑,其中编译器(当然)不允许对象添加不属于声明类型的属性。

以上是关于为啥我的 Typescript 对象允许这个额外的属性?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 TypeScript 中的类允许使用鸭子类型

返回具有额外属性的 arg 的 TypeScript 函数 (TS2322)

为啥允许使用 kebab-case 非标准属性,而不允许使用其他属性?以及如何在 TypeScript 中定义这样的类型?

为啥 Typescript 允许将 `a: 1, b: 2` 分配给类型 `a: any | b:任何`? [复制]

Typescript接口,强制执行额外属性的类型[重复]

为啥 TypeScript 对象不能用泛型类型索引?