Discriminated unions for Customization Args
See original GitHub issueThe customization args in our codebase are currently being migrated to use union types instead of the pre-existing convention of any
. For example:
https://github.com/oppia/oppia/blob/dec1135d9da71daec3f00f268657a6a10f239b6d/core/templates/services/state-interaction-stats.service.ts#L41-L44
https://github.com/oppia/oppia/blob/dec1135d9da71daec3f00f268657a6a10f239b6d/core/templates/services/state-interaction-stats.service.ts#L48-L64
While this works, it still allows objects to be constructed incorrectly. For example, the following code would compile fine:
var fractionAnswer: IFractionDict = { /* ... */ };
var vizInfo: IVisualizationInfo = {
data: [fractionAnswer],
id: 'TextInput',
}
Describe the solution you’d like I’d like to strengthen the types to use discriminated unions instead. Doing so would make the code above a compile-time error, which could save developers a lot of time and effort; especially with more complicated interfaces.
To illustrate, here’s the previous example with discriminated unions:
export interface IFractionInputVisualizationInfo {
id: 'FractionInput'; // The discriminant
data: IFractionDict[];
}
export interface ITextInputVisualizationInfo {
id: 'TextInput'; // The discriminant
data: string[];
}
type IVisualizationInfo = ( // The union
ITextInputVisualizationInfo |
IFractionInputVisualizationInfo);
// Now, this becomes a compiler error:
var fractionAnswer: IFractionDict;
var vizInfo: IVisualizationInfo = {
data: [fractionAnswer],
id: 'TextInput',
}
Note that discriminated unions of interfaces can not be extended. However, classes can also contain discriminants:
class TextInputAnswer {
visualizationId: 'TextInput'; // The discriminant
// ...
}
class FractionInputAnswer {
visualizationId: 'FractionInput'; // The discriminant
// ...
}
type Answer = TextInputAnswer | FractionInputAnswer;
But this comes with a caveat: the value of the discriminant field is undefined
.
This is because discriminants act as types, not values. To have the discriminant behave as expected, we’d need to do an actual assignment:
class TextInputAnswer1 {
visualizationId: 'TextInput';
}
class TextInputAnswer2 {
visualizationId: 'TextInput' = 'TextInput';
}
new TextInputAnswer1().visualizationId; // undefined
new TextInputAnswer2().visualizationId; // 'TextInput'
It’s my personal opinion that this confusing duplication is outweighed by the compile-time safety this convention would bring us.
Places where this is applicable
- Interaction Answers
- Interaction Rules
- Interaction Customization Args
- core/templates/domain/statistics/LearnerAnswerDetailsObjectFactory.ts
Issue Analytics
- State:
- Created 3 years ago
- Comments:19 (19 by maintainers)
Top GitHub Comments
@nishantwrp do you have plans to implement this for Answer objects? I’d like to start updating all answers to meet the following pattern:
For specs, we can test the
invalidBackendDict
branch by passing in an invalid dict with a type ofany
. Note: usingany
is the only way to bypass the compiler errors that would otherwise protect us from passing in such a dict./cc @seanlip @vojtechjelinek
To be clear, I’m not asking you to do everything by yourself – I’m happy and willing to share the workload. However, I’d like you to make the call on how to split this work given that it’s so intimately related to your project.
Thanks!
@brianrodri I’ve updated the places where this needs to be done in the issue description. Please add things there so we can work on them later. Thanks!