Description
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: #10570 (comment)
- 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 #10570 (comment)
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.