为什么我的 Typescript 对象上允许这个额外的属性?

Why is this extra property allowed on my Typescript object?

提问人:MaxAxeHax 提问时间:3/13/2019 最后编辑:MaxAxeHax 更新时间:3/13/2019 访问量:8518

问:

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

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

目前,我似乎已经找到了类型系统能够实现的极限,但它似乎不一致,我也可能只是使用了错误的语法。

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

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

这是我的方法的最小化版本(或直接链接到 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 类型 typescript-generic

评论


答:

3赞 James Monger 3/13/2019 #1

如果你想象这是一个这样的界面:fields

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

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

然后这失败了,因为它与该接口匹配 - 它没有 or 属性:ModelSize

fields: {
    extra: string
}

但是,这通过:

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

因为匿名接口,所以有接口的扩展。它看起来像这样:Fields

interface ExtendedFields extends Fields {
    extra: string;
}

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

评论

0赞 MaxAxeHax 3/13/2019
谢谢你的解释,它为我澄清了一些事情。我想我对创建类型化对象文字的情况感到困惑,其中编译器(当然)不允许对象添加不属于声明类型的属性。
9赞 Titian Cernicova-Dragomir 3/13/2019 #2

这是预期行为。基本接口仅指定最低要求是什么,打字稿中没有要求实现类字段和接口字段之间的精确匹配。出现错误的原因不是存在,而是缺少其他字段。(底部错误是fieldVehicle2extraProperty '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 // 
    }
}

评论

0赞 MaxAxeHax 3/13/2019
非常酷的解决方案IMO。这有点骇人听闻,对于非类型系统的书来说并不容易理解:D例如,请问在哪里可以了解有关条件类型检查的更多信息?我想我在 TS 手册中没有看到任何这些内容。
1赞 Titian Cernicova-Dragomir 3/13/2019
@MaxAxeHax 嗯 ..不确定。我阅读了 PR 并虔诚地关注了 GitHub 项目。此外,我喜欢做很多实验,在这里回答问题有助于我积累知识。有官方文档,但这些文档的解释非常狭窄,更多的是对语言功能的简要描述,他们没有深入探讨你可以用它做什么有趣的事情。