An issue arises when you specify T extends Product
in TypeScript because there's no guarantee that T
will be accurate. For instance, consider the following example:
type Product = { type: string };
interface SubProductA extends Product {
name: string
}
interface SubProductB extends Product {
rating: number
}
declare let filter: (product: Product) => boolean
const product1: Product = {
type: "foo"
}
const product2: SubProductA = {
type: "bar",
name: "Fred"
}
const product3: SubProductB = {
type: "baz",
rating: 5
}
filter(product1); //valid! it's a Product. But not SubProductA
filter(product2); //valid! it's a Product *and* SubProductA
filter(product2); //valid! it's a Product. But not SubProductA
The filter
function only accepts a parameter of type Product
, so passing the parent type is completely acceptable - it will work without any issues. Similarly, passing a subtype is also permissible since it can be assigned to its parent. However, ensuring that filter
always receives a specific subtype is not guaranteed. To illustrate this further:
type Animal = { };
interface Cat extends Animal {
cute: number
}
interface Dog extends Animal {
friendly: number
}
function getCatsAndDogs(): Array<Cat | Dog> {
return []; //dummy implementation
}
const dogFilter: (x: Dog) => boolean = x => x.friendly > 9;
const catFilter: (x: Cat) => boolean = x => x.cute > 9;
let arr: Array<Animal> = getCatsAndDogs(); //valid (Cat | Dog) is assignable to Animal
arr.filter(dogFilter); //not valid - we cannot guarantee Animal would be Dog
arr.filter(catFilter); //not valid - we cannot guarantee Animal would be Cat
If your intention is to utilize only the common parent and not rely on anything defined in a subtype, then using the parent type directly without generics is preferable:
function setFilter(productType: string) {
filter = (product: Product) => product.type === productType;
}
This method works because every subtype of Product
is considered a product, but not every Product
is a specific subtype. This scenario is similar to how every Cat
is an Animal
, but not every Animal
is a Cat
.
In case you do require filtering based on a subtype, you can employ type assertion as a workaround, such as:
filter = (p: Product) => (g as SubTypeA).name === "Fred";
Or when utilizing a generic argument T
:
filter = (p: Product) => (p as T)/* include something T specific not applicable for Product*/;
This process, known as downcasting, involves converting from a superclass or parent type to a subclass or child type.
However, absolute type safety cannot be ensured during compile time, unless you possess additional information that the compiler lacks and are certain that filter
will specifically receive a subtype. In such cases, avoiding direct type assertions and employing type guards is strongly recommended:
type Product = { type: string };
interface SubProductA extends Product {
name: string
}
interface SubProductB extends Product {
rating: number
}
function isSubProductA(product: Product): product is SubProductA {
return "name" in product;
}
declare let filter: (product: Product) => boolean
filter = (product: Product) => {
if (isSubProductA(product)) {
//no need to do type assertion we're sure of the type
return product.name === "Fred";
}
return false;
}
See on TypeScript Playground