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

Default generic return types for class methods #18636

Closed
Joge97 opened this issue Sep 21, 2017 · 4 comments
Closed

Default generic return types for class methods #18636

Joge97 opened this issue Sep 21, 2017 · 4 comments
Labels
Needs More Info The issue still hasn't been fully clarified

Comments

@Joge97
Copy link

Joge97 commented Sep 21, 2017

#13487 added default generic types, but it's still not possible to provide a default implementation for a method, which returns the default generic type:

class A {
    foo: any;
}

class B extends A {
    bar: any;
}

class C<T = A> {
    // this is the default implementation of the `create` method
    public create(): T {
        // Until now the error `Type 'A' is not assignable to type T` is thrown
        // To surpress this error you have to make an unsafe upcast to `T`
        // e.g. `return new A() as T;`
        return new A();
    }
}

class D extends C<B> {
    // An error should be thrown and the user should be forced to overwrite the default implementation,
    // because he provided another Type than the default type `A`
}

class E extends C<A> {
    // The default type `A` was specified,
    // so the default implementation of the method is fine
}

class F extends C {
    // No type was specified and the default type is used,
    // so the default implementation of the method is fine
}

Here the class C, which has the default type A for its generic type T, provides a default implementation for the method create. If this method is not overwritten it returns an instance of the default type A. Now there are two possible options of extending the class C:

  • Another type than the default type A was specified: An error should be thrown, that the type of the returned value of the create method is not assignable to the specified type and the method should be overwritten to return a value of the correct type.
  • The same type as the default type A or no type was specified: The default implementation of the create method works fine and no error should be thrown.

With this approach unsafe upcast to the generic type T of the return value of the default create method would be prevented.

@mhegazy
Copy link
Contributor

mhegazy commented Sep 21, 2017

Default type parameters just allow users of the type to avoid specifying the default on every reference.

In side the case, T is still a type parameter and can not be assumed to be the default. remember a class can be instantiated with new and callers can pass type parameters. so it is legal to write (new C<number>()).create() and expect the result to be a number as the class contract suggests.

Even assuming that these classes has to be abstract, it is not clear to me what is the relationship between a type parameter and a method definition. you could have multiple type parameters that are optional, e.g. class C<T= number, U = string, V = any>, then which ones require you to redefine a method when you extend? are they the methods that use them in return type? what about parameter types? what about ones that use them implicitly?

@mhegazy mhegazy added the Needs More Info The issue still hasn't been fully clarified label Sep 21, 2017
@Joge97
Copy link
Author

Joge97 commented Sep 28, 2017

It is clear to me, that all types declared by the generic type should equal the generic type and this behaviour would not be changed.

However, I forgot to say, that the desired behaviour with default parameters only applies if the generic type was combined with an extend constraint:

class C<T extends A = A> {
    // this is the default implementation of the `create` method
    public create(): T {
        // Until now the error `Type 'A' is not assignable to type T` is thrown
        // To suppress this error, you must make an unsafe upcast to `T`
        // e.g. `return new A() as T;`
        return new A();
    }
}

With this approach only public/protected return types and public/protected class fields will be affected. Other type declarations like parameters would persists. Even multiple type parameters would not change the behaviour, because everywhere where a public/protected method, which returns a generic type with an extend constraint and a default type or writes a value, which has the type of the default type, to a class member, the method would no longer be valid and must be overwritten.

So, when exactly a method should be overwritten:

  • If a public/protected method return a generic type, which has an extend constraint with a default type and the user has specified another type than the default type.
  • If the class has a public/protected class filed, which has the type of a generic type with extend constraint and default type, and a public/protected method explicitly or implicitly writes a value with type of the default type to it, but the user has specified another type than the default type.
  • Getters and setters are treated the same way

Here is a more real-world scenario to understand it better:

/* AbstractAPI for defining a base API */

abstract class AbstractController {
    public abstract control(): void;
}

abstract class AbstractPermissions {
    public abstract asList(): string[];
}

class EmptyPermissions extends AbstractPermissions {
    public asList(): string[] {
        return [];
    }
}

// The generic type U has an extend constraint and a default type
abstract class AbstractAPI<T extends AbstractController,
    U extends AbstractPermissions = EmptyPermissions> {
    private _controller: T;
    private _permissions: U;

    public getController(): T {
        return this._controller;
    }

    public get controller(): T {
        return this.getController();
    }

    public getPermissions(): U {
        return this._permissions;
    }

    public get permissions(): U {
        return this.getPermissions();
    }
	
    public abstract createController(): T;
	
    // this is the default implementation of the `createPermissions` method
    public createPermissions(): U {
	// Until now the error `Type 'EmptyPermissions' is not assignable to type U` is thrown
        // To suppress this error, you must make an unsafe upcast to `U`
        // e.g. `return new EmptyPermissions() as U;`
        return new EmptyPermissions();
    }

    public do(): void {
        // Until now the error `Type 'EmptyPermissions' is not assignable to type U` is thrown
        // To suppress this error, you must make an unsafe upcast to `U`
        // e.g. `this._permissions = new EmptyPermissions() as U;`
        this._permissions = new EmptyPermissions();
    }
}

/* SearchAPI with custom controller and permissions */

class SearchController extends AbstractController {
    public control(): void {
        console.log('Hello search world!')
    }
}

class SearchPermissions extends AbstractPermissions {
    public asList(): string[] {
        return ['search'];
    }
}

// An error should be thrown, that the `createPermissions` method must be overwritten,
// because it returns `EmptyPermissions` and not `SearchPermissions`
// A second error should be thrown, that the `do` method must be overwritten,
// because it writes an `EmptyPermissions` and not a `SearchPermissions` value
// to `this._permissions`, which is implicitly public accessible 
class SearchAPI extends AbstractAPI<SearchController, SearchPermissions> {
    public createController(): SearchController {
        return new SearchController();
    }
}

/* PublicAPI with custom controller, but empty permissions */

class PublicController extends AbstractController {
    public control(): void {
        console.log('Hello public world!')
    }
}

// No error should be thrown, because the default permissions type is used
class PubliAPI extends AbstractAPI<PublicController> {
    public createController(): PublicController {
        return new PublicController();
    }
}

I hope now it is clearer what I mean with a default implementation of methods, which interfere with generic types with an extend constraint and default type.

Maybe another keyword like default should be introduced:

class C<T extends A = A> {
    public default create(): T {
        return new A();
    }
}

This would make it a lot clearer I think.

@mhegazy
Copy link
Contributor

mhegazy commented Oct 12, 2017

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

@mhegazy mhegazy closed this as completed Oct 12, 2017
@Joge97
Copy link
Author

Joge97 commented Oct 13, 2017

@mhegazy Have you reviewed my last comment, before closing the issue?

@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
Needs More Info The issue still hasn't been fully clarified
Projects
None yet
Development

No branches or pull requests

2 participants