This question builds on the question and accepted answer found in this link.
I want to write a form builder class, where an underlying type T is used to determine which functions can be called. As an example, say this is our model we want a form for:
interface MyFormModel {
name: string;
options: number[];
isPrimary: boolean;
address: {
street: string;
houseNumber: number;
someArrayThatsRelevantToAddresses: string[];
}
}
I want to be able to create a checkbox for the property 'isPrimary', and then I want the returned value of this not to include the method used for creating checkboxes, since there are no more boolean properties left to call "createCheckbox" for. I keep track of which properties have already been used up using a second type parameter K. After every function call with a key from T, K is updated so we track which keys have already been used up. Here's my implementation:
// T is the model for the form, K will accumulate props from T as we build the form
class FormBuilder<T, K extends Partial<T> = {}> {
buildCheckbox<NewKey extends BooleanKeysOf<T>>(): FormBuilder<T, AddProp<T, K, NewKey>> {
return this as any;
}
buildBlock<NewKey extends CompoundKeysOf<T>>(key: NewKey): FormBuilder<T, AddProp<T, K, NewKey>> {
return this as any;
}
buildInput<NewKey extends StringOrNumberKeysOf<T>>(): FormBuilder<T, AddProp<T, K, NewKey>> {
return this as any;
}
buildArray<NewKey extends ArrayKeysOf<T>>(): FormBuilder<T, AddProp<T, K, NewKey>> {
return this as any;
}
finishForm(): K extends T ? FormForModel<T> : MissingProps<T, K> { // tells us which props are missing!
// use childforms to build a complete form
return 'hello' as any;
}
// Get a FormBuilder that accumulates props to build one of the object properties of the parent form
getChildBuilder<NewKey extends CompoundKeysOf<T>>(): FormBuilder<T[NewKey]> {
return new FormBuilder();
}
}
This uses some helper types:
/**
* Utility types
*/
// Get from Type T all of the keys that have values of type SomeType
type KeysOfType<T, SomeType> = { [K in keyof T]: T[K] extends SomeType ? K : never }[keyof T]
// Get all keys that are boolean, or string | number, or arrays, or other objects
type BooleanKeysOf<T> = KeysOfType<T, boolean>;
type StringOrNumberKeysOf<T> = KeysOfType<T, string | number>;
type ArrayKeysOf<T> = KeysOfType<T, any[]>;
type CompoundKeysOf<T> = Exclude<KeysOfType<T, object>, ArrayKeysOf<T>>; // get all props that have object types, except arrays
// a type representing the form we're building
type FormForModel<T> = { [K in keyof T]: {} } };
// Add to K another prop that's a part of T
type AddProp<T, K extends Partial<T>, NewKey extends keyof T> = K & { [S in NewKey]: T[S] };
// Which props are missing from K to fill out T?
type MissingProps<T, K extends Partial<T>> = Omit<T, keyof K>;
Using this approach, it's easy enough to limit the type parameter to each function to be only keys that haven't been called before, until the call signature becomes NewKey extends never and no call can be made.
However I'd like to completely remove a function from the type signature when it has no legal values to be called with. I imagine something like this:
buildCheckbox<NewKey extends Exclude<BooleanKeysOf<T>, keyof K>>(): MaybeExcludeCheckbox<T, K, NewKey> {
return this as any;
}
type MaybeExcludeCheckbox<T, K extends Partial<T>, NewKey extends keyof T> = Exclude<keyof T, NewKey> extends never? Omit<FormBuilder<T, AddProp<T, K, NewKey>>, 'buildCheckbox'> : FormBuilder<T, AddProp<T, K, NewKey>>
What this does is check if there are any remaining boolean properties, if there are it returns the FormBuilder type as before, if there aren't it omits the 'buildCheckbox' property. However there may be cases where I want to turn multiple functions on or off. Is there a more concise way to add/remove multiple properties each time a function is called?