Why is this extra property allowed on my Typescript object?
This is the expected behavior. The base interface only specifies what the minimum requirements for field
are, there is not requirement in typescript for an exact match between the implementing class field and the interface field. The reason you get an error on Vehicle2
is not the presence of extra
but rather that the other fields are missing. (The bottom error is Property 'S_MODEL' is missing in type '{ extra: string; }'.
)
You can do some type trickery to get an error if those extra properties are present using conditional types:
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 //
}
}
If you imagine that fields
is an interface like so:
interface Fields {
Model: string;
Size: number;
}
(It's done anonymously, but it does match this interface because of your [key in Vehicle.Fields]: any
)
Then this fails, because it doesn't match that interface - it doesn't have a Model
or a Size
property:
fields: {
extra: string
}
However, this passes:
fields: {
Model: string;
Size: number;
extra: string
}
Because the anonymous interface there is an extension of your Fields
interface. It would look something like this:
interface ExtendedFields extends Fields {
extra: string;
}
This is all done anonymously through the TypeScript compiler, but you can add properties to an interface and still have it match the interface, just like an extended class is still an instance of the base class