Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Covariance on generic return type with constraints is not respected anymore (TypeScript 2.4.2) #17509

Closed
AGiorgetti opened this issue Jul 29, 2017 · 3 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@AGiorgetti
Copy link

AGiorgetti commented Jul 29, 2017

TypeScript Version: 2.4.1 / 2.4.2 / nightly (2.5.0-dev.201xxxxx)

Code

interface IMyObject {
    id: string;
    name: string
}

class MyObject implements IMyObject {
    id: string;
    name: string;
}

interface IMyFactoryInterface {
    // Covariant return type (IMyObject and its subtypes should be accepted)
    <T extends IMyObject>(name: string): T;
}

// Covariant return type (IMyObject and its subtypes should be accepted)
type IMyFactoryType = <T extends IMyObject>(name: string) => T;  

function MyFactory(name: string): IMyObject {
    return <IMyObject>{
        id: name,
        name: name
    };
}

function MyFactoryClass(name: string): MyObject {
    var obj = new MyObject();
    obj.id = name;
    obj.name = name;
    return obj;
}

// It does not respect covariance on the generic type constraint
// Type 'IMyObject' is not assignable to type 'T'
let factoryFunc: IMyFactoryInterface;
factoryFunc = MyFactory;
console.log(factoryFunc('a').name);
factoryFunc = MyFactoryClass;
console.log(factoryFunc('a').name);

let factoryFunc2: IMyFactoryType;
factoryFunc2 = MyFactory;
console.log(factoryFunc2('a').name);
factoryFunc2 = MyFactoryClass;
console.log(factoryFunc2('a').name);

Try it out compiling with the lastet TypeScript installment and with the previos 2.4.0 version.

npm install typescript2.4.2 -g
tsc

npm install typescript2.4.0 -g
tsc

Expected behavior:

everything should compile and work without errors

Actual behavior:

compiling with TypeScript 2.4.1+ I get the following error:

 Type 'IMyObject' is not assignable to type 'T'

Workaround

disable the new strict checking on generics with 'noStrictGenericChecks' in tsconfig.json

@ahejlsberg
Copy link
Member

This is working as intended and is a result of the stricter checks introduced in #16368. In your example, the IMyFactoryType (or the interface, they're structurally identical) represents a function that is supposed to return exactly typed values of any type that derives from IMyObject, even though the function doesn't actually have any parameters involving T that would allow it to discover what T is and create an appropriate return value. In other words:

function foo<T extends IMyObject>(name: string): T {
    // T could be any type that derives from IMyObject, but there's no way
    // to discover what T is because it isn't used in a parameter type.
}

The MyFactory function isn't assignable to IMyFactoryType because, while MyFactory creates an object that is assignable to IMyObject, it doesn't create something that is assignable to T (which could be any type that derives from IMyObject). In other words, this obviously wouldn't work:

interface IMyDerived extends IMyObject {
    foo: string;
    bar: number;
}

let derived = factoryFunc<IMyDerived>("hello");  // How could function know what to do?

In fact, the only call to factoryFunc that would be safe is one where the type argument is omitted, which causes type inference to just infer the IMyObject bound. But that's just a single corner case.

It may be that you meant for the type parameter to be declared on the IMyFactoryType instead of on the call signature within the type:

type IMyFactoryType<T extends IMyObject> = (name: string) => T;

This would make a lot more sense, but it would of course also require you to specify a type argument when you reference the type.

@ahejlsberg ahejlsberg added the Working as Intended The behavior described is the intended behavior; this is not a bug label Jul 29, 2017
@AGiorgetti
Copy link
Author

AGiorgetti commented Jul 30, 2017

I gave more tought about this and you are right, there's really no way to discover what T was in the code above; I was indeed using the generic return type in a hacky way to avoid an explicit type assertion in the code.

In short the problem arose from a function that call some external (non typed) JavaScript library that creates wrappers for some service endpoints for which I wrote a TypeScript code generator (just service interfaces and DTOs).

I was using the generic type on the return value to avoid an extra type assertion in the code, which I'll admit is not really a best practice.

I got rid of the problem adding an 'extra layer' that handles factory functions discovering and the type assertions in a more correct way.

Now I have something like this:

// Ok I'll admit this is a hack to avoid an explicit type assertion in the code.
// The correct code should have been:
// const f = getServiceFactory("service1") as (name: string) => MyService1;
// This is very ugly to write, so I did this:
// const f = getServiceFactory<MyService1>("service1"); // ofc you can create e service and assert it to the wrong type... pay attention
function getServiceFactory<T extends IMyService>(name: string): (name: string) => T {
    let myServiceFactoryFunc: (name: string) => IMyService; // <- explicitly return the common interface

    myServiceFactoryFunc = getTheFactoryFuncFromJS(name); // some code to retrieve an instace of a factory function from 'name'

    return myServiceFactoryFunc as (name: string) => T;
}

Closing the issue, thanks for looking into it!

@seepel
Copy link

seepel commented Aug 15, 2017

Sorry if this isn't the right place to put this, but if the function does have a parameter should it be inferable? For example redux has the typing:

interface Action {
    type: any;
}

interface Dispatch {
    <A extends Action>(action: A): A;
}

With strict set to true the following fails to compile with something along the lines Type action is not assignable to A which surprised me:

const dispatch: Dispatch = (action: Action) => action;

@microsoft microsoft locked and limited conversation to collaborators Jun 14, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

3 participants