如何抽象一个基本接口,其中某些字段确定另一个字段的类型/值,然后从中定义更严格的子接口?
我的应用程序允许用户为其他人填写问卷。在创建表单时,允许用户在5个不同类别的问题之间选择,每幅映射到响应
字段的特定类型。新创建的问卷和已提交的问卷都共享相同的问题
模型,因此,默认情况下,wendment
具有null value/type。而且,只要不需要一个特定的问题才能回答,响应
可能保留为null
。这是当前的问题
模型。
export interface Question {
prompt: string
category: QuestionCategory
options: string[]
isRequired: boolean
response: AnyQuestionResponse
}
export type QuestionCategory = 'TEXT' | 'PARAGRAPH' | 'DATETIME' | 'SINGLE_CHOICE' | 'MULTIPLE_CHOICE'
export type AnyQuestionResponse = string | Date | number | number[] | null
这是类别与响应类型
- 文本 - >字符串
- 段落 - >字符串
- DateTime->日期
- single_choice->编号
- retuers_choice->数字[]
但是,这不是整个故事。就像我上面说的那样,一个问题的iSrequired
字段会影响响应是否可以null
。因此,有两个独立的字段驱动另一种类型。然后将其进一步考虑,如果类别为single_choice
或protedn_choice
,则允许响应值不得超过选项的字段长度(因为它只是索引值用户选择的数组中的任何选项)。
理想情况下,我想拥有类似于特定问题类型的基本问题类型的东西。我对打字稿的经验不太经验,所以在伪代码中,我想是这样的。
export type QuestionCategory = 'TEXT' | 'PARAGRAPH' | ... // Should have an associated child `ResponseType` type
export type IsRequiredType = boolean // Should have an associated child `NullIfFalse` type
export interface BaseQuestion<T=QuestionCategory, R=IsRequiredType> {
prompt: string
category: QuestionCategory
options: string[]
isRequired: R
response: T.ResponseType | R.NullIfFalse // T dictates what its non-null type is while R determines whether it can be null
}
export interface TextQuestion<R=IsRequiredType> extends BaseQuestion<'TEXT', R> { }
// ... ParagraphQuestion, DateQuestion, SingleQuestion ...
export interface MultiChoiceQuestion<R=IsRequiredType> extends BaseQuestion<'MULTIPLE_CHOICE', R> { }
// .................. Example models ..........................
const textQuestion: TextQuestion = {
prompt: "Example",
category: "TEXT", // Response must be string
options: [],
isRequired: false, // Null response allowed
response: 'asdf' // <--- OK
}
const requiredTextQuestion: TextQuestion = {
prompt: "Example",
category: "TEXT", // Response must be string
options: [],
isRequired: true, // Response must not be null
response: null // <--- ERROR WOULD BE THROWN HERE
}
const singleChoiceQuestion: SingleChoiceQuestion = {
prompt: "Example",
category: "SINGLE_CHOICE", // Response must be an integer number
options: ["A", "B", "C"], // Response must be between 0 and 2
isRequired: false, // Null response allowed
response: 3 // <--- ERROR THROWN HERE
}
如果有人对如何实施有想法/想法,我将不胜感激。我觉得这可能比值得的更麻烦,所以我认为无论如何我都不会实施它,但是我确实将其视为学习数据结构策略(如果有的话)的一种练习。另外,我认为找到优化的解决方案很有趣。尽管我可以看到这直接帮助我的类型验证算法,如果/何时QuestionsCategory
将来会引入问题类型。他们目前工作,非常彻底,但非常宽松和无组织。我认为这是一个更清洁的解决方案的验证方式:
type AnyQuestion = TextQuestion | ... | MultiChoiceQuestion
function validate(question: AnyQuestion): boolean {
switch (question.category) {
case 'TEXT':
return validateTextQuestion(question as TextQuestion)
case 'PARAGRAPH':
return validateParagraphQuestion(question as ParagraphQuestion)
case 'DATETIME':
return validateDateQuestion(question as DateQuestion)
// ...
}
// Validation requirements are more constrained in accordance with what the specific question type allows ...
function validateTextQuestion(question: TextQuestion): boolean { ... }
function validateParagraphQuestion(question: ParagraphQuestion): boolean { ... }
function validateDateQuestion(question: DateQuestion): boolean { ... }
// ....
}
与我的工作版本当前片段相比:
export class SubmissionResponsesValidator {
// Checks that all submission's responses field satisfies the following requirements:
// - Is an array of valid Question objects
// - A response is provided if the question is required to answer
// - The response type matches with the question's corresponding category
// - NOTE: An empty array is possible (if all questions unrequired and all responses null)
public static validate(questions: Question[]): string | true {
// 1. Check is array
if (!tc.isArray(questions)) {
return UIError.somethingWentWrong
}
// 2. Check each item is Question type
let allAreQuestions = questions.every(question => this.isQuestion(question))
if (!allAreQuestions) {
return UIError.somethingWentWrong
}
// 3. Check that each question's response is Provided if required to respond
let allResponsesProvided = questions.every(question => this.responseIsProvidedIfRequired(question))
if (!allResponsesProvided) {
return UIError.responsesInvalid
}
// 4. Check that each question's response type matches the category
let allResponseMatchCategory = questions.every(question => this.responseMatchesCategory(question))
if (!allResponseMatchCategory) {
return UIError.somethingWentWrong
}
return true
}
// 2. Checks each item is Question type
private static isQuestion(question: Question): boolean {
let list = ['TEXT', 'PARAGRAPH', 'DATETIME', 'SINGLE_CHOICE', 'MULTIPLE_CHOICE']
if (!tc.isString(question.prompt) ||
!list.includes(question.category) ||
!tc.isArrayOfString(question.options) ||
!tc.isBool(question.isRequired) ||
!this.isResponseType(question.response)) {
return false
}
return true
}
// 2. ...
// Checks that a value is of Question response type
// Must be string / number / date / number[] / null
private static isResponseType(response: AnyQuestionResponse): boolean {
if (tc.isString(response) ||
tc.isNumber(response) ||
tc.isDate(response) ||
tc.isArrayOfNumbers(response) ||
tc.isNull(response)) {
return true
}
return false
}
// 3a. Check that question response is provided if required to respond to
// Note this does not check for type since 2 already does that
private static responseIsProvidedIfRequired(question: Question): boolean {
if (question.isRequired) {
return tc.isDefined(question.response)
}
return true
}
// 3b. Check that question's response matches with its category
private static responseMatchesCategory(question: Question): boolean {
// No need to check for required
if (!question.response) {
return true
}
switch (question.category) {
case 'SINGLE_CHOICE':
return tc.isNumber(question.response)
case 'MULTIPLE_CHOICE':
return tc.isArrayOfNumbers(question.response)
case 'TEXT':
return tc.isString(question.response)
case 'PARAGRAPH':
return tc.isString(question.response)
case 'DATETIME':
return tc.isDate(question.response)
default:
return false
}
}
}
My app allows users to create questionnaires for other people to fill out. While creating a form, users are allowed to select between 5 different categories of questions, and each one maps to a specific type of the response
field. Both a newly created questionnaire and one that has been submitted share the same Question
model, so be default, the response
has null value/type. And as long as a particular question isn't required to answer, the response
may remain as null
. Here is the current Question
model.
export interface Question {
prompt: string
category: QuestionCategory
options: string[]
isRequired: boolean
response: AnyQuestionResponse
}
export type QuestionCategory = 'TEXT' | 'PARAGRAPH' | 'DATETIME' | 'SINGLE_CHOICE' | 'MULTIPLE_CHOICE'
export type AnyQuestionResponse = string | Date | number | number[] | null
And here is how the category corresponds with the response type
- TEXT -> string
- PARAGRAPH -> string
- DATETIME -> Date
- SINGLE_CHOICE -> number
- MULTIPLE_CHOICE -> number[]
However, this isn't the whole story. Like I said above, a question's isRequired
field affects whether a response can be null
. So there are two independent fields driving the type of another. And taking it even further, if the category is SINGLE_CHOICE
or MULTIPLE_CHOICE
, the allowable response value(s) should not exceed the option's field length (since it is just the index value of whatever option in the array a user selects).
Ideally, I'd like to have something like a BaseQuestion type of which specific question types extend. I'm not too experienced with Typescript so in pseudocode I imagine it would be something like this.
export type QuestionCategory = 'TEXT' | 'PARAGRAPH' | ... // Should have an associated child `ResponseType` type
export type IsRequiredType = boolean // Should have an associated child `NullIfFalse` type
export interface BaseQuestion<T=QuestionCategory, R=IsRequiredType> {
prompt: string
category: QuestionCategory
options: string[]
isRequired: R
response: T.ResponseType | R.NullIfFalse // T dictates what its non-null type is while R determines whether it can be null
}
export interface TextQuestion<R=IsRequiredType> extends BaseQuestion<'TEXT', R> { }
// ... ParagraphQuestion, DateQuestion, SingleQuestion ...
export interface MultiChoiceQuestion<R=IsRequiredType> extends BaseQuestion<'MULTIPLE_CHOICE', R> { }
// .................. Example models ..........................
const textQuestion: TextQuestion = {
prompt: "Example",
category: "TEXT", // Response must be string
options: [],
isRequired: false, // Null response allowed
response: 'asdf' // <--- OK
}
const requiredTextQuestion: TextQuestion = {
prompt: "Example",
category: "TEXT", // Response must be string
options: [],
isRequired: true, // Response must not be null
response: null // <--- ERROR WOULD BE THROWN HERE
}
const singleChoiceQuestion: SingleChoiceQuestion = {
prompt: "Example",
category: "SINGLE_CHOICE", // Response must be an integer number
options: ["A", "B", "C"], // Response must be between 0 and 2
isRequired: false, // Null response allowed
response: 3 // <--- ERROR THROWN HERE
}
If anybody has ideas/thoughts on how to implement I'd be more than grateful to hear about. I have a feeling this can be more trouble setting up than it's worth so I don't think I'll implement this anyways, but I do see this more of as an exercise to learn data structuring strategies, if anything. Plus I think it's fun to find optimized solutions. Though I can see this directly helping out with my type validating algorithms if/when more QuestionCategory
question types are introduced in the future. They currently work and are very thorough, yet very loose and unorganized. This is what I imagine a cleaner solution would look like for validation:
type AnyQuestion = TextQuestion | ... | MultiChoiceQuestion
function validate(question: AnyQuestion): boolean {
switch (question.category) {
case 'TEXT':
return validateTextQuestion(question as TextQuestion)
case 'PARAGRAPH':
return validateParagraphQuestion(question as ParagraphQuestion)
case 'DATETIME':
return validateDateQuestion(question as DateQuestion)
// ...
}
// Validation requirements are more constrained in accordance with what the specific question type allows ...
function validateTextQuestion(question: TextQuestion): boolean { ... }
function validateParagraphQuestion(question: ParagraphQuestion): boolean { ... }
function validateDateQuestion(question: DateQuestion): boolean { ... }
// ....
}
Compared to a current snippet of my working version:
export class SubmissionResponsesValidator {
// Checks that all submission's responses field satisfies the following requirements:
// - Is an array of valid Question objects
// - A response is provided if the question is required to answer
// - The response type matches with the question's corresponding category
// - NOTE: An empty array is possible (if all questions unrequired and all responses null)
public static validate(questions: Question[]): string | true {
// 1. Check is array
if (!tc.isArray(questions)) {
return UIError.somethingWentWrong
}
// 2. Check each item is Question type
let allAreQuestions = questions.every(question => this.isQuestion(question))
if (!allAreQuestions) {
return UIError.somethingWentWrong
}
// 3. Check that each question's response is Provided if required to respond
let allResponsesProvided = questions.every(question => this.responseIsProvidedIfRequired(question))
if (!allResponsesProvided) {
return UIError.responsesInvalid
}
// 4. Check that each question's response type matches the category
let allResponseMatchCategory = questions.every(question => this.responseMatchesCategory(question))
if (!allResponseMatchCategory) {
return UIError.somethingWentWrong
}
return true
}
// 2. Checks each item is Question type
private static isQuestion(question: Question): boolean {
let list = ['TEXT', 'PARAGRAPH', 'DATETIME', 'SINGLE_CHOICE', 'MULTIPLE_CHOICE']
if (!tc.isString(question.prompt) ||
!list.includes(question.category) ||
!tc.isArrayOfString(question.options) ||
!tc.isBool(question.isRequired) ||
!this.isResponseType(question.response)) {
return false
}
return true
}
// 2. ...
// Checks that a value is of Question response type
// Must be string / number / date / number[] / null
private static isResponseType(response: AnyQuestionResponse): boolean {
if (tc.isString(response) ||
tc.isNumber(response) ||
tc.isDate(response) ||
tc.isArrayOfNumbers(response) ||
tc.isNull(response)) {
return true
}
return false
}
// 3a. Check that question response is provided if required to respond to
// Note this does not check for type since 2 already does that
private static responseIsProvidedIfRequired(question: Question): boolean {
if (question.isRequired) {
return tc.isDefined(question.response)
}
return true
}
// 3b. Check that question's response matches with its category
private static responseMatchesCategory(question: Question): boolean {
// No need to check for required
if (!question.response) {
return true
}
switch (question.category) {
case 'SINGLE_CHOICE':
return tc.isNumber(question.response)
case 'MULTIPLE_CHOICE':
return tc.isArrayOfNumbers(question.response)
case 'TEXT':
return tc.isString(question.response)
case 'PARAGRAPH':
return tc.isString(question.response)
case 'DATETIME':
return tc.isDate(question.response)
default:
return false
}
}
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论