question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Consider inferring class members types by implemented interface members (continuation of #340)

See original GitHub issue

Continuation of #340, #1373, #5749, #6118, #10570, #16944, #23911.

Why a new issue?

Many previous issues discussed about having contextual types based on both extended base class members and implemented interface members. This proposal only applies to implements, not extends.

Discussions in #6118 ends with an edge case when a class extends a base class and implements another interface. I think this problem would not occur if the scope is limited to implements keyword only, and I believe would be a step forward from having parameters inferred as any.

Previous issues have been locked, making it impossible to continue the discussion as time passes.

Proposal

Using terminology “option 1”, “option 2”, “option 3” referring to this comment: https://github.com/microsoft/TypeScript/issues/10570#issuecomment-296860943

  • For members from extends keep option 1.
  • For members from implements:
    • Use option 3 for non-function properties that don’t have type annotation.
    • Use option 2 for function properties and methods.

Examples

Code example in the linked comment:

    class C extends Base.Base implements Contract {
        item = createSubitem(); // ⬅️ No type annotation -- inferred as `Subitem`
    }

Since only the implements keyword is considered, item will be inferred as Subitem.

Example in https://github.com/microsoft/TypeScript/issues/10570#issuecomment-296860943

interface I {
  kind: 'widget' | 'gadget';
}

class C implements I {
  kind = 'widget'; // ⬅️ No type annotation -- inferred as 'widget' | 'gadget'
}

// Above behavior is consistent with:
const c: I = {
  kind: 'widget' // ⬅️ c.kind is also 'widget' | 'gadget'
}
interface I {
  kind: 'widget' | 'gadget';
}

class C implements I {
  kind: 'widget' = 'widget'; // ⬅️ Explicit type annotation required to pin as 'widget'
}

Example in #16944:

interface IComponentLifecycle<P> {
  componentWillReceiveProps?(nextProps: Readonly<P>, nextContext: any): void;
}

interface IProps {
  hello: string;
}

class X implements IComponentLifecycle<IProps> {
  componentWillReceiveProps(nextProps) {
    //                      ^ Contextually typed as Readonly<IProps>
  }
}

Example in #340:

interface SomeInterface1 {
    getThing(x: string): Element;
}

interface SomeInterface2 {
    getThing(x: number): HTMLElement;   
}

declare class SomeClass implements SomeInterface1, SomeInterface2 {
    getThing(x) {
        //   ^ Contextually inferred from
        //     (SomeInterface1 & SomeInterface2)['getThing']
        //     As of TS 3.5, it is an implicit any.
    }
}

// Above behavior is consistent with:
const c: SomeInterface1 & SomeInterface2 = {
    getThing(x) {
        //   ^ Implicit any, as of TS 3.5
    },
}

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.

Issue Analytics

  • State:open
  • Created 4 years ago
  • Reactions:14
  • Comments:10 (2 by maintainers)

github_iconTop GitHub Comments

9reactions
lukeedcommented, Aug 5, 2021

I was surprised to find that this wasn’t already possible, especially given the fact that plain objects can be quickly stamped out using a complex type without needing to redefine all type/method arguments repeatedly:

type Hello = {
    add(x: number, y: number): number;
}

let demo: Hello = {
    add(x, y) {
        return x + y;
    }
}

This allows the definition/contract to be written once & all implementations must then abide by it.

However, with classes, all classes have to abide by the definition (of course), but each class definition has to redeclare the interface definition. This is unnecessarily duplicative, especially since the implemented interface is already “loaded” and used as part of the type checking:

interface Hello {
    add(x: number, y: number): number;
}

class Demo implements Hello {
    add(x: string, y: string) {
        return x + y;
    }
    //=> "Type '(x: string, y: string) => string' is not assignable to type '(x: number, y: number) => number'."
}

AKA – TS already knows exactly what it’s supposed to be.

Requiring that the definition be inlined into every implementation means that the interface is really just an existence and a “repeat after me” check. The let demo: Hello approach allows for strictness and brevity, but classes achieve strictness thru duplication.

1reaction
svicaliforniacommented, Aug 7, 2020
  • If the property/method name was assigned in one or more ancestor classes but always without an explicit type, then use the inferred type from the highest (farthest) ancestor class where the property/method name was used. Without explicit typing, we should expect the inferred type from the highest ancestor class to carry down to each subclass. (This is similar to option 2 of #10570, except more specific about ancestor selection.)

This would absolutely not work. The highest ancestor isn’t always assignable to the lowest one:

class Animal {
    parent = new Animal();
    move() { }
}
class Dog extends Animal {
    parent = new Dog();
    woof() {
        this.parent.woof();
    }
}
class Greyhound extends Dog {
    // are you suggesting we infer this as Animal?
    //  that isn't assignable to Dog.parent so this would just be an error, how is that useful?
    parent = new Dog()
}

I’m saying that if an instance property in a subclass needs a different type than in its superclass, then that new type should be explicitly defined, like this:

class Dog extends Animal {
    parent: Dog = new Dog();
    woof() {
        this.parent.woof();
    }
}

Otherwise, it should inherit the type from its superclass, which maximizes polymorphism by default — in this case, making Dog more interchangeable with its superclass Animal and other subclasses of Animal.

There’s no need — and lots of inconvenience — to force developers to re-confirm their intent by explicitly typing field after adding interface

that is why I’m pushing for #36165, putting field: inherit = ... isn’t all that much inconvenience. And by having to put it there you are prompted to consider if it should be something different instead.

But we shouldn’t need to add an inherit keyword just to get the benefits of type inheritance that other OOP languages provide by default. If we favor type inheritance and require developers to apply more specific types when they are specifically needed, then we will make TypeScript’s type inheritance more useful and similar to other languages, and I believe that the preferable choice in the vast majority of use cases.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Explicit Interface Implementation - C# Programming Guide
A class can implement interfaces that contain a member with the same signature in C#. Explicit implementation creates a class member ...
Read more >
Handbook - Interfaces - TypeScript
When an interface type extends a class type it inherits the members of the class but not their implementations. It is as if...
Read more >
Computer Sciences (COMP SCI) - Courses - Guide
This course introduces balanced search trees, graphs, graph traversal algorithms, hash tables and sets, and complexity analysis and about classes of problems ...
Read more >
Distance Education for Teacher Training: Modes, Models, and ...
Section II: Methods: What Leads to Successful Teaching and Learning in a ... and students also believe that video adds to the quality...
Read more >
Understanding and using interfaces in TypeScript
Types of variables are not predictable in JavaScript. ... A class or function can implement an interface to define the implementation of the ......
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found