The problem is that when you define T extends Product then TypeScript can't guarantee that T would be correct. Here is a minimal example, let's say we have:
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
See on TypeScript Playground
The filter expects a function that only expects Product, so calling it with the parent type is fine - that's guaranteed to work. You can also call it with a subtype - that's also fine since it's assignable to its parent. However, you cannot guarantee that filter will be called with a specific subtype. Just to illustrate this as clearly as possible:
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
See on TypeScript Playground
So, if you intend to only use the common parent, so you don't depend on anything defined in a subtype, then you don't need a generic, just use the parent type:
function setFilter(productType: string) {
filter = (product: Product) => product.type === productType;
}
This works because every subtype of Product is a product but not every Product is a specific subtype. The same way as every Cat is an Animal but not every Animal is a Cat.
If you do need to use a subtype for filtering, you can get around it using a type assertion which will look something like this:
filter = (p: Product) => (g as SubTypeA).name === "Fred";
Or if using a generic argument T then:
filter = (p: Product) => (p as T)/* include something T specific not applicable for Product*/;
This is known as downcasting because you convert from the parent/super type to the child/sub type.
However, as the compiler already said, type safety cannot be guaranteed at compile time. Unless you have information the compiler doesn't and you're sure that filter will actually be called with a specific subtype, then you shouldn't just use a straight type assertion there. To guarantee type safety, you can use a type guard:
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
setFilteras generic?