I've known of and studied generics on and off for years and could never really get a grasp on them well enough to be able to use that level of TypeScript in my own projects. I imagine some degree of that is me not thinking of the types of ideas I'd need to think of for an anic light bulb to go off and another degree of it is me just not understanding it well enough to identify when I could've benefitted from it tremendously.
I'm currently working on an app where I know there has to be a way it can make my code cleaner and more reusable but I'm not having anything click in terms of how I might be able to use them.
The app is a micro frontend that will serve as an "ingredient manager" for the overall app I'm building. It will allow the user to create profiles for their ingredients and categorize them however they see fit. The interface for an Ingredient
item looks like this
interface IngredientNote{
title : string;
description : string;
}
interface Nutrient{
name : string;
amount : number;
}
interface Nutrition{
servingSize : number;
nutrients : Nutrient[];
ingredients : string[];
}
interface IngredientCategory{
name : string;
subCategory? : IngredientCategory;
}
interface Ingredient{
name : string;
brand : string;
id : string;
photo? : string;
nutrition? : Nutrition;
notes? : IngredientNote[];
data? : WhereItGetsInteresting; // <---
locations : IngredientCategory[];
}
Seeing that there's so many different types of ingredients with their own important things to consider about how you should or shouldn't use them in a recipe as well as all the health related needs that might lead one to want to monitor their sodium, calcium, potassium, etc. intake I came up with different "ingredient profiles" as I'm calling them to use as types for the data
field in the Ingredient
object which goes as follows
// Omitted for brevity, will go into details shortly
interface FlourProfile{
//....
}
interface SaltProfile{
//....
}
interface SugarProfile{
//....
}
interface GrainProfile{
//....
}
interface NutProfile{
//....
}
interface SeedProfile{
//....
}
interface DairyProfile{
//....
}
interface ProduceProfile{
//....
}
interface SweetenerProfile{
//....
}
interface OilProfile{
//....
}
interface HerbProfile{
//....
}
interface ExtractProfile{
//....
}
This brings me to my first curiosity about how generics may be able to apply because I could very easily do:
interface Ingredient{
name : string;
brand : string;
id : string;
photo? : string;
nutrition? : Nutrition;
notes? : IngredientNote;
data? : FlourProfile | SaltProfile | SugarProfile | GrainProfile | EtcProfile;
locations : IngredientCategory[];
}
Doing that would work perfectly fine, however from what I've learned so far part of why generics even exist is to simplify these types of situations with a good balance of flexibility and safety so I'm sure there's a better way of defining that. As far as all the other things I'm scratching my head about, lets look at the make up of the profiles first.
Flour Profile
//This interface will be used in other profiles as well.
interface NutrientAmount{
servingSize : number;
amount : number;
}
type FlourType = 'wheat' | 'whole wheat' | 'rice' | 'rye'; //there's more but I'm still compiling the list.
type FlourClassification = 'cake' | 'pastry' | 'all purpose' | 'bread' | '0' | '00' | '000' | '0000';
FlourProfile{
type : FlourType | string;
classification : FlourClassification | string;
maxHydration? : number;
protein? : NutrientAmount;
bleached : boolean;
}
Salt Profile
type SaltType = 'table salt' | 'sea salt' | 'kosher salt';
type SaltConsistency = 'extra fine' | 'fine' | 'coarse' | 'fine crystal' | 'crystal';
interface SaltProfile{
type : SaltType | string;
consistency : SaltConsistency | string;
sodium? : NutrientAmount;
iodized : boolean;
}
Sugar Profile
type SugarType = 'cane' | 'coconut' | 'palm' | 'other';
type CaneType = 'white' | 'light brown' | 'dark brown' | 'raw';
type SugarConsistencyType = 'powdered' | 'small granules' | 'large granules' | 'crystals' | 'rock' | 'block' | 'cake' | 'paste' | 'liquid';
interface SugarInfo{
consistency : SugarConsistencyType;
}
interface CaneInfo extends SugarInfo{
type : CaneType;
}
interface SugarProfile{
type : SugarType;
info : SugarInfo | CaneInfo;
source : string;
// In this instance if the user selects coconut, palm or other in the 'type'
// property, I want the 'info' property to be of type SugarInfo and CaneInfo
// if the user selects cane for the type.
}
Grain Profile
interface GrainProfile{
protein : NutrientAmount;
maxObsorption? : number;
}
Nut Profile
interface NutProfile{
species : string;
roasted : boolean;
salted : boolean;
}
Seed Profile
interface SeedProfile{
species : string;
// still figuring out other things to go here
}
Dairy Profile
type DairyType = 'cow' | 'goat';
type DairyForm = 'milk' | 'cream' | 'yogurt' | 'butter' | 'cheese';
type MilkType = 'low fat' | 'skim' | '2%' | 'whole' | 'butter milk';
type MilkState = 'powdered' | 'liquid' | 'condensed';
type CreamType = 'half & half' | 'heavy cream' | 'sour cream';
type CreamState = 'liquid' | 'whipped' | 'frozen';
type YogurtType = 'plain' | 'flavored';
type ButterType = 'unsalted' | 'salted';
type CheeseTextureType = 'smooth/creamy' | 'smooth/chunky' | 'crumbly' | 'solid';
type CheeseHydrationType = 'very wet' | 'wet' | 'damp' | 'dry' | 'very dry';
type CheeseState = 'spreadable' | 'shredded' | 'crumbled' | 'block' | 'ball';
interface MilkSpecs{
type : MilkType;
state : MilkState;
}
interface CreamSpecs{
type : CreamType;
state : CreamState;
}
interface YogurtSpecs{
type : YogurtType;
flavor? : string;
}
interface ButterSpecs{
type : ButterType;
}
interface CheeseSpecs{
kind : string;
texture : CheeseTextureType;
hydration : CheeseHydrationType;
state : CheeseState;
}
interface DairyProfile{
type : DairyType;
form : DairyForm;
specs : MilkSpecs | CreamSpecs | YogurtSpecs | ButterSpecs | CheeseSpecs;
pasturized : boolean;
homogenized : boolean
}
Produce Profile
type ProduceType = 'fruit' | 'vegetable';
type ProduceState = 'fresh' | 'frozen' | 'canned' | 'preserved' | 'pickled' | 'dried' | 'powdered';
interface ProduceProfile{
type : ProduceType;
state : ProduceState;
}
Oil Profile
type OilState = 'liquid' | 'solid';
interface OilProfile{
type : string;
state : OilState;
}
HerbProfile
type PlantPart = 'root' | 'stem' | 'leaf' | 'flower' | 'bark' | 'berry' | 'fruit' | 'seed';
interface HerbProfile{
species : string;
part : PlantPart | string;
}
Extract Profile
type ExtractType = 'water' | 'alcohol' | 'oil';
type ExtractState = 'liquid' | 'paste';
interface ExtractProfile{
type : ExtractType;
state : ExtractState;
}
Once I define everything to this point I start the process of breaking everything into smaller re-usable parts and orchestrate them back together into the bigger pieces. I'm about to start doing that and thought I'd stop to ask how I could incorporate generics into this to make it even more reusable. For example, look at all the type
and state
properties I use in my interfaces that provide the same style of results just with different sets of details. So consider I want to do something like..... idk lets say
interface SpecPrimative{
type : SomeCleverGenericsMagic;
state : MoreGenericsMagic;
}
interface SomethingSomewhereInDairy extends SpecPrimative<AwesomeGenericsMagic>{
// stuff I want to add in
someProp : MoreAwesomeGenericsMagic | SomeOtherAwesomeGenericsMagic<MaybeSomethingHere>;
// more stuff
}
interface SomethingSomewhereInSugar extends SpecPrimative<AnotherAwesomeGeneric>{
// stuff
someOtherProp : MaybeAnArgumentFromTheAwesomeGeneric;
// more stuff
}
How could that be applied to this scenario? I know this technically isn't a question about a specific problem but the more I've studied into this and watched videos everything is either of:
- A: So basic that I can't really grasp the magic of it and how to wield it.
- B: I personally need to know a lot more about their overall project and how it works to understand the means of flexibility and customization they're achieving by doing one magic generics thing vs another totally different magical generics thing they did to something else.
I've been using Angular since the release of v2 so I know I've been using generics this entire time but it's mainly been from following the platform's design patterns. Like RXJS BehaviorSubject
for example, When we use them we do new BehaviorSubject<OurOwnInterface>({...});
. So I've always been on the "create generics to pass into things" side of things as opposed to the "create things that handle generic types" side of things and just want to get a grasp once and for all how I can realistically apply that part of TypeScript to my projects.
I've known of and studied generics on and off for years and could never really get a grasp on them well enough to be able to use that level of TypeScript in my own projects. I imagine some degree of that is me not thinking of the types of ideas I'd need to think of for an anic light bulb to go off and another degree of it is me just not understanding it well enough to identify when I could've benefitted from it tremendously.
I'm currently working on an app where I know there has to be a way it can make my code cleaner and more reusable but I'm not having anything click in terms of how I might be able to use them.
The app is a micro frontend that will serve as an "ingredient manager" for the overall app I'm building. It will allow the user to create profiles for their ingredients and categorize them however they see fit. The interface for an Ingredient
item looks like this
interface IngredientNote{
title : string;
description : string;
}
interface Nutrient{
name : string;
amount : number;
}
interface Nutrition{
servingSize : number;
nutrients : Nutrient[];
ingredients : string[];
}
interface IngredientCategory{
name : string;
subCategory? : IngredientCategory;
}
interface Ingredient{
name : string;
brand : string;
id : string;
photo? : string;
nutrition? : Nutrition;
notes? : IngredientNote[];
data? : WhereItGetsInteresting; // <---
locations : IngredientCategory[];
}
Seeing that there's so many different types of ingredients with their own important things to consider about how you should or shouldn't use them in a recipe as well as all the health related needs that might lead one to want to monitor their sodium, calcium, potassium, etc. intake I came up with different "ingredient profiles" as I'm calling them to use as types for the data
field in the Ingredient
object which goes as follows
// Omitted for brevity, will go into details shortly
interface FlourProfile{
//....
}
interface SaltProfile{
//....
}
interface SugarProfile{
//....
}
interface GrainProfile{
//....
}
interface NutProfile{
//....
}
interface SeedProfile{
//....
}
interface DairyProfile{
//....
}
interface ProduceProfile{
//....
}
interface SweetenerProfile{
//....
}
interface OilProfile{
//....
}
interface HerbProfile{
//....
}
interface ExtractProfile{
//....
}
This brings me to my first curiosity about how generics may be able to apply because I could very easily do:
interface Ingredient{
name : string;
brand : string;
id : string;
photo? : string;
nutrition? : Nutrition;
notes? : IngredientNote;
data? : FlourProfile | SaltProfile | SugarProfile | GrainProfile | EtcProfile;
locations : IngredientCategory[];
}
Doing that would work perfectly fine, however from what I've learned so far part of why generics even exist is to simplify these types of situations with a good balance of flexibility and safety so I'm sure there's a better way of defining that. As far as all the other things I'm scratching my head about, lets look at the make up of the profiles first.
Flour Profile
//This interface will be used in other profiles as well.
interface NutrientAmount{
servingSize : number;
amount : number;
}
type FlourType = 'wheat' | 'whole wheat' | 'rice' | 'rye'; //there's more but I'm still compiling the list.
type FlourClassification = 'cake' | 'pastry' | 'all purpose' | 'bread' | '0' | '00' | '000' | '0000';
FlourProfile{
type : FlourType | string;
classification : FlourClassification | string;
maxHydration? : number;
protein? : NutrientAmount;
bleached : boolean;
}
Salt Profile
type SaltType = 'table salt' | 'sea salt' | 'kosher salt';
type SaltConsistency = 'extra fine' | 'fine' | 'coarse' | 'fine crystal' | 'crystal';
interface SaltProfile{
type : SaltType | string;
consistency : SaltConsistency | string;
sodium? : NutrientAmount;
iodized : boolean;
}
Sugar Profile
type SugarType = 'cane' | 'coconut' | 'palm' | 'other';
type CaneType = 'white' | 'light brown' | 'dark brown' | 'raw';
type SugarConsistencyType = 'powdered' | 'small granules' | 'large granules' | 'crystals' | 'rock' | 'block' | 'cake' | 'paste' | 'liquid';
interface SugarInfo{
consistency : SugarConsistencyType;
}
interface CaneInfo extends SugarInfo{
type : CaneType;
}
interface SugarProfile{
type : SugarType;
info : SugarInfo | CaneInfo;
source : string;
// In this instance if the user selects coconut, palm or other in the 'type'
// property, I want the 'info' property to be of type SugarInfo and CaneInfo
// if the user selects cane for the type.
}
Grain Profile
interface GrainProfile{
protein : NutrientAmount;
maxObsorption? : number;
}
Nut Profile
interface NutProfile{
species : string;
roasted : boolean;
salted : boolean;
}
Seed Profile
interface SeedProfile{
species : string;
// still figuring out other things to go here
}
Dairy Profile
type DairyType = 'cow' | 'goat';
type DairyForm = 'milk' | 'cream' | 'yogurt' | 'butter' | 'cheese';
type MilkType = 'low fat' | 'skim' | '2%' | 'whole' | 'butter milk';
type MilkState = 'powdered' | 'liquid' | 'condensed';
type CreamType = 'half & half' | 'heavy cream' | 'sour cream';
type CreamState = 'liquid' | 'whipped' | 'frozen';
type YogurtType = 'plain' | 'flavored';
type ButterType = 'unsalted' | 'salted';
type CheeseTextureType = 'smooth/creamy' | 'smooth/chunky' | 'crumbly' | 'solid';
type CheeseHydrationType = 'very wet' | 'wet' | 'damp' | 'dry' | 'very dry';
type CheeseState = 'spreadable' | 'shredded' | 'crumbled' | 'block' | 'ball';
interface MilkSpecs{
type : MilkType;
state : MilkState;
}
interface CreamSpecs{
type : CreamType;
state : CreamState;
}
interface YogurtSpecs{
type : YogurtType;
flavor? : string;
}
interface ButterSpecs{
type : ButterType;
}
interface CheeseSpecs{
kind : string;
texture : CheeseTextureType;
hydration : CheeseHydrationType;
state : CheeseState;
}
interface DairyProfile{
type : DairyType;
form : DairyForm;
specs : MilkSpecs | CreamSpecs | YogurtSpecs | ButterSpecs | CheeseSpecs;
pasturized : boolean;
homogenized : boolean
}
Produce Profile
type ProduceType = 'fruit' | 'vegetable';
type ProduceState = 'fresh' | 'frozen' | 'canned' | 'preserved' | 'pickled' | 'dried' | 'powdered';
interface ProduceProfile{
type : ProduceType;
state : ProduceState;
}
Oil Profile
type OilState = 'liquid' | 'solid';
interface OilProfile{
type : string;
state : OilState;
}
HerbProfile
type PlantPart = 'root' | 'stem' | 'leaf' | 'flower' | 'bark' | 'berry' | 'fruit' | 'seed';
interface HerbProfile{
species : string;
part : PlantPart | string;
}
Extract Profile
type ExtractType = 'water' | 'alcohol' | 'oil';
type ExtractState = 'liquid' | 'paste';
interface ExtractProfile{
type : ExtractType;
state : ExtractState;
}
Once I define everything to this point I start the process of breaking everything into smaller re-usable parts and orchestrate them back together into the bigger pieces. I'm about to start doing that and thought I'd stop to ask how I could incorporate generics into this to make it even more reusable. For example, look at all the type
and state
properties I use in my interfaces that provide the same style of results just with different sets of details. So consider I want to do something like..... idk lets say
interface SpecPrimative{
type : SomeCleverGenericsMagic;
state : MoreGenericsMagic;
}
interface SomethingSomewhereInDairy extends SpecPrimative<AwesomeGenericsMagic>{
// stuff I want to add in
someProp : MoreAwesomeGenericsMagic | SomeOtherAwesomeGenericsMagic<MaybeSomethingHere>;
// more stuff
}
interface SomethingSomewhereInSugar extends SpecPrimative<AnotherAwesomeGeneric>{
// stuff
someOtherProp : MaybeAnArgumentFromTheAwesomeGeneric;
// more stuff
}
How could that be applied to this scenario? I know this technically isn't a question about a specific problem but the more I've studied into this and watched videos everything is either of:
- A: So basic that I can't really grasp the magic of it and how to wield it.
- B: I personally need to know a lot more about their overall project and how it works to understand the means of flexibility and customization they're achieving by doing one magic generics thing vs another totally different magical generics thing they did to something else.
I've been using Angular since the release of v2 so I know I've been using generics this entire time but it's mainly been from following the platform's design patterns. Like RXJS BehaviorSubject
for example, When we use them we do new BehaviorSubject<OurOwnInterface>({...});
. So I've always been on the "create generics to pass into things" side of things as opposed to the "create things that handle generic types" side of things and just want to get a grasp once and for all how I can realistically apply that part of TypeScript to my projects.
1 Answer
Reset to default 0You can make your unions as generic params:
Playground
interface Ingredient<T extends DairyProfile<any>>{
name : string;
brand : string;
id : string;
photo? : string;
nutrition? : any;
notes? : any[];
data? : T
locations : any[];
}
interface DairyProfile<T extends MilkSpecs | CreamSpecs | YogurtSpecs | ButterSpecs | CheeseSpecs>{
type : DairyType;
form : DairyForm;
specs : T
pasturized : boolean;
homogenized : boolean
}
declare const ingredient01: Ingredient<string>; // Type 'string' does not satisfy the constraint 'DairyProfile<any>'.(2344)
declare const ingredient02: Ingredient<DairyProfile<string>>; // Type 'string' does not satisfy the constraint 'MilkSpecs | CreamSpecs | YogurtSpecs | ButterSpecs | CheeseSpecs'
declare const ingredient: Ingredient<DairyProfile<CheeseSpecs>> // ok
ingredient.data?.specs.hydration // (property) CheeseSpecs.hydration: CheeseHydrationType | undefined
Profile
here that all other profiles extend to, I don't see how you could use generics here if you are not able to constrain the generic for example would it make sense to do:interface Ingredient<T> { data?: T }
and thenconst ingredient: Ingredient<string> = { data: 'a' }
? Also can you please try to reduce the question length by removing parts that are not strictly necessary? – apokryfos Commented Feb 16 at 9:04