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:
assertFormFilled
function makes sense?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.
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>
.