RichardForrester
RichardForrester

Reputation: 1078

Typescript, interface, type, multiple optional properties, property conditioned on other property

I have need of an interface or type that requires the property updatedAt if the property updatedBy is present (and vis a versa); however, it should allow for neither property to be set.

interface BaseRow {
    id: string
    createdAt: string
    createdBy: string
}

interface Updated {
    updatedAt: string
    updatedBy: string
}

interface UpdatedRow extends BaseRow, Updated {}

type Row = BaseRow | UpdatedRow

Based on the above, I would expect that the following would cause the typescript compiler to throw an error because updatedAt is present but updatedBy is not.

const x: Row = {
    id: 'someID',
    createdAt: 'someISO',
    createdBy: 'someID',
    updatedAt: 'someISO'
}

The above does not however throw any error.

Why doesn't the above solution work as expected? What is the best way to achieve an interface or type that conditionally requires two or more properties?

Typescript Playground

Upvotes: 3

Views: 2282

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249466

According to the PR that implements strict checking on object literals the check that is performed is :

... it is an error for the object literal to specify properties that don't exist in the target type

While the properties are on different branches of the union type, the property exists on the union type. Since the other updated property does not exist, the object literal, does not conform to the UpdatedRow interface, but it does conform to the Row interface, so it is assignable to the variable. This is why there is not error.

A solution, which may or may not be applicable in your case, is to ensure that an updated row is incompatible with a non updated row by adding a string property that is typed to a different string literal type on the two interfaces:

interface BaseRow {
    id: string
    createdAt: string
    createdBy: string
}

interface Updated {
    type: 'u'
    updatedAt: string
    updatedBy: string
}

interface UpdatedRow extends BaseRow, Updated {}

type Row = (BaseRow & { type: 'n'})  | UpdatedRow

const x: Row = { // Error, no updatedBy
    type: 'u',
    id: 'someID',
    createdAt: 'someISO',
    createdBy: 'someID',
    updatedAt: 'someISO'
}

const x2: Row = { // Error, with updatedAt is not assignable to (BaseRow & { type: 'n'}) 
    type: 'n',
    id: 'someID',
    createdAt: 'someISO',
    createdBy: 'someID',
    updatedAt: 'someISO'
}

Upvotes: 4

Related Questions