I have an object that is going to have field products
, this field will be filled with the response returned by different endpoints which each return a type of product. something like:
const products = {
// each of this kind of products comes from endpoint response
someProduct: {},
someOtherProduct: {}
anotherProduct: {}
}
All product types share some common properties such as name, description, price. but they also have specific properties that apply only to one type of product. in some cases, I need to traverse all products to render a list of those common properties, but other times I need to render specific data for each type of product.
How do I type this product
object, if it contains different product types, each of which has different properties and comes from an external data source(server)?
EDIT 1: Product Type
Each product has an attribute type
, to identify each product type
EDIT 2: Selected product type
When I render all products in a list, the user can click on any of them and I store the selected product in a variable... so I can use that selected product to print specific product details... how to type this selected product, if it can be of any type of product?
CodePudding user response:
You can do something like:
In this case, we are defining a BaseProduct for all the common attributes. We extend this product interface to define each of the other interfaces.
Then, we define a type Product
which can be any kind of the Product
interfaces defined previously.
And finally, we can define an Array of Product
(if your server answer with an array) or an object which can have any string as key
and Product
as values.
interface BaseProduct {
name: string;
description: string;
price: number;
type: string;
}
interface SomeProduct extends BaseProduct {
genericAttrOne: string;
genericAttrTwo: string;
}
interface OtherProduct extends BaseProduct {
anAttributeOne: string;
anAttributeTwo: string;
}
interface AnotherProduct extends BaseProduct {
anotherAttributeOne: string;
anotherAttributeTwo: string;
}
type Product = SomeProduct | OtherProduct | AnotherProduct;
const productsAsArray: Product[] = [];
const productsAsObject: { [key: string]: Product }: {};
CodePudding user response:
Here's an example for strongly typing the response that you described:
// Start with the base product, defining the common properties
type Product<
T extends string = string,
P = Record<never, never>,
> = P & {
type: T;
name: string;
description: string;
price: number;
};
// Then define each product's specific type and properties
type SomeProduct = Product<'some', {
someValue1: string;
someValue2: number;
}>;
// For every product
type AnotherProduct = Product<'another', {
anotherValue1: string;
anotherValue2: number;
}>;
// Then assign them all to properties in an object of known products.
// Make sure to use the same property key as the value of each product's
// `type` field (more on that below)
type Products = {
some: SomeProduct;
another: AnotherProduct;
// etc...
};
// Create a type for known products any generic product with common properties
type AllProducts = Products & { [key: string]: Product };
declare const products: AllProducts;
products.some.name // ok
products.another.name // ok
products.some.type // "some"
products.some.someValue1 // ok
products.some.anotherValue1 // property doesn't exist
products.another.type // "another"
products.another.someValue1 // property doesn't exist
products.another.anotherValue1 // ok
// All other products will only have the commoon properties
products.mysteryProduct.name; // ok
// Narrowing a product type:
type AnyProduct = Products[keyof Products] | Product;
// This only works if the type name is equal to the property key for
// each product (see note before the Products type above)
function isProduct <T extends string>(
product: AnyProduct,
typeNameAndPropertyKey: T,
): product is AllProducts[T] {
return product.type === typeNameAndPropertyKey;
}
function doSomethingWithAProduct <T extends string>(products: AllProducts, typeName: T): void {
const selectedProduct: AnyProduct = products[typeName];
if (isProduct(selectedProduct, 'some')) {
selectedProduct.someValue1 // ok
selectedProduct.anotherValue1 // property doesn't exist
}
if (isProduct(selectedProduct, 'another')) {
selectedProduct.someValue1 // property doesn't exist
selectedProduct.anotherValue1 // ok
}
}
CodePudding user response:
Judging by your subsequent comments to the two (very good) answers, in which you ask about typing an undetermined product type at runtime (i.e. when a user selects a product) you need to use type guards:
// here’s the type guard for one type, you’ll need one for all six; we’re checking against your `type` property but it could be anything
const isMyProductType = (obj: any): obj is MyProductType => obj.type === “myProductType”;
const onClick = (selectedProduct) => {
if (isMyProductType(selectedProduct)) {
// TS now will treat selectedProduct as an object of MyProductType
// do stuff
} else if (/* another type guard etc … */) {
// etc
}
}