I created a custom select
component to use in my forms and want it to be flexible enough to handle multiple types while still being type safe. I have a few types in my project which look like this.
export type DairyType = 'cow' | 'goat';
export type DairyForm = 'milk' | 'cream' | 'yogurt' | 'butter' | 'cheese';
export type MilkType = 'low fat' | 'skim' | '2%' | 'whole' | 'butter milk';
export type MilkState = 'powdered' | 'liquid' | 'condensed';
export type CreamType = 'half & half' | 'heavy cream' | 'sour cream';
export type CreamState = 'liquid' | 'whipped' | 'frozen';
export type YogurtType = 'plain' | 'flavored';
export type ButterType = 'unsalted' | 'salted';
export type CheeseTextureType = 'smooth/creamy' | 'smooth/chunky' | 'crumbly' | 'solid';
export type CheeseHydrationType = 'very wet' | 'wet' | 'damp' | 'dry' | 'very dry';
export type CheeseState = 'spreadable' | 'shredded' | 'crumbled' | 'block' | 'ball' | 'sliced';
That's just for the different types of dairy products alone. I want to be able to apply these types to my select
component in each instance so it can be both flexible and type safe. So far been able to think to try is this,
export class SelectInputComponent<ValueType> {
@Input() OptionType! : ValueType;
@Input() OptionList! : ValueType[];
@Input() Control! : FormControl< ValueType | null>;
@Input() Label! : string;
ToggleOptions = false;
updateValue(value : ValueType) : void {
this.Control.setValue(value);
this.ToggleOptions = false;
}
toggleOptions(){ this.ToggleOptions = !this.ToggleOptions; }
}
Then in the parent component's template I can do this
<lib-select-input
ngDefaultControl
[Control]="Control.controls.type"
[OptionType]="'CreamType'"
[OptionList]="[ 'half & half', 'heavy cream', 'sour cream']"
[Label]="'type'"
/>
The theory is if I can pass the type into the OptionType
input, it can be inferred onto the ValueType
generic defined on the component class which can then be inferred onto the OptionList
and Control
properties and the argument type of the updateValue()
method.
It eventually hit me that whatever type I could pass in wouldn't be magically imported inside the component and upon testing this component it works, however it's as if everything is of type any
because I can literally pass anything into the OptionType
and OptionList
inputs and it will just accept whatever.
I got the idea because I was thinking about it in terms of if I had something like this
export interface SomeInterface {
propA : string;
propB : number;
propC : SomeOtherInterface;
propD : boolean;
}
If I were to pass an object of type SomeInterface
into the OptionType
input the shape of that object will get inferred onto ValueType
because there's an actual chunk of data that can be read and understood by the compiler to discern how ValueType
should be shaped but when it comes to something like 'milk' | 'cream' | 'yogurt' | 'butter' | 'cheese'
.... I'm not sure about how I could infer that in this manner because doing it as shown in my example of the parent component's template, type string
just gets inferred which is what allows me to enter anything and it will allow it. Does anybody know how I could get this to work?
I created a custom select
component to use in my forms and want it to be flexible enough to handle multiple types while still being type safe. I have a few types in my project which look like this.
export type DairyType = 'cow' | 'goat';
export type DairyForm = 'milk' | 'cream' | 'yogurt' | 'butter' | 'cheese';
export type MilkType = 'low fat' | 'skim' | '2%' | 'whole' | 'butter milk';
export type MilkState = 'powdered' | 'liquid' | 'condensed';
export type CreamType = 'half & half' | 'heavy cream' | 'sour cream';
export type CreamState = 'liquid' | 'whipped' | 'frozen';
export type YogurtType = 'plain' | 'flavored';
export type ButterType = 'unsalted' | 'salted';
export type CheeseTextureType = 'smooth/creamy' | 'smooth/chunky' | 'crumbly' | 'solid';
export type CheeseHydrationType = 'very wet' | 'wet' | 'damp' | 'dry' | 'very dry';
export type CheeseState = 'spreadable' | 'shredded' | 'crumbled' | 'block' | 'ball' | 'sliced';
That's just for the different types of dairy products alone. I want to be able to apply these types to my select
component in each instance so it can be both flexible and type safe. So far been able to think to try is this,
export class SelectInputComponent<ValueType> {
@Input() OptionType! : ValueType;
@Input() OptionList! : ValueType[];
@Input() Control! : FormControl< ValueType | null>;
@Input() Label! : string;
ToggleOptions = false;
updateValue(value : ValueType) : void {
this.Control.setValue(value);
this.ToggleOptions = false;
}
toggleOptions(){ this.ToggleOptions = !this.ToggleOptions; }
}
Then in the parent component's template I can do this
<lib-select-input
ngDefaultControl
[Control]="Control.controls.type"
[OptionType]="'CreamType'"
[OptionList]="[ 'half & half', 'heavy cream', 'sour cream']"
[Label]="'type'"
/>
The theory is if I can pass the type into the OptionType
input, it can be inferred onto the ValueType
generic defined on the component class which can then be inferred onto the OptionList
and Control
properties and the argument type of the updateValue()
method.
It eventually hit me that whatever type I could pass in wouldn't be magically imported inside the component and upon testing this component it works, however it's as if everything is of type any
because I can literally pass anything into the OptionType
and OptionList
inputs and it will just accept whatever.
I got the idea because I was thinking about it in terms of if I had something like this
export interface SomeInterface {
propA : string;
propB : number;
propC : SomeOtherInterface;
propD : boolean;
}
If I were to pass an object of type SomeInterface
into the OptionType
input the shape of that object will get inferred onto ValueType
because there's an actual chunk of data that can be read and understood by the compiler to discern how ValueType
should be shaped but when it comes to something like 'milk' | 'cream' | 'yogurt' | 'butter' | 'cheese'
.... I'm not sure about how I could infer that in this manner because doing it as shown in my example of the parent component's template, type string
just gets inferred which is what allows me to enter anything and it will allow it. Does anybody know how I could get this to work?
1 Answer
Reset to default 1Ok the answer JUST came to me which I can't believe I didn't think about it sooner. All I have to do is in my parent component's class define a property as follows
export class ParentComponent{
// stuff
SelectType : CreamType = 'half & half';
// more stuff
}
Then in the template I can do this
<lib-select-input
ngDefaultControl
[Control]="Control.controls.type"
[OptionType]="SelectType"
[OptionList]="[ 'half & half', 'heavy cream']"
[Label]="'type'"
/>
Once I pass SelectType
into the OptionType
input, the items in OptionList
will throw errors / warnings if they're not of the same type as the SelectType
property.
UPDATE
Ok I got excited too fast. Even though this works, it seems the compiler still looks at 'thing01' | 'thing02' | 'thing03'
and rationalizes it as "a variety of strings" which still winds up allowing me to pass any array of strings into the OptionList
input.