0

The following code is just an example to reproduce an error that I don't understand.
In setFilter I have narrowed the type of T to Product, so I would expect to be able to set filter successfully, because T and Product have the same subtypes.

How is it true that: 'T' could be instantiated with a different subtype of constraint 'Product'.?

EDIT: I realised that you can pass a subtype into setFilter that would make these signatures incompatible... so how could I define filter to allow this pattern?

type Product = { type: string };

let filter: (product: Product) => boolean = () => true;

function setFilter<T extends Product>(productType: string) {
  filter = (product: T) => product.type === productType;
  /* Type '(product: T) => boolean' is not assignable to type '(product: Product) => boolean'.
    Types of parameters 'product' and 'product' are incompatible.
    Type 'Product' is not assignable to type 'T'.
    'Product' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'Product'.
  */
}

const res = [{ type: 'subscription' }, { type: 'hardware' }, { type: 'hardware' }];
setFilter('hardware');

console.log(res.filter(filter));
7
  • Could You provide working code example ? Commented Feb 10, 2020 at 8:48
  • @SkorpEN this example seems complete Commented Feb 10, 2020 at 8:53
  • Do you really need generics here? I can explain why assignment is not allowed, but what's the point to define setFilter as generic? Commented Feb 10, 2020 at 10:19
  • @AlekseyL. this is a minimal example to reproduce the problem, the original code is complex. Commented Feb 10, 2020 at 10:34
  • If you understand why the assignment is not allowed, I'm not sure what is the question 🙂 Commented Feb 10, 2020 at 10:40

2 Answers 2

1

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

Sign up to request clarification or add additional context in comments.

Comments

0

This works for me, just remove (:T):

type Product = { type: string };
let filter: (product: Product) => boolean = () => true;
function setFilter<T extends Product>(productType: string) {

filter = (product) => product.type === productType;
}

const res = [{ type: 'subscription' }, { type: 'hardware' }, { type: 
'hardware' }];
setFilter('hardware');

console.log(res.filter(filter));

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.