4
votes

I recently upgraded to typescript 2.4 and I got several errors complaining that my types were no longer assignable.

Here is the scenario where I am encountering the error:

interface Parent {
    prop: any
}

interface Child extends Parent {
    childProp: any
}

type Foo<T> = <P extends Parent>(parent: P) => T

function createFooFunction<T>(arg: T): Foo<T> {
    // Error here!
    return (child: Child): T => {
        return arg;
    }
}

In typescript 2.3 this is acceptable, but typescript 2.4 produces this error

Type '(child: Child) => T' is not assignable to type 'Foo<T>'.
Types of parameters 'child' and 'parent' are incompatible.
    Type 'P' is not assignable to type 'Child'.
    Type 'Parent' is not assignable to type 'Child'.
        Property 'childProp' is missing in type 'Parent'.

In regards to the last line of the error, I noticed that if I make the Child's properties optional, then typescript will be satisfied, i.e. if I make this change

interface Child extends Parent {
    childProp?: any
}

Although that is not an ideal fix, since in my case, childProp is required.

I also noticed that changing the Foo's argument type directly to Parent will also satisfy typescript, i.e. making this change

type Foo<T> = (parent: Parent) => T

This isn't a fix either though, since I do not control the Foo type, nor the Parent. They both come from vendor .d files, so I cannot modify them.

But either way, I'm not sure I understand why this is an error. The Foo type is saying that it requires something that extends Parent, and Child is such an object, so why would typescript think its not assignable?

Edit: I've marked this as answered since adding the --noStrictGenericChecks flag will suppress the errors (accepted answer here). However, I would still like to know why this is an error in the first place since I'd rather keep the strict checks and refactor my code if its wrong, rather than just short circuiting it.

So to re-iterate the heart of the question, since Child extends Parent, why does typescript no longer think that Child is assignable to Parent, and in terms of OOP generics, why is that more correct than it was before?

2

2 Answers

2
votes

Version 2.4 introduces stricter checking for generics

Read this https://blogs.msdn.microsoft.com/typescript/2017/06/27/announcing-typescript-2-4/ and look for the heading Stricter checking for generics

From the article:

As a temporary workaround for any breakage, you may be able to suppress some of these errors using the new --noStrictGenericChecks flag.

It also introduces strict contravariance for callbacks.

4
votes

I think the problem is that you are returning a function that requires a Child as an argument, but assigning it to a function that has to be able to take anything that derives from Parent, not just Child.

Let's say you call createFooFunction<T> a get Foo<T>. By the definition of Foo<T>, it's possible to make a class Child2 that extends Parent and pass it as an argument to the Foo<T> that you got. This is a problem since you actually returned a function that can only get Child as an argument, never Child2.

This is actually more correct. Remember that you're not taking a Child and assigning it to Parent. You're taking a function that only expects a Child and assigning it to one that can take any kind of Parent. This is very different. If, on the other hand, we were dealing with a return value and not an input, this would be fine. This is the difference between covariance and contravariance, which can be a little confusing in the beginning. In C#, you'd be able to specify this in your own generic classes using in and out keywords.

In C# for example, if you had an IEnumerable<Child> (declared as IEnumerable<out T>), it's also naturally an IEnumerable<Parent>, since you're iterating - you're getting Child objects out - you can also get Parent objects out, because every Child is also a Parent. Therefore, you can assign IEnumerable<Child> to IEnumerable<Parent> but not the other way around! Since you wouldn't be guaranteed to get a Child object.

On the other hand, if you had something like IComparer<Parent> (declared as IComparer<in T>) that can compare two Parent objects - well since every Child is also a Parent, it can also compare any two Child objects. Therefore, you can assign IComparer<Parent> to IComparer<Child>, but not the other way around - something that can compare a Child only knows how to compare a Child! Which is your problem here.

You can understand in and out as input (arguments) and output (return) values in callbacks. You can only make inputs more specific (contravariance) and outputs more generic (covariance).

By the way, I think <P extends Parent> is completely useless here (and it adds to the confusion), because you could pass anything that extends Parent in that function anyway even if it wasn't generic. This would only be useful if you returned the type like this: <P extends Parent>(parent: P) => P to preserve the correct type P in the return value. Without generics, you'd have to take Parent and return Parent, so you'd get a Parent back even if you put in something more specific. As to why the error goes away if you get rid of this, I really have no idea.