How to avoid `Subsequent property declarations must have the same type` with Interfaces

Viewed 2918

I have some interface, that has an enum field. Also want to make an overload for this interface which will take one more property on the case of some specific enum value.

As an example:

enum CellValueType {
    DATE,
    STRING,
    NUMBER
}


export interface TableCell {
    type: CellValueType;
    value: string; // ...
}

export interface TableCell {
    type: CellValueType.DATE;
    format: string; // Should be exist only on the DATE type
    value: string; // ...
}

But, the problem is using the interface I got an error:

Subsequent property declarations must have the same type. Property 'type' must be of type 'CellValueType', but here has type 'CellValueType.DATE'.

Error is understandable, no questions about it.

The main question is how to avoid it in a legal way.

I know, that I can just use type instead of interface and get this construction:

export type TableCell = {
    type: CellValueType;
    value: string;
} | {
    type: CellValueType.DATE;
    format: string; 
    value: string;
}

Works pretty well, but the problem is that classes cannot implement union types.


Not working as expected, with DATE still compatible with the first signature. To work correctly, it needs the type: CellValueType to be changed to type: Exclude<CellValueType, CellValueType.DATE>

Also, I can create a sub-interface only for the DATE enum type but want also to avoid creating an interface for every case.

1 Answers

When you define two interfaces under the same name, a declaration merge is performed.

export interface TableCell {
    type: CellValueType;
    value: string; // ...
}

export interface TableCell {
    type: CellValueType.DATE;
    format: string; // Should be exist only on the DATE type
    value: string; // ...
}

This is the by-design behavior you encountered when trying to merge CellValueType with CellValueType.DATE. Quoting the handbook:

The compiler will issue an error if the interfaces both declare a non-function member of the same name, but of different types

Now, when you tried to use a union to create the desired type, you encountered another by-design behavior - since the CellValueType.DATE is a member of CellValueType, a wider type is chosen (think about it: A | ... | A | B is the same as A | B):

type isInType = CellValueType.DATE extends CellValueType ? true : false; //true;
type isNotInType = CellValueType extends CellValueType.DATE ? true : false; //false;
type cellType = TableCell["type"]; //CellValueType

With that out of the way, why don't you want to create interfaces that extend the base type? It is a perfectly valid abstraction strategy. Take a look at the example below (note the as const assertion, or the type of CellValueType.DATE will be widened to CellValueType):

export enum CellValueType {
    DATE,
    STRING,
    NUMBER
}

interface TableCell {
    value: string;
}

export interface DateCell extends TableCell {
    type: CellValueType.DATE;
    format: string;
}

export interface NumberCell extends TableCell {
    //number-specific props here
}

export class DateCell implements DateCell {
    format = "yyyy-MM-dd";
    type = CellValueType.DATE as const;
    value = "2021-02-22";
}

Playground

Related