如何抽象一个基本接口,其中某些字段确定另一个字段的类型/值,然后从中定义更严格的子接口?

发布于 2025-01-24 20:53:00 字数 7366 浏览 1 评论 0原文

我的应用程序允许用户为其他人填写问卷。在创建表单时,允许用户在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_choiceprotedn_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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文