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

inherit keyword as shorthand to refer to super class fields and methods. #36165

Open
5 tasks done
tadhgmister opened this issue Jan 13, 2020 · 4 comments
Open
5 tasks done
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@tadhgmister
Copy link

tadhgmister commented Jan 13, 2020

Search Terms

Inherit, inheritance, type inference, subclass, superclass, override,

Suggestion

This issue addresses the lack of easy way to refer to types of fields or methods of a base class or implemented interfaces without a high amount of repetition. Proposed is a keyword inherit which will act as a type alias which is contextually resolved for the corresponding type of the base class.

A simpler way to understand this proposal is to think of it as a solution to #10570 by offering an explicit but convenient way to refer to the base class, let us look at a typical example:

abstract class Geometry {
    public abstract model: "square" | "circle";
}

class Canvas extends Geometry {
    // initially set to square but still allow to change to circle later.
    public model = "square";
    // ERROR  ^ Type 'string' is not assignable to type '"square" | "circle"'
}

In this case we want to preserve the same type for field model from the class Geometry. the most obvious solution is to rewrite "square" | "circle" but we can do a little better by instead using Geometry["model"] to allow for changes in the base class to propagate automatically.

class Canvas extends Geometry {
    public model: Geometry["model"] = "square";
    //  This way we explicitly inherit the same type as base class.
}

This is better but still requires us to refer to the base class and rewrite the field name for each field initialized in this way. The proposed solution is to add a keyword inherit which will use the base class and the field name to resolve to the appropriate type.

class Canvas extends Geometry {
    public model: inherit = "square";
    //  This would be equivalent to above example using Geometry["model"]
}

Here is an example playground where I define inherit as a type alias where the arguments could be gained automatically from the context of where it is used.
This would also support being used in method parameters and return types which would be equivalent to using Parameters and ReturnType where appropriate. For example each of these 3 declarations would be equivelent to the following comment

class WithInherit extends Base {
    a: inherit; 
    // a: Base["a"]
    b: Readonly<inherit>; // note that we can also compose it with other types
    // b: Readonly<Base["b"]>
    foo(param: inherit): inherit {}
    // foo(param: Parameters<Base["foo"]>[0]): ReturnType<Base["foo"]>{}
}

This would also address #2000 with additional capability, if all parameters and return type use inherit it will ensure the call signature exactly matches the super class. except without forcing redefining it and allowing extensions like Readonly<inherit> shown above or "fly" | inherit shown below.

Use Cases and Examples

Composition

The proposed keyword would not be required to use alone but could be used like any other type alias. This means that further additions to the original type can be done such as Partial<inherit> or similar. Consider this example where we want to override a method doAction to accept additional kinds of parameters:

class FlyingRobot extends Robot {
    doAction(action: inherit | "fly"){
        // type of input is either "fly" or something the base class supports
        if(action == "fly"){
            // support our case
            return this.fly()
        } else {
            // any other value must be valid to base class since we used inherit.
            return super.doAction(action);
        }
    }
    fly(){}
}

This allows us to support an obvious variation of the type defined by the base class and can be easily understood and written without even looking at the original declaration of Robot[“doAction”].

Interfaces and Multiple Implementations

In the event that a class implements several interfaces, inherit will look up fields on an intersection of the base class (if present) and all implemented interfaces:

class Foo extends Base implements A, B {
    a: inherit; // resolves to (Base & A & B)["a"]
}

For cases where multiple interfaces would define the same method or field inherit would resolve to the intersection of all necessary values, or in the case of method overrides resolve only to the last method to remain consistent with Parameters and similar constructs.

Constraints, Valid and Invalid Uses.

The inherit keyword only makes sense inside a class that either extends another class and/or implements at least one interface. If there is no base class then an error should be raised. As well the inherit keyword would only be applicable in one of the following positions:

  • Instance field
  • Getter return value
  • Setter parameter
  • Method parameter
  • Method rest parameter
  • Method return value

For parameters and return types, if the corresponding field on the base class is not a function then a compile error should be thrown. Either with a very similar message to the one thrown by Parameters<0> or one more specific like “cannot inherit parameter when super field “foo” is not a function”.

For rest parameters, inherit should be equivalent to a extends / infer statement with the same number of other parameters as specified, with all other types set to any for the inference. So if the function is written like foo(a: number, b: string, …rest: inherit) the inherit would resolve like this:
Base[“foo”] extends (a:any, b:any, ...rest: infer T)=>any ? T : never. And the never there would never be used since if Base[“foo”] is not a function then a compile error is thrown. Any other case even where Base[“foo”] takes only 1 parameter will still give valid results for the rest parameter.

Note that for fields initialized to arrow functions, it is not valid to use inherit in the parameters or return type of the arrow function. Instead the full field can use inherit then the parameters will be inferred using normal means: Playground Link

class Example implements Base {
    // a and b here are automatically typed when `inherit` resolves to a valid function type. this is nothing new.
    foo: inherit = (a, b) => {};
}

Cases that don’t work

Methods with several overloads and methods with generics are 2 cases where the currently existing Parameters and similar construct cannot fully capture the information. This proposal would suggest that no additional work be done to let inherit do any extra work in these cases but does recognize that that would be possible in the future. This means that if a method has a generic the inherit keyword will only use the constraint without preserving the generic. Similarly when a method has several overloads inherit will only use the type specified in the last overload which without specifying the other necessary call signatures is not valid.

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code *
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

* in order to not break any existing code that may use a type alias called inherit, this would need to allow the keyword to be shadowed similar to other built in names.

@RyanCavanaugh RyanCavanaugh added In Discussion Not yet reached consensus Suggestion An idea for TypeScript labels Jan 13, 2020
@falsandtru
Copy link
Contributor

I don't think inherit keyword is a good approach but any solution for this problem is needed. Re-declaring complex types and following upstream changes of types are complicated.

https://github.com/falsandtru/spica/blob/master/src/promise.ts

@tadhgmister
Copy link
Author

There may be better approaches but I don't think this is too complicated or involves upstream types. The way I see it typescript throws errors that clearly demonstrate inconsistencies in type when appropriate, for example:

class A {
    foo(a: { x: "Some Data", y: "That is known" }) { };
}
class B extends A {
    foo(a: number) { }
    /* Gives this error:
Property 'foo' in type 'B' is not assignable to the same property in base type 'A'.
  Type '(a: number) => void' is not assignable to type '(a: { x: "Some Data"; y: "That is known"; }) => void'.
    Types of parameters 'a' and 'a' are incompatible.
      Type '{ x: "Some Data"; y: "That is known"; }' is not assignable to type 'number'.(2416)
    */
}

So clearly in this case typescript knows that number is invalid and it knows what the valid type expected is, I am basically wanting a way to refer to that expected type easily.

I can for example define inherit as a type alias with a few type arguments, where all the type arguments can be gotten from context. So my suggestion for the special behaviour would automatically grab the values for those types from the surrounding code, they should all be well defined for all given cases.

type Func = (...args: any[]) => any;

// given number of normal parameters before the rest parameter, this gives the appropriate type for the rest parameter.
// there might be a way to define this recursively but for now it is only well defined for 0 to 4 arguments.
// otherwise this returns a string constant explaining limitation in implementation.
type GetRestParam<F extends Func, P extends number> = P extends 0 ? F extends ((...args: infer A)=>any) ? A : never
    : P extends 1 ? F extends ((x0: any, ...args: infer A) => any) ? A : never
    : P extends 2 ? F extends ((x0: any, x1: any, ...args: infer A) => any) ? A : never
    : P extends 3 ? F extends ((x0: any, x1: any, x2:any, ...args: infer A) => any) ? A : never
    : P extends 4 ? F extends ((x0: any, x1: any, x2:any, x3:any, ...args: infer A) => any) ? A : never
    : "REST PARAMETERS ONLY IMPLEMENTED FOR UP TO 4 PRIOR ARGUMENTS"

type KeysOfMethods<BaseType> = { [K in keyof BaseType]: BaseType[K] extends Func ? K : never }[keyof BaseType];
/**
 * demonstration of how the `inherit` keyword would work as working with explicit type arguments.
 * @param BaseType - intersection of extended base class and all implemented interfaces
 * @param Field - the field name or method name being used. if Pos is not "Field" this must point to a method.
 * @param Pos - one of:
 *              "Field" for the type of the defined field (default if not given)
 *              "Return" for the return type of the method
 *              "Param" for a parameter of the method (Idx specifies position)
 *              "Rest" for a rest parameter (...args).
 * @param Idx - when Pos is "Param" this is the position of the parameter, 
 *              when Pos is "Rest" this is the number of parameters before the rest parameter.
 */
type inherit<BaseType, Field extends (Pos extends ("Param" | "Return" | "Rest") ? KeysOfMethods<BaseType> : keyof BaseType),
    Pos extends "Field" | "Param" | "Return" | "Rest" = "Field",
    Idx extends (Pos extends "Param" | "Rest" ? number : never) = never
    > = Pos extends "Field" ? BaseType[Field]
    : BaseType[Field] extends Func
    ? Pos extends "Return" ? ReturnType<BaseType[Field]>
    : Pos extends "Param" ? Parameters<BaseType[Field]>[Idx]
    : Pos extends "Rest" ? GetRestParam<BaseType[Field], Idx>
    : never // case where Pos is not one of the 4 choices
    : never; // case where BaseType[Field] is not a function for one of the choices that require a function

Here is an example usage with explicit type arguments: Also as a Playground

interface Base {
    field: number;
    foo(a: number, b: string): boolean;
    bar(a: number, b: string, c?: any): boolean;
}

class Sub implements Base {
    field: inherit<Base, "field"> = 1;
    foo(a: inherit<Base, "foo", "Param", 0>, b: inherit<Base, "foo", "Param", 1>): inherit<Base, "foo", "Return"> {
        return true;
    }
    bar(a: inherit<Base, "bar", "Param", 0>, ...rest: inherit<Base, "bar", "Rest", 1>): inherit<Base, "bar", "Return"> {
        return true;
    }
}

@nmay231
Copy link

nmay231 commented Jan 24, 2023

I would personally prefer inherit to be an actual prefix keyword (like readonly) rather than part of the type annotation so that entire method declarations can inherit types:

class Foo extends Base {
  inherit method(arg1, arg2) {}
  method(inherit arg1, inherit arg2) {}
  method(arg1: number, arg2: string) {} // Explicitly type
}

@tadhgmister
Copy link
Author

I would personally prefer inherit to be an actual prefix keyword (like readonly) rather than part of the type annotation

See the example of FlyingRobot in the initial post, by using it in the type position it allows composition like a: Readonly<inherit> or doAction(action: inherit | "fly"), if the only use case was to copy the entire field / method type signature then I doubt the team would have been so hesitant to implement a feature like this a long time ago.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

4 participants