Search code examples
typescriptdomain-driven-design

Transition from Partial to Required


I have a domain object Form and this form is being updated with data in random order. So that makes my domain relax the required fields on the form.

Whenever user has to do an action with I the form has to be filled.

My main question: How do you deal with transition from object having optional fields to same structure object but with all required.

Few subquestions:

  1. Does the assertFormFilled function makes sense?
  2. Is there a way to automate assertion of all properties.
  3. Am I missing some domain concept here?
  4. Does Partial<IForm> in domain layer makes better sense than Required<IForm> in doSomething?
interface IForm {
  question1?: string
  question2?: boolean
  question3?: number
}

class Form implements IForm {
  question1: string | undefined
  question2: boolean | undefined
  question3: number | undefined
}

class User {
  form?: IForm
  
  doSomething = (form: Required<IForm>): void => {
    //todo use filled form
  }

  assertFormFilled = (): Required<IForm> => {
    if (this.form && this.form.question1 && !!this.form.question2 && !!this.form.question3) {
        return {
          question1: this.form.question1, 
          question2: this.form.question2, 
          question3: this.form.question3, 
          }
    }
    else {
      throw new Error('Form is not filled')
    }
  }
}


const user = new User()

user.form = { question2: false}
const form = user.assertFormFilled()
user.doSomething(form)

For example.


Solution

  • TypeScript doesn't automatically use control flow analysis to narrow the type of an object with an optional property to one with a required property when you check for the presence of the property. This is a design limitation mentioned several places in GitHub issues; for example, see microsoft/TypeScript#33205. The issue is that such narrowing would require that the type checker synthesize new types each time a check is made (as opposed to just filtering a union) and that could negatively affect compiler performance.

    So, if you want this kind of behavior, you'll have to emulate it by creating a custom type guard function. For example, you could write this:

    function hasDefinedProps<
        T extends { [P in K]?: any },
        K extends PropertyKey>(
            obj: T, ...keys: K[]
        ): obj is T & { [P in K]-?: Exclude<T[P], undefined> } {
        return keys.every(k => obj[k] !== undefined)
    }
    

    which lets you call something like hasDefinedProps(obj, "foo", "bar") and, if it returns true, narrows the type of obj to one in which the foo and bar properties are required and cannot be undefined.

    With your example code this becomes:

    assertFormFilled = (): Required<IForm> => {
        if (this.form && hasDefinedProps(this.form, "question1", "question2", "question3")) {
            return this.form // okay
            /* IForm & { question1: string; question2: boolean; question3: number; } */
        } else {
            throw new Error("Form is not filled");
        }
    }
    

    which works, because this.form has been narrowed from IForm to a type equivalent to Required<IForm>.

    Playground link to code