It appears that what you're seeking is known as a "Discriminated Union". Simply put, this concept allows the type in a union to be narrowed based on the value of a specific property. In your scenario, you are looking for one (or more) types where the options
property is required, and another type where it is optional.
export type QuestionType =
| "DROPDOWN"
| "CHECKBOX"
| "RADIO"
| "SHORT_TEXT"
| "LONG_TEXT";
export interface Option {
id: string;
label: string;
value?: string;
}
export interface Intent {};
export interface BaseQuestion {
[keyName: string]: any;
type: QuestionType;
id: string;
label: string;
helpText?: string;
placeholder?: string;
value?: string;
validations?: any;
required?: boolean;
priority?: number;
intent?: Intent[];
}
export interface TextQuestion extends BaseQuestion {
type: "SHORT_TEXT" | "LONG_TEXT";
options?: Option[];
}
export interface OptionQuestion extends BaseQuestion {
type: "DROPDOWN" | "CHECKBOX" | "RADIO";
options: Option[];
}
export type Question = TextQuestion | OptionQuestion;
const q: Question = {
type: "SHORT_TEXT",
id: "id_foo",
label: "Test Question",
};
You can experiment with this further in the Typescript playground: you'll see that the validator accepts the q
object without an options
property. However, changing the type
property on q
to something other than "SHORT_TEXT"
or "LONG_TEXT"
will prompt an error from the validator.
If you examine q
in the interactive playground, you'll notice that it is identified as a TextQuestion
due to type narrowing. While this prevents altering the q.type
property later, keep in mind that this guard only applies within blocks where the type of the union has been narrowed already. For instance, if you iterate over a list of questions and reassign the type:
(questions as Question[]).forEach(q => {
q.type = "DROPDOWN";
});
In this scenario, Typescript does not narrow the type of each object and won't prevent reassigning the type
.
I hope this information is helpful! ✌️