Possibly an odd question, but i\'m curious if it\'s possible to make an interface where one property or the other is required.
So, for example...
int
You can create few interfaces for the required conditions and join them in a type like here:
interface SolidPart {
name: string;
surname: string;
action: 'add' | 'edit' | 'delete';
id?: number;
}
interface WithId {
action: 'edit' | 'delete';
id: number;
}
interface WithoutId {
action: 'add';
id?: number;
}
export type Entity = SolidPart & (WithId | WithoutId);
const item: Entity = { // valid
name: 'John',
surname: 'Doe',
action: 'add'
}
const item: Entity = { // not valid, id required for action === 'edit'
name: 'John',
surname: 'Doe',
action: 'edit'
}
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
}
})
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);
There're some cool Typescript option that you could use https://www.typescriptlang.org/docs/handbook/utility-types.html#omittk
Your question is: make an interface where either 'text' or attachment exist. You could do something like:
interface AllMessageProperties {
text: string,
attachement: string,
}
type Message = Omit<AllMessageProperties, 'text'> | Omit<AllMessageProperties, 'attachement'>;
const messageWithText : Message = {
text: 'some text'
}
const messageWithAttachement : Message = {
attachement: 'path-to/attachment'
}
const messageWithTextAndAttachement : Message = {
text: 'some text',
attachement: 'path-to/attachment'
}
// results in Typescript error
const messageWithOutTextOrAttachement : Message = {
}
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, so after while of trial and error and googling I found that the answer didn't work as expected for my use case. So in case someone else is having this same problem I thought I'd share how I got it working. My interface was such:
export interface MainProps {
prop1?: string;
prop2?: string;
prop3: string;
}
What I was looking for was a type definition that would say that we could have neither prop1 nor prop2 defined. We could have prop1 defined but not prop2. And finally have prop2 defined but not prop1. Here is what I found to be the solution.
interface MainBase {
prop3: string;
}
interface MainWithProp1 {
prop1: string;
}
interface MainWithProp2 {
prop2: string;
}
export type MainProps = MainBase | (MainBase & MainWithProp1) | (MainBase & MainWithProp2);
This worked perfect, except one caveat was that when I tried to reference either prop1 or prop2 in another file I kept getting a property does not exist TS error. Here is how I was able to get around that:
import {MainProps} from 'location/MainProps';
const namedFunction = (props: MainProps) => {
if('prop1' in props){
doSomethingWith(props.prop1);
} else if ('prop2' in props){
doSomethingWith(props.prop2);
} else {
// neither prop1 nor prop2 are defined
}
}
Just thought I'd share that, cause if I was running into that little bit of weirdness then someone else probably was too.