Skip to content

Commit 621bbe5

Browse files
Tadhg McDonald-JensenTadhg McDonald-Jensen
authored andcommitted
PoC for #36165 'inherit' keyword
Adds the example inherit type alias with generics into the es5.d.ts lib and adds some logic to getTypeFromTypeAliasReference in checker.go that auto fills the generics when omitted to the 'inherit' type alias. Also has updated tests to see the correct behaviour of the intended feature. AI usage disclosure: - the go code was mostly written by AI and should not be considered production quality. - the test code was composed by AI from many examples I wrote to test the feature.
1 parent 9eedcb0 commit 621bbe5

7 files changed

Lines changed: 1178 additions & 0 deletions

File tree

‎tsc/internal/bundled/libs/lib.es5.d.ts‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1577,6 +1577,46 @@ interface ArrayLike<T> {
15771577
readonly [n: number]: T;
15781578
}
15791579

1580+
declare namespace inherit {
1581+
type _Func = (...args: any[]) => any;
1582+
1583+
// given number of normal parameters before the rest parameter, this gives the appropriate type for the rest parameter.
1584+
// there might be a way to define this recursively but for now it is only well defined for 0 to 4 arguments.
1585+
// otherwise this returns a string constant explaining limitation in implementation.
1586+
type _GetRestParam<F extends _Func, P extends number> = P extends 0 ? F extends ((...args: infer A)=>any) ? A : never
1587+
: P extends 1 ? F extends ((x0: any, ...args: infer A) => any) ? A : never
1588+
: P extends 2 ? F extends ((x0: any, x1: any, ...args: infer A) => any) ? A : never
1589+
: P extends 3 ? F extends ((x0: any, x1: any, x2:any, ...args: infer A) => any) ? A : never
1590+
: P extends 4 ? F extends ((x0: any, x1: any, x2:any, x3:any, ...args: infer A) => any) ? A : never
1591+
: "REST PARAMETERS ONLY IMPLEMENTED FOR UP TO 4 PRIOR ARGUMENTS"
1592+
1593+
type _KeysOfMethods<BaseType> = { [K in keyof BaseType]: BaseType[K] extends _Func ? K : never }[keyof BaseType];
1594+
1595+
}
1596+
/**
1597+
* demonstration of how the `inherit` keyword would work as working with explicit type arguments.
1598+
* @param BaseType - intersection of extended base class and all implemented interfaces
1599+
* @param Field - the field name or method name being used. if Pos is not "Field" this must point to a method.
1600+
* @param Pos - one of:
1601+
* "Field" for the type of the defined field (default if not given)
1602+
* "Return" for the return type of the method
1603+
* "Param" for a parameter of the method (Idx specifies position)
1604+
* "Rest" for a rest parameter (...args).
1605+
* @param Idx - when Pos is "Param" this is the position of the parameter,
1606+
* when Pos is "Rest" this is the number of parameters before the rest parameter.
1607+
*/
1608+
type inherit<BaseType, Field extends (Pos extends ("Param" | "Return" | "Rest") ? inherit._KeysOfMethods<BaseType> : keyof BaseType),
1609+
Pos extends "Field" | "Param" | "Return" | "Rest" = "Field",
1610+
Idx extends (Pos extends "Param" | "Rest" ? number : never) = never
1611+
> = Pos extends "Field" ? BaseType[Field]
1612+
: BaseType[Field] extends inherit._Func
1613+
? Pos extends "Return" ? ReturnType<BaseType[Field]>
1614+
: Pos extends "Param" ? Parameters<BaseType[Field]>[Idx]
1615+
: Pos extends "Rest" ? inherit._GetRestParam<BaseType[Field], Idx>
1616+
: never // case where Pos is not one of the 4 choices
1617+
: never; // case where BaseType[Field] is not a function for one of the choices that require a function
1618+
1619+
15801620
/**
15811621
* Make all properties in T optional
15821622
*/

‎tsc/internal/checker/checker.go‎

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24138,6 +24138,129 @@ func (c *Checker) getTupleElementType(t *Type, index int) *Type {
2413824138
return nil
2413924139
}
2414024140

24141+
// getInheritTypeArguments implements the `inherit` proof-of-concept. Given a `TypeReferenceNode`
24142+
// that refers to the global `inherit` type alias and was written with no explicit type arguments,
24143+
// it synthesizes the type arguments the alias expects based on where `inherit` appears:
24144+
//
24145+
// inherit<BaseType, Field, Pos, Idx>
24146+
//
24147+
// where:
24148+
// - BaseType is the intersection of the enclosing class's extends type and all implemented interfaces,
24149+
// - Field is a string-literal type of the member name,
24150+
// - Pos is one of "Field" | "Param" | "Return" | "Rest",
24151+
// - Idx is a number-literal type (parameter index for "Param", count of leading params for "Rest").
24152+
//
24153+
// It returns nil when `inherit` is used in a position that is not supported (e.g. outside a class, or
24154+
// not in a field/parameter/return type), so that the caller reports the normal diagnostic.
24155+
func (c *Checker) getInheritTypeArguments(node *ast.Node) []*Type {
24156+
// Determine the enclosing class and its base-type intersection.
24157+
classNode := ast.GetContainingClass(node)
24158+
if classNode == nil {
24159+
return nil
24160+
}
24161+
baseType := c.getInheritBaseType(classNode)
24162+
if baseType == nil {
24163+
return nil
24164+
}
24165+
24166+
// Classify the position of `inherit`. `inherit` may be the direct type of a declaration
24167+
// (`foo: inherit`) or nested inside a composed type (`foo: Readonly<inherit>`,
24168+
// `foo: inherit | "x"`). Walk up through any enclosing type nodes so that composition works;
24169+
// the walk stops at the first non-type ancestor, which should be the owning declaration.
24170+
cur := node
24171+
for cur.Parent != nil && ast.IsTypeNode(cur.Parent) {
24172+
cur = cur.Parent
24173+
}
24174+
parent := cur.Parent
24175+
if parent == nil {
24176+
return nil
24177+
}
24178+
stringArg := func(s string) *Type { return c.getStringLiteralType(s) }
24179+
24180+
switch {
24181+
case ast.IsPropertyDeclaration(parent) && parent.Type() == cur:
24182+
// `public foo: inherit` -> inherit<Base, "foo", "Field">
24183+
name := c.getInheritMemberName(parent)
24184+
if name == "" {
24185+
return nil
24186+
}
24187+
return []*Type{baseType, stringArg(name), stringArg("Field")}
24188+
24189+
case ast.IsParameterDeclaration(parent) && parent.Type() == cur:
24190+
// Parameter type: either a normal parameter or a rest parameter of a method.
24191+
paramNode := parent
24192+
method := paramNode.Parent
24193+
if method == nil || !ast.IsFunctionLikeDeclaration(method) {
24194+
return nil
24195+
}
24196+
name := c.getInheritMemberName(method)
24197+
if name == "" {
24198+
return nil
24199+
}
24200+
params := method.Parameters()
24201+
index := -1
24202+
for i, p := range params {
24203+
if p == paramNode {
24204+
index = i
24205+
break
24206+
}
24207+
}
24208+
if index < 0 {
24209+
return nil
24210+
}
24211+
idxArg := c.getNumberLiteralType(jsnum.Number(index))
24212+
if paramNode.AsParameterDeclaration().DotDotDotToken != nil {
24213+
// `...rest: inherit` -> inherit<Base, name, "Rest", index>
24214+
return []*Type{baseType, stringArg(name), stringArg("Rest"), idxArg}
24215+
}
24216+
// `param: inherit` -> inherit<Base, name, "Param", index>
24217+
return []*Type{baseType, stringArg(name), stringArg("Param"), idxArg}
24218+
24219+
case ast.IsFunctionLikeDeclaration(parent) && parent.Type() == cur:
24220+
// Method return type: `foo(): inherit` -> inherit<Base, name, "Return">
24221+
name := c.getInheritMemberName(parent)
24222+
if name == "" {
24223+
return nil
24224+
}
24225+
return []*Type{baseType, stringArg(name), stringArg("Return")}
24226+
}
24227+
24228+
return nil
24229+
}
24230+
24231+
// getInheritMemberName returns the text of a declaration's name, or "" if it is not a simple
24232+
// identifier/string name (e.g. a computed property name, which `inherit` does not support).
24233+
func (c *Checker) getInheritMemberName(decl *ast.Node) string {
24234+
name := decl.Name()
24235+
if name == nil {
24236+
return ""
24237+
}
24238+
switch name.Kind {
24239+
case ast.KindIdentifier, ast.KindStringLiteral, ast.KindNumericLiteral, ast.KindPrivateIdentifier:
24240+
return name.Text()
24241+
}
24242+
return ""
24243+
}
24244+
24245+
// getInheritBaseType computes the BaseType argument for `inherit`: the intersection of the class's
24246+
// extends type (if any) and all implemented interface types. Returns nil if the class neither
24247+
// extends nor implements anything.
24248+
func (c *Checker) getInheritBaseType(classNode *ast.Node) *Type {
24249+
classType := c.getDeclaredTypeOfSymbol(c.getSymbolOfDeclaration(classNode))
24250+
var baseTypes []*Type
24251+
baseTypes = append(baseTypes, c.getBaseTypes(classType)...)
24252+
for _, typeRefNode := range ast.GetImplementsHeritageClauseElements(classNode) {
24253+
t := c.getReducedType(c.getTypeFromTypeNode(typeRefNode))
24254+
if !c.isErrorType(t) {
24255+
baseTypes = append(baseTypes, t)
24256+
}
24257+
}
24258+
if len(baseTypes) == 0 {
24259+
return nil
24260+
}
24261+
return c.getIntersectionType(baseTypes)
24262+
}
24263+
2414124264
/**
2414224265
* Get type from reference to type alias. When a type alias is generic, the declared type of the type alias may include
2414324266
* references to the type parameters of the alias. We replace those with the actual type arguments by instantiating the
@@ -24160,6 +24283,16 @@ func (c *Checker) getTypeFromTypeAliasReference(node *ast.Node, symbol *ast.Symb
2416024283
typeParameters := c.typeAliasLinks.Get(symbol).typeParameters
2416124284
if len(typeParameters) != 0 {
2416224285
numTypeArguments := len(typeArguments)
24286+
// `inherit` proof-of-concept: when the global `inherit` type alias is used with no explicit
24287+
// type arguments, synthesize them from the syntactic position in the enclosing class (base
24288+
// type(s), member name, position kind, and index) and instantiate the alias with those.
24289+
// If the position is not a valid use of `inherit`, fall through so the normal
24290+
// "requires N type arguments" diagnostic is reported.
24291+
if numTypeArguments == 0 && symbol.Name() == "inherit" {
24292+
if inheritArgs := c.getInheritTypeArguments(node); inheritArgs != nil {
24293+
return c.getTypeAliasInstantiation(symbol, inheritArgs, nil)
24294+
}
24295+
}
2416324296
minTypeArgumentCount := c.getMinTypeArgumentCount(typeParameters)
2416424297
if numTypeArguments < minTypeArgumentCount || numTypeArguments > len(typeParameters) {
2416524298
message := core.IfElse(minTypeArgumentCount == len(typeParameters),

‎tsc/internal/fourslash/tests/util/util.go‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -937,6 +937,11 @@ var CompletionGlobalTypeDecls = []fourslash.CompletionsExpectedItem{
937937
Kind: new(lsproto.CompletionItemKindClass),
938938
SortText: new(string(ls.SortTextGlobalsOrKeywords)),
939939
},
940+
&lsproto.CompletionItem{
941+
Label: "inherit",
942+
Kind: new(lsproto.CompletionItemKindClass),
943+
SortText: new(string(ls.SortTextGlobalsOrKeywords)),
944+
},
940945
&lsproto.CompletionItem{
941946
Label: "ArrayLike",
942947
Kind: new(lsproto.CompletionItemKindInterface),
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
inheritTypeAliasPoC.ts(87,5): error TS2322: Type '"square"' is not assignable to type '"circle"'.
2+
inheritTypeAliasPoC.ts(116,5): error TS2322: Type '"triangle"' is not assignable to type '"circle" | "square"'.
3+
inheritTypeAliasPoC.ts(122,44): error TS2322: Type 'string' is not assignable to type 'boolean'.
4+
inheritTypeAliasPoC.ts(128,15): error TS2322: Type 'number' is not assignable to type 'string'.
5+
inheritTypeAliasPoC.ts(138,8): error TS2707: Generic type 'inherit' requires between 2 and 4 type arguments.
6+
inheritTypeAliasPoC.ts(141,21): error TS2707: Generic type 'inherit' requires between 2 and 4 type arguments.
7+
8+
9+
==== inheritTypeAliasPoC.ts (6 errors) ====
10+
// Proof-of-concept test suite for the `inherit` type alias (issue #36165).
11+
//
12+
// `inherit` is a global type alias (defined in lib.es5.d.ts) that, when used with no explicit
13+
// type arguments, is resolved contextually by the checker to the matching member type of the
14+
// enclosing class's base class and/or implemented interfaces:
15+
//
16+
// - Instance field -> Base[Field]
17+
// - Method parameter -> Parameters<Base[Field]>[Index]
18+
// - Method rest parameter -> trailing tuple after `Index` leading params
19+
// - Method return value -> ReturnType<Base[Field]>
20+
//
21+
// where Base is the intersection of the extends type and all implemented interfaces.
22+
//
23+
// This file is both a demonstration and an executable spec. Cases marked "SHOULD RESOLVE" are the
24+
// intended behavior; cases marked "SHOULD ERROR" document intentional constraints. Where the
25+
// current PoC does not yet match the intended behavior (notably `inherit` nested inside a composed
26+
// type such as `Readonly<inherit>` or `inherit | "x"`), the baseline will reveal the gap.
27+
28+
interface Base {
29+
field: "square" | "circle";
30+
foo(a: number, b: string): boolean;
31+
bar(a: number, b: string, c?: any): boolean;
32+
}
33+
34+
// ---------------------------------------------------------------------------
35+
// 1. Baseline: every supported position, implements an interface.
36+
// ---------------------------------------------------------------------------
37+
class AllPositions implements Base {
38+
field: inherit = "square"; // SHOULD RESOLVE: "square" | "circle"
39+
foo(a: inherit, b: inherit): inherit { // SHOULD RESOLVE: a:number, b:string, ret:boolean
40+
return true;
41+
}
42+
bar(a: inherit, ...rest: inherit): inherit { // SHOULD RESOLVE: a:number, rest:[string, any?], ret:boolean
43+
return true;
44+
}
45+
}
46+
47+
// ---------------------------------------------------------------------------
48+
// 2. Equivalence with the explicit (generic) form that compiles today.
49+
// ---------------------------------------------------------------------------
50+
class ExplicitForm implements Base {
51+
field: inherit<Base, "field"> = "circle";
52+
foo(a: inherit<Base, "foo", "Param", 0>, b: inherit<Base, "foo", "Param", 1>): inherit<Base, "foo", "Return"> {
53+
return true;
54+
}
55+
bar(a: inherit<Base, "bar", "Param", 0>, ...rest: inherit<Base, "bar", "Rest", 1>): inherit<Base, "bar", "Return"> {
56+
return true;
57+
}
58+
}
59+
60+
// ---------------------------------------------------------------------------
61+
// 3. extends-only (class base, no interfaces).
62+
// ---------------------------------------------------------------------------
63+
class Geometry {
64+
model: "square" | "circle" = "square";
65+
render(scale: number): void {}
66+
}
67+
class Canvas extends Geometry {
68+
model: inherit = "square"; // SHOULD RESOLVE: "square" | "circle"
69+
render(scale: inherit): inherit {} // SHOULD RESOLVE: scale:number, ret:void
70+
}
71+
72+
// ---------------------------------------------------------------------------
73+
// 4. Multiple bases: extends + implements, intersected as (Base & A & B).
74+
// ---------------------------------------------------------------------------
75+
interface A { a: number; shared: string; }
76+
interface B { b: "x" | "y"; shared: string; }
77+
class BaseC { c: boolean = true; }
78+
class Multi extends BaseC implements A, B {
79+
a: inherit = 1; // SHOULD RESOLVE: number
80+
b: inherit = "x"; // SHOULD RESOLVE: "x" | "y"
81+
c: inherit = false; // SHOULD RESOLVE: boolean (from extends)
82+
shared: inherit = "s"; // SHOULD RESOLVE: string (A & B agree)
83+
}
84+
85+
// ---------------------------------------------------------------------------
86+
// 4b. Conflicting member types across interfaces narrow to their intersection.
87+
// Foo.shape = "square" | "circle", Bar.shape = "circle" | "triangle"
88+
// => (Foo & Bar)["shape"] = "circle"
89+
// ---------------------------------------------------------------------------
90+
interface Foo { shape: "square" | "circle"; }
91+
interface Bar { shape: "circle" | "triangle"; }
92+
class IntersectNarrow implements Foo, Bar {
93+
shape: inherit = "circle"; // SHOULD RESOLVE: "circle" (only common member)
94+
}
95+
class IntersectNarrowBad implements Foo, Bar {
96+
shape: inherit = "square"; // SHOULD ERROR: "square" not in the intersection "circle"
97+
~~~~~
98+
!!! error TS2322: Type '"square"' is not assignable to type '"circle"'.
99+
}
100+
101+
// ---------------------------------------------------------------------------
102+
// 5. Composition: inherit nested inside other type constructors.
103+
// ---------------------------------------------------------------------------
104+
interface Robot {
105+
sensors: {sight?: boolean, hearing?: boolean, touch?: boolean};
106+
doAction(action: "walk" | "turn"): void;
107+
}
108+
class FlyingRobot implements Robot {
109+
sensors: Required<inherit> = {sight: true, hearing: false, touch: false}; // SHOULD RESOLVE: {sight: boolean, hearing: boolean, touch: boolean};
110+
doAction(action: inherit | "fly"): inherit { // SHOULD RESOLVE: action:"walk"|"turn"|"fly"
111+
}
112+
}
113+
114+
// ---------------------------------------------------------------------------
115+
// 6. Arrow-function field: field-level inherit, params inferred by assignment.
116+
// ---------------------------------------------------------------------------
117+
class ArrowField implements Base {
118+
field: inherit = "square";
119+
foo: inherit = (a, b) => true; // SHOULD RESOLVE: foo:(a:number,b:string)=>boolean
120+
bar: inherit = (a, b, c) => true;
121+
}
122+
123+
// ---------------------------------------------------------------------------
124+
// 7. Negative / assignment checks: inherit must produce the NARROW type, not `any`.
125+
// ---------------------------------------------------------------------------
126+
class WrongField implements Base {
127+
field: inherit = "triangle"; // SHOULD ERROR: not "square" | "circle"
128+
~~~~~
129+
!!! error TS2322: Type '"triangle"' is not assignable to type '"circle" | "square"'.
130+
foo(a: inherit, b: inherit): inherit { return true; }
131+
bar(a: inherit, ...rest: inherit): inherit { return true; }
132+
}
133+
class WrongReturn implements Base {
134+
field: inherit = "square";
135+
foo(a: inherit, b: inherit): inherit { return "nope"; } // SHOULD ERROR: string not assignable to boolean
136+
~~~~~~
137+
!!! error TS2322: Type 'string' is not assignable to type 'boolean'.
138+
bar(a: inherit, ...rest: inherit): inherit { return true; }
139+
}
140+
class WrongParamUse implements Base {
141+
field: inherit = "square";
142+
foo(a: inherit, b: inherit): inherit {
143+
const s: string = a; // SHOULD ERROR: number not assignable to string
144+
~
145+
!!! error TS2322: Type 'number' is not assignable to type 'string'.
146+
return true;
147+
}
148+
bar(a: inherit, ...rest: inherit): inherit { return true; }
149+
}
150+
151+
// ---------------------------------------------------------------------------
152+
// 8. Invalid positions / constraints.
153+
// ---------------------------------------------------------------------------
154+
class NoBase {
155+
x: inherit = 1; // SHOULD ERROR: class has no base/interfaces
156+
~~~~~~~
157+
!!! error TS2707: Generic type 'inherit' requires between 2 and 4 type arguments.
158+
}
159+
160+
type OutsideClass = inherit; // SHOULD ERROR: inherit used outside a class
161+
~~~~~~~
162+
!!! error TS2707: Generic type 'inherit' requires between 2 and 4 type arguments.
163+

0 commit comments

Comments
 (0)