为什么我的Typescript对象可以使用此额外属性?

时间:2019-03-13 09:01:36

标签: typescript types typescript-generics

我们最近开始在网络平台项目中使用打字稿。

强大的键入系统应该是它的一大优点,它可以在编译时检查各种类型的正确性(假设我们努力地对模型进行正确的建模和声明)。

当前,我似乎已经发现类型系统能够实现的限制,但似乎不一致,而且我可能只是使用了错误的语法。

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

  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!
    }
}

当第二种情况下编译器似乎完全能够拒绝无效的属性名称时,为什么第三个接口声明不引发错误?

2 个答案:

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

答案 1 :(得分: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 // 
    }
}