Typescript Interface - Possible to make "one or the other" properties required?
You can use a union type to do this:
interface MessageBasics {
timestamp?: number;
/* more general properties here */
}
interface MessageWithText extends MessageBasics {
text: string;
}
interface MessageWithAttachment extends MessageBasics {
attachment: Attachment;
}
type Message = MessageWithText | MessageWithAttachment;
If you want to allow both text and attachment, you would write
type Message = MessageWithText | MessageWithAttachment | (MessageWithText & MessageWithAttachment);
If you're truly after "one property or the other" and not both you can use never
in the extending type:
interface MessageBasics {
timestamp?: number;
/* more general properties here */
}
interface MessageWithText extends MessageBasics {
text: string;
attachment?: never;
}
interface MessageWithAttachment extends MessageBasics {
text?: never;
attachment: string;
}
type Message = MessageWithText | MessageWithAttachment;
// 👍 OK
let foo: Message = {attachment: 'a'}
// 👍 OK
let bar: Message = {text: 'b'}
// ❌ ERROR: Type '{ attachment: string; text: string; }' is not assignable to type 'Message'.
let baz: Message = {attachment: 'a', text: 'b'}
Example in Playground
You can go deeper with @robstarbuck solution creating the following types:
type Only<T, U> = {
[P in keyof T]: T[P];
} & {
[P in keyof U]?: never;
};
type Either<T, U> = Only<T, U> | Only<U, T>;
And then Message
type would look like this
interface MessageBasics {
timestamp?: number;
/* more general properties here */
}
interface MessageWithText extends MessageBasics {
text: string;
}
interface MessageWithAttachment extends MessageBasics {
attachment: string;
}
type Message = Either<MessageWithText, MessageWithAttachment>;
With this solution you can easily add more fields in MessageWithText
or MessageWithAttachment
types without excluding it in another.
Thanks @ryan-cavanaugh that put me in the right direction.
I have a similar case, but then with array types. Struggled a bit with the syntax, so I put it here for later reference:
interface BaseRule {
optionalProp?: number
}
interface RuleA extends BaseRule {
requiredPropA: string
}
interface RuleB extends BaseRule {
requiredPropB: string
}
type SpecialRules = Array<RuleA | RuleB>
// or
type SpecialRules = (RuleA | RuleB)[]
// or (in the strict linted project I'm in):
type SpecialRule = RuleA | RuleB
type SpecialRules = SpecialRule[]
Update:
Note that later on, you might still get warnings as you use the declared variable in your code. You can then use the (variable as type)
syntax.
Example:
const myRules: SpecialRules = [
{
optionalProp: 123,
requiredPropA: 'This object is of type RuleA'
},
{
requiredPropB: 'This object is of type RuleB'
}
]
myRules.map((rule) => {
if ((rule as RuleA).requiredPropA) {
// do stuff
} else {
// do other stuff
}
})