5. Classifiers
5.1. N4JS Specific Classifiers
N4JS provides three new metatypes: class, interface, and enums. In this section we describe classes and interfaces. These metatypes, called classifiers, share some common properties which are described before type specific properties are outlined in the following sections.
All of these metatypes can be marked with type access modifiers:
enum N4JSTypeAccessModifier: project | public;
5.1.1. Properties
Properties defined by syntactic elements:
annotations
-
Arbitrary annotations, see Annotations for details.
accessModifier
-
N4JS type access modifier:
public
, orproject
;public
can be combined with@Internal
; if export istrue
the default is else the default isprivate
. name
-
The simple name of a classifier. If the classifier is defined by an anonymous class expression, an artificial but unique name is created. The name needs to be a valid identifier, see Valid Names.
typePars
-
Collection of type parameters of a generic classifier; empty by default.
ownedMembers
-
Collection of owned members, i.e. methods and fields defined directly in the classifier and, if present, the explicitly defined constructor. Depending on the concrete classifier, additional constraints are defined.
typingStrategy
-
The definition-site typing strategy. By default nominal typing is used. See Structural Typing for details.
The following pseudo properties are defined via annotations:
export
-
Boolean property set to true if the
export
modifier is set. If value is true, the classifier may be accessible outside the project. final
-
Boolean property which is set to final if annotation
@Final
is set. Also see Final Modifier deprecated
-
Boolean property set to true if annotation
@Deprecated
is set.Version 0.4, not implemented in Version 0.3
We additionally define the following pseudo properties:
acc
-
Type access modifier as described in Accessibility of Types, Top-Level Variables and Function Declarations, it is the aggregated value of the
accessModifier
and the export property. owned{Fields|Methods|Getters|Setters|Accessors}
-
Filters ownedMembers by metatype, short for
etc. members
-
Reflexive transitive closure of all members of a classifier and its super classifiers, see Common Semantics of Classifiers on how this is calculated.
fields|methods|getters|setters|accessors
-
Filters members by metatype, short for
etc. superClassifiers
-
Classes and interface may extend or implement classes or interfaces. Any class or interface extended or interface implemented is called super classifier. We distinguish the directly subtyped classifiers and from the transitive closure of supertypes
5.1.2. Common Semantics of Classifiers
Req. IDE-42: Subtyping of Classifiers (ver. 1)
For a given type C
, and supertypes directly subtyped
C
, the following constraints must be true:
-
The supertypes must be accessible to the subtype:
must be accessible toC
. -
All type parameters of the direct supertypes have to be bound by type arguments in the subtype and the type arguments have to be substitutable types of the type parameters.
-
Wildcards may not be used as type argument when binding a supertype’s type parameters.
-
A classifier cannot be directly subtyped directly multiple times:
In order to simplify the following constraints, we use the pseudo property to refer to all members of a classifier. This includes all members directly declared by the classifier itself, i.e. the , and all members inherited from its super classifiers. The concrete mechanisms for inheriting a member are different and further constraint (cf. Redefinition of Members). A classifier only inherits its members from its direct supertypes, although the supertypes may contains members inherited from their supertypes.
5.1.3. Classes
5.1.3.1. Definition of Classes
Classes are either declared with a class declaration on top level, or they can be used as anonymous classes in expressions. The latter may have a name, which may be used for error messages and reflection.
At the current stage, class expressions are effectively disabled at least until the semantics of them are finalized in ECMAScript 6.
In N4JS (as in many other languages) multi-inheritance of classes is not supported.
Although the diamond problem
(of functions being defined in both superclasses) could be solved via union and intersection types, this would lead to problems when calling these super implementations.
This is particularly an issue due to JavaScript not supporting
multiple prototypes.[27]
Interfaces, however, allow for multi-inheritance. Since the former can also define functions with bodies, this is not a hard restriction.
5.1.3.1.1. Syntax
N4ClassDeclaration <Yield>:
=>(
{N4ClassDeclaration}
annotations+=Annotation*
(declaredModifiers+=N4Modifier)*
'class' typingStrategy=TypingStrategyDefSiteOperator? name=BindingIdentifier<Yield>?
)
TypeVariables?
ClassExtendsClause<Yield>?
Members<Yield>
;
N4ClassExpression <Yield>:
{N4ClassExpression}
'class' name=BindingIdentifier<Yield>?
ClassExtendsClause<Yield>?
Members<Yield>;
fragment ClassExtendsClause <Yield>*:
'extends' (
=>superClassRef=ParameterizedTypeRefNominal ('implements' ClassImplementsList)?
| superClassExpression=LeftHandSideExpression<Yield>
)
| 'implements' ClassImplementsList
;
fragment ClassImplementsList*:
implementedInterfaceRefs+=ParameterizedTypeRefNominal
(',' implementedInterfaceRefs+=ParameterizedTypeRefNominal)*
;
fragment Members <Yield>*:
'{'
ownedMembers+=N4MemberDeclaration<Yield>*
'}'
;
5.1.3.1.2. Properties
These are the properties of class, which can be specified by the user: Syntax N4 Class Declaration and Expression
abstract
-
Boolean flag indicating whether class may be instantiable; default is
false
, see Abstract Classes. external
-
Boolean flag indicating whether class is a declaration without implementation or with an external (non-N4JS) implementation; default is
false
, see Definition Site Structural Typing. defStructural
-
Boolean flag indicating whether subtype relation uses nominal or structural typing, see Definition Site Structural Typing for details.
superType/sup
-
The type referenced by is called direct superclass of a class, and vice versa the class is a direct subclass of . Instead of , we sometimes simply write . The derived set is defined as the transitive closures of all direct and indirect superclasses of a class. If no supertype is explicitly stated, classes are derived from
N4Object
. implementedInterfaces
/interfaces
-
Collection of interfaces directly implemented by the class; empty by default. Instead of
implementedInterfaces
, we simply writeinterfaces
. ownedCtor
-
Explicit constructor of a class (if any), see Constructor and Classifier Type.
And we additionally define the following pseudo properties:
ctor
-
Explicit or implicit constructor of a class, see Constructor and Classifier Type.
fields
-
Further derived properties for retrieving all methods (property ), fields (property ), static members (property ), etc. can easily be added by filtering properties or .
5.1.3.1.3. Type Inference
The type of a class declaration or class expression C
(i.e., a class definition in general) is of type constructor{C}
if it is not abstract,
that is if it can be instantiated.
If it is abstract, the type of the definition simply is type{C}
:
Req. IDE-43: Structural and Nominal Supertypes (ver. 1)
The type of supertypes and implemented interfaces is always the nominal type, even if the supertype is declared structurally.
5.1.3.2. Semantics
This section deals with the (more or less) type-independent constraints on classes.
Class expressions are not fully supported at the moment.
Definition: Transitive closure of members
The reflexive transitive closure of members of a class is indirectly defined by the override and implementation constraints defined in Redefinition of Members.
Note that since overloading is forbidden, the following constraint is true [28]:
Remarks: Class and method definition is quite similar to the proposed ECMAScript version 6 draft [ECMA15a(p.S13.5)], except that an N4 class and members may contain
-
annotations, abstract and access modifiers
-
fields
-
types
-
implemented interfaces
Note that even static
is used in ECMAScript 6.
Mixing in members (i.e. interface’s methods with default implementation or fields) is similar to mixing in members from roles as defined in [Dart13a(p.S9.1)]. It is also similar to default implementations in Java 8 [Gosling15a]. In Java, however, more constraints exist, (for example, methods of interfaces must be public).
This first example shows a very simple class with a field, a constructor and a method.
class C {
data: any;
constructor(data: any) {
this.data = data;
}
foo(): void { }
}
The following example demonstrate how a class can extend a superclass and implement an interface.
interface I {
foo(): void
}
class C{}
class X extends C implements I {
@Override
foo(): void {}
}
A class C
is a subtype of another classifier S
(which can be a class or interface) if the other classifier S
is (transitively) contained in the supertypes (superclasses or implemented interfaces) of the class:
Req. IDE-44: Implicit Supertype of Classes (ver. 1)
-
The implicit supertype of all classes is
N4Object
. All classes with no explicit supertype are inherited fromN4Object
. -
If the supertype is explicitly set to
Object
, then the class is not derived fromN4Object
. Meta-information is created similar to anN4Object
-derived class. Usually, there is no reason to explicitly derive a class fromObject
. -
External classes are implicitly derived from , unless they are annotated with
@N4JS
(cf.External Declarations).
5.1.3.3. Final Modifier
Extensibility refers to whether a given classifier can be subtyped.
Accessibility is a prerequisite for extensibility.
If a type cannot be seen, it cannot be subclassed.
The only modifier influencing the extensibility directly is the annotation @Final
, which prevents all subtyping.
The following table shows how to prevent other projects or vendors from subtyping by also restricting the accessibility of the constructor:
Type C Settings |
Subclassed in | ||
---|---|---|---|
Project |
Vendor |
World |
|
|
no |
no |
no |
|
yes |
no |
no |
|
yes |
yes |
no |
Since interfaces are always to be implemented, they must not be declared final.
5.1.3.4. Abstract Classes
A class with modifier abstract
is called an abstract class and has its property set to true.
Other classes are called concrete classes.
Req. IDE-45: Abstract Class (ver. 1)
-
A class
C
must be declared abstract if it owns or inherits one or more abstract members and neither C nor any interfaces implemented by C implements these members. A concrete class has to, therefore, implement all abstract members of its superclasses’ implemented interfaces. Note that a class may implement fields with field accessors and vice versa. -
An abstract class may not be instantiated.
-
An abstract class cannot be set to final (with annotation
@Final
).
Req. IDE-46: Abstract Member (ver. 1)
-
A member declared as abstract must not have a method body (in contrary a method not declared as abstract have to have a method body).
-
Only methods, getters and setters can be declared as abstract (fields cannot be abstract).
-
It is not possible to inherit from an abstract class which contains abstract members which are not visible in the subclass.
-
An abstract member must not be set to final (with annotation
@Final
). -
Static members must not be declared abstract.
Remarks:
-
We decided to disallow abstract static members, since we cannot guarantee that a static members is not accessed in all cases
-
Only static members can override static members and only instance members can override other instance members of course.
-
An abstract member must not be declared in a final class (i.e. a class annotated with
@Final
). This is not explicitly defined as constraint in [Req-IDE-46] since abstract classes must not defined final anyway. We also do not produce error message for abstract members in final classes since these errors would be consequential errors.
Abstract members might be declared private, as they can be accessed from within the module. This is to be changed in order to be aligned with TypeScript, cf. #1221. However we also want to add class expressions — and then the abstract members may be accessed (and overridden) in nested classes created by means of class expressions. |
5.1.3.5. Non-Instantiable Classes
To make a class non-instantiable outside a defining compilation unit, i.e. disallow creation of instances for this class, simply declare the constructor as private. This can be used for singletons.
5.1.3.6. Superclass
Req. IDE-47: Superclass (ver. 1)
For a class C
with a supertype , the following constraints must hold;
-
must reference a class declaration
S
-
S
must be be extendable in the project ofC
-
All abstract members in
S
must be accessible fromC
:
M
is accessible fromC
.
Note thatM
need not be an owned member ofS
and that this constraint applies even ifC
is abstract).
All members of superclasses become members of a class. This is true even if the owning classes are not directly accessible to a class. The member-specific access control is not changed.
5.1.4. Interfaces
5.1.4.1. Definition of Interfaces
5.1.4.1.1. Syntax
N4InterfaceDeclaration <Yield>:
=> (
{N4InterfaceDeclaration}
annotations+=Annotation*
(declaredModifiers+=N4Modifier)*
'interface' typingStrategy=TypingStrategyDefSiteOperator? name=BindingIdentifier<Yield>?
)
TypeVariables?
InterfaceImplementsList?
Members<Yield>
;
fragment InterfaceImplementsList*:
'implements' superInterfaceRefs+=ParameterizedTypeRefNominal
(',' superInterfaceRefs+=ParameterizedTypeRefNominal)*
;
5.1.4.1.2. Properties
These are the additional properties of interfaces, which can be specified by the user:
superInterfaces
-
Collection of interfaces extended by this interface; empty by default. Instead of
superInterfaces
, we simply writeinterfaces
.
5.1.4.1.4. Semantics
Interfaces are used to describe the public API of a classifier. The main requirement is that the instance of an interface, which must be an instance of a class since interfaces cannot have instances, provides all members declared in the interface. Thus, a (concrete) class implementing an interface must provide implementations for all the fields, methods, getters and setters of the interface (otherwise it the class must be declared abstract). The implementations have to be provided either directly in the class itself, through a superclass, or by the interface if the member has a default implementation.
A field declaration in an interface denotes that all implementing classes can either provide a field of the same name and the same(!) type or corresponding field accessors. If no such members are defined in the class or a (transitive) superclass, the field is mixed in from the interface automatically. This is also true for the initializer of the field.
All instance methods, getters and setters declared in an interface are implicitly abstract if they do not provide a default implementation.
The modifier abstract
is not required, therefore, in the source code.
The following constraints apply:
Req. IDE-48: Interfaces (ver. 1)
For any interface I
, the following must hold:
-
Interfaces may not be instantiated.
-
Interfaces cannot be set to final (with annotation @Final): .
-
Members of an interface must not be declared private. The default access modifier in interfaces is the the type’s visibility or
project
, if the type’s visibility isprivate
. -
Members of an interface, except methods, must not be declared
@Final
:not allowing field accessors to be declared final was a deliberate decision, because it would complicate the internal handling of member redefinition; might be reconsidered at a later time -
The literal may not be used in the initializer expression of a field of an interface.
This restriction is required, because the order of implementation of these fields in an implementing class cannot be guaranteed. This applies to both instance and static fields in interfaces, but in case of static fields,this
is also disallowed due to Static Members of Interfaces.
It is possible to declare members in interfaces with a smaller visibility as the interface itself. In that case, clients of the interface may be able to use the interface but not to implement it.
In order to simplify modeling of runtime types, such as elements, interfaces do not only support the notation of static methods but constant data fields as well. Since IDL [OMG14a] is used to describe these elements in specifications (and mapped to JavaScript via rules described in [W3C12a]) constant data fields are an often-used technique there and they can be modeled in N4JS 1:1.
As specified in [Req-IDE-56], interfaces cannot contain a constructor i.e.
.
The following example shows the syntax for defining interfaces. The second interface extends the first one. Note that methods are implicitly defined abstract in interfaces.
interface I {
foo(): void
}
interface I2 extends I {
someText: string;
bar(): void
}
If a classifier C
implements an interface I
, we say I
is implemented by C
.
If C
redefines members declared in I
, we say that these members are implemented by C
.
Members not redefined by C
but with a default implementations are mixed in or consumed by C
.
We all cases we call C
the implementor.
Besides the general constraints described in Common Semantics of Classifiers, the following constraints must hold for extending or implementing interfaces:
Req. IDE-49: Extending Interfaces (ver. 1)
For a given type
I
, and directly extended by I
, the following constraints must be true:
-
Only interfaces can extend interfaces: must be interfaces.
-
An interface may not directly extend the same interface more than once:
for any . -
An interface may (indirectly) extend the same interface more than once only if
-
is not parameterized, or
-
in all cases is extended with the same type arguments for all invariant type parameters.
Note that for type parameters of that are declared covariant or contravariant on definition site, different type arguments may be used.
-
-
All abstract members in , , must be accessible from
I
:
M
is accessible fromI
.
Note thatM
need not be an owned member of .
Req. IDE-50: Implementing Interfaces (ver. 1)
For a given type
C
, and directly implemented
by C
, the following constraints must be true:
-
Only classes can implement interfaces:
C
must be a Class. -
A class can only implement interfaces: must be interfaces.
-
A class may not directly implement the same interface more than once:
for any . -
A class may (indirectly) implement the same interface more than once only if
-
is not parameterized, or
-
in all cases is implemented with the same type arguments for all invariant type parameters.
Note that for type parameters of that are declared covariant or contravariant on definition site, different type arguments may be used.
-
-
All abstract members in , , must be accessible from
C
:
M
is accessible fromC
.
Note thatM
need not be an owned member of .
For default methods in interfaces, see Default Methods in Interfaces.
5.1.5. Generic Classifiers
Classifiers can be declared generic by defining a type parameter via type-param
.
Definition: Generic Classifiers
A generic classifier is a classifier with at least one type parameter.
That is, a given classifier C
is generic if and only if .
If a classifier does not define any type parameters, it is not generic, even if its superclass or any implemented interface is generic.
The format of the type parameter expression is described in Parameterized Types. The type variable defined by the type parameter’s type expression can be used just like a normal type inside the class definition.
If using a generic classifier as type of a variable, it may be parameterized.
This is usually done via a type expression (cf. Parameterized Types) or via typearg
in case of supertypes.
If a generic classifier defines multiple type variables, these variables are bound in the order of their definition.
In any case, all type variables have to be bound.
That means in particular that raw types are not allowed. (cf Parameterized Types for details).
If a generic classifier is used as super classifier, the type arguments can be type variables.
Note that the type variable of the super classifier is not lifted, that is to say that all type variables are to be explicitly bound in the type references used in the extend
, with
, or implements
section using typearg
.
If a type variable is used in typearg
to bound a type variable of a type parameter, it has to fulfil possible type constraints (upper/lower bound) specified in the type parameter.
This example demonstrates how to define a generic type and how to refer to it in a variable definition.
export class Container<T> {
private item: T;
getItem(): T {
return this.item;
}
setItem(item: T): void {
this.item = item;
}
}
This type can now be used as a type of a variable as follows
import Container from "p/Container"
var stringContainer: Container<string> = new Container<string>();
stringContainer.setItem("Hello");
var s: string = stringContainer.getItem();
In line 3, the type variable T
of the generic class Container
is bound to string
.
For a given generic class G
class A{}
class B{}
class C extends A{}
class G<S, T extends A, U extends B> {
}
the variable definition
var x: G<Number,C,B>;
would bind the type variables as follows:
|
|
Bound by first type argument, no bound constraints defined for |
|
|
Bound by second type argument, |
|
|
Bound by third type argument, |
Req. IDE-51: Generic Superclass, Type Argument with Type Variable (ver. 1)
For a given generic superclass SuperClass
class SuperClass<S, T extends A, U extends B> {};
and a generic subclass SubClass
class SubClass<X extends A> extends SuperClass<Number, X, B> {..};
the variable definition
var s: SubClass<C>;
would bind the type variables as follows:
TypeVariable | Bound to | Explanation |
---|---|---|
|
|
Type variable |
|
|
Type variable |
|
|
Type variable |
|
|
Bound by first type argument specified in variable definition. |
5.1.6. Definition-Site Variance
In addition to use-site declaration of variance in the form of Java-like wildcards, N4JS provides support for definition-site declaration of variance as known from languages such as C# and Scala.
The variance of a parameterized type states how its subtyping relates to its type arguments’ subtyping.
For example, given a parameterized type G<T>
and plain types A
and B
, we know
-
if
G
is covariant w.r.t. its parameterT
, then -
if
G
is contravariant w.r.t. its parameterT
, then -
if
G
is invariant w.r.t. its parameterT
, then
Note that variance is declared per type parameter, so a single parameterized type with more than one type parameter may be, for example, covariant w.r.t. one type parameter and contravariant w.r.t. another.
Strictly speaking, a type parameter/variable itself is not co- or contravariant;
however, for the sake of simplicity we say T
is covariant as a short form for G
is covariant with respect to its type parameter T
(for contravariant and invariant accordingly).
To declare the variance of a parameterized classifier on definition site, simply add keyword in
or out
before the corresponding type parameter:
class ReadOnlyList<out T> { // covariance
// ...
}
interface Consumer<in T> { // contravariance
// ...
}
In such cases, the following constraints apply.
Req. IDE-174: Definition-Site Variance (ver. 1)
Given a parameterized type with a type parameter , the following must hold:
-
T
may only appear in variance-compatible positions:-
if
T
is declared on definition site to be covariant, then it may only appear in covariant positions within the type’s non-private member declarations. -
if
T
is declared on definition site to be contravariant, then it may only appear in contravariant positions within the type’s non-private member declarations. -
if
T
is invariant, i.e. neither declared covariant nor declared contravariant on definition site, then it may appear in any position (where type variables are allowed).Thus, no restrictions apply within the declaration of private members and within the body of field accessors and methods.
-
-
definition-site variance may not be combined with incompatible use-site variance:
-
if
T
is declared on definition site to be covariant, then no wildcard with a lower bound may be provided as type argument forT
. -
if
T
is declared on definition site to be contravariant, then no wildcard with an upper bound may be provided as type argument forT
. -
if
T
is invariant, i.e. neither declared covariant nor declared contravariant on definition site, then any kind of wildcard may be provided as type argument.Unbounded wildcards are allowed in all cases.
-
For illustration purposes, let’s compare use-site and definition-site declaration of variance. Since use-site variance is more familiar to the Java developer, we start with this flavor.
class Person {
name: string;
}
class Employee extends Person {}
interface List<T> {
add(elem: T)
read(idx: int): T
}
function getNameOfFirstPerson(list: List<? extends Person>): string {
return list.read(0).name;
}
Function getNameOfFirstPerson
below takes a list and returns the name of the first person in the list.
Since it never adds new elements to the given list, it could accept List
s of any subtype of Person
, for example a List<Employee>
.
To allow this, its formal parameter has a type of List<? extends Person>
instead of List<Person>
.
Such use-site variance is useful whenever an invariant type, like List
above, is being used in a way such that it can be treated as if it were co- or contravariant.
Sometimes, however, we are dealing with types that are inherently covariant or contravariant, for example an ImmutableList
from which we can only read elements would be covariant.
In such a case, use-site declaration of variance is tedious and error-prone: we would have to declare the variance wherever the type is being used and would have to
make sure not to forget the declaration or otherwise limit the flexibility and reusability of the code (for example, in the above code we could not call getNameOfFirstPerson
with a List<Employee>
).
The solution is to declare the variance on declaration site, as in the following code sample:
interface ImmutableList<out T> {
// add(elem: T) // error: such a method would now be disallowed
read(idx: int): T
}
function getNameOfFirstPerson2(list: ImmutableList<Person>): string {
return list.read(0).name;
}
Now we can invoke getNameOfFirstPerson2
with a List<Employee>
even though the implementor of getNameOfFirstPerson2
did not add a
use-site declaration of covariance, because the type ImmutableList
is declared to be covariant with respect to its parameter T
, and this applies globally
throughout the program.
5.2. Members
A member is either a method (which may be a special constructor function), a data field, or a getter or a setter. The latter two implicitly define an accessor field. Similar to object literals, there must be no data field with the same name as a getter or setter.
(overriding, implementation and consumption) is described in Redefinition of Members.
5.2.1. Syntax
enum N4JSMemberAccessModifier: private | project | protected | public;
N4MemberDeclaration: N4MethodDeclaration | N4FieldDeclaration | N4GetterDeclaration | N4SetterDeclaration;
5.2.1.1. Properties
Members share the following properties:
annotations
-
Arbitrary annotations, see Annotations for details.
accessModifier
-
N4JS member access modifier:
private
,project
,potected
, orpublic
; the latter two can be combined with@Internal
; default isproject
for classes and private interfaces. For a non-private interface defaults to the interface’s visibility. name
-
The simple name of the member, that is an identifier name (cf. Valid Names).
static
-
Boolean property to distinguish instance from classifier members, see Static Members.
The following pseudo properties are defined via annotations:
deprecated
-
Boolean property set to true if annotation
@Deprecated
is set. [29]
And we additionally define the following pseudo properties:
acc
-
Member access modifier as described in Accessibility of Members, it is the aggregated value of the and the property.
owner
-
Owner classifier of the member.
typeRef
-
Type of the member—this is the type of a field or the type of the method which is a function type (and not the return type).
assignability
-
Enumeration, may be one of the following values:
set
-
Member may only be set, i.e. it could only be used on the left hand side of an assignment.
get
-
Member may only be retrieved, i.e. it could only be used on the right hand side of an assignment. This is the default setting for methods.
any
-
Member may be set or retrieved, i.e. it could only be used on the left or right hand side of an assignment. This is the default setting for fields.
is related but not equal to writable modifiers used for fields.
We define a partial order on this enumeration as follows: |
abstract
-
All members have a flag , which is user-defined for methods, getters and setter, but which is always false for fields.
The following pseudo property is set to make fields compatible with properties of an object literal, however it cannot be changed:
configurable
-
Boolean flag reflecting the property descriptor , this is always set to false for members.
5.2.2. Semantics
The members of a given classifier C
must be named such that the following constraints are met:
Req. IDE-52: Member Names (ver. 1)
-
The name of a member is given as an identifier, a string literal, a numeric literal, or as a computed property name with a compile-time expression (see Compile-Time Expressions). In particular, string literals, e.g.
['myProp']
, built-in symbols, e.g.[Symbol.iterator]
, and literals of@StringBased
enums are all valid computed property names. -
No two members may have the same name, except one is static and the other is non-static:
-
The member name must be a valid identifier name, see Identifier Grammar.
Thus, overloading of methods is not supported [30] and no field may have the same name as a method. However, overriding of methods, getters, and setters are possible, see Redefinition of Members. Static members may also have the same name as non-static members.[31]
The dollar character $
is not allowed for user-defined member identifiers as the dollar sign is used for rewriting private members.
5.2.3. Methods
Methods are simply JavaScript functions. They are defined similarly to methods as proposed in [ECMA15a(p.S13.5)] except for the type information and some modifiers.
5.2.3.1. Syntax
N4MethodDeclaration <Yield>:
=> ({N4MethodDeclaration}
annotations+=Annotation*
accessModifier=N4JSMemberAccessModifier?
(abstract?=’abstract’ | static?=’static’)?
TypeVariables?
(
generator?='*' LiteralOrComputedPropertyName<Yield> -> MethodParamsReturnAndBody <Generator=true>
| AsyncNoTrailingLineBreak LiteralOrComputedPropertyName<Yield> -> MethodParamsReturnAndBody <Generator=false>
)
) ';'?
;
fragment MethodParamsAndBody <Generator>*:
StrictFormalParameters<Yield=Generator>
(body=Block<Yield=Generator>)?
;
fragment MethodParamsReturnAndBody <Generator>*:
StrictFormalParameters<Yield=Generator>
(':' returnTypeRef=TypeRef)?
(body=Block<Yield=Generator>)?
;
fragment LiteralOrComputedPropertyName <Yield>*:
name=IdentifierName | name=STRING | name=NumericLiteralAsString
| '[' (=>((name=SymbolLiteralComputedName<Yield> | name=StringLiteralAsName) ']') | computeNameFrom=AssignmentExpression<In=true,Yield> ']')
;
SymbolLiteralComputedName <Yield>:
BindingIdentifier<Yield> ('.' IdentifierName)?
;
BindingIdentifier <Yield>:
IDENTIFIER
| <!Yield> 'yield'
| N4Keyword
;
IdentifierName: IDENTIFIER | ReservedWord | N4Keyword;
NumericLiteralAsString: DOUBLE | INT | OCTAL_INT | HEX_INT | SCIENTIFIC_INT;
StringLiteralAsName: STRING;
fragment AsyncNoTrailingLineBreak *: (declaredAsync?='async' NoLineTerminator)?; // See Asynchronous Functions
fragment StrictFormalParameters <Yield>*:
'(' (fpars+=FormalParameter<Yield> (',' fpars+=FormalParameter<Yield>)*)? ')'
;
FormalParameter <Yield>:
{FormalParameter} BindingElementFragment<Yield>
;
fragment BindingElementFragment <Yield>*:
(=> bindingPattern=BindingPattern<Yield>
| annotations+=Annotation*
(
variadic?='...'? name=BindingIdentifier<Yield> ColonSepTypeRef?
)
)
('=' initializer=AssignmentExpression<In=true, Yield>)?
;
fragment ColonSepTypeRef*:
':' declaredTypeRef=TypeRef
;
5.2.3.2. Properties
Methods have all the properties of members and the following additional properties can be explicitly defined:
abstract
-
Method is declared but not defined.
typePars
-
Collection of type parameters of a generic method; empty by default.
returnTypeRef
-
Return type of the method, default return type is . The type of the method as a member of the owning classifier is not the method’s return type but is instead a function type.
fpars
-
List of formal parameters, may be left empty.
body
-
The body of the method (this is not available in the pure types model)
The following pseudo properties are defined via annotations:
final
-
Boolean flag set to true if annotation
@Final
is set. The flag indicates that method must not be overridden in subclasses; see Final Methods. declaresOverride
-
Flag set to true if annotation
@Overrides
is set. The flag indicates that method must override a method of a superclass; see Overriding of Members.
Additionally, we define the following pseudo properties:
overrides
-
True if method overrides a super method or implements an interface method, false otherwise.
typeRef
-
Type of the method. This is, in fact, a function type (and not the return type).
The following pseudo property is set to make methods compatible with properties of an object literal, however it cannot be changed:
enumerable
-
Boolean flag reflecting the property descriptor , this is always set to false for methods.
5.2.3.3. Semantics
Since methods are ECMAScript functions, all constraints specified in Function Type apply to methods as well. This section describes default values and function type conformance which is required for overriding and implementing methods.
In addition, method declarations and definitions have to comply with the constraints for naming members of classifiers (cf. [Req-IDE-52]) and with the constraints detailed in the following sections on final methods (Final Methods), abstract methods (Abstract Methods and method overriding and implementation (Overriding of Members, Implementation of Members).
The following constraints are defined for methods in ECMAScript 6 [ECMA15a(p.207)]
Req. IDE-53: Method Definition ECMAScript 6 (ver. 1)
-
It is a Syntax Error if any element of the BoundNames of StrictFormalParameters also occurs in the VarDeclaredNames of FunctionBody.
-
It is a Syntax Error if any element of the BoundNames of StrictFormalParameters also occurs in the LexicallyDeclaredNames of FunctionBody.
Methods – like functions – define a variable execution environment and therefore provide access to the actual passed-in parameters through the implicit arguments
variable inside of their bodies (c.f. Arguments Object).
Methods are similar to function definitions but they must not be assigned to or from variables.
The following code issues an error although the type of the method would be compatible to the type of the variable v
:
class C {
m(): void {}
}
var v: {function():void} = new C().m;
Req. IDE-54: Method Assignment (ver. 1)
-
In contrast to ECMAScript 2015, methods are defined as readonly, that is, it is not possible to dynamically re-assign a property defined as method with a new value. This is because assigning or re-assigning a method breaks encapsulation. Methods are the Acronyms of a class, their implementation is internal to the class.
-
When assigning a method to a variable, a warning is issued since this would lead to an detached this reference inside the method when it is called without explicitly providing the receiver. No warning is issued only if it is guaranteed that no problems will occur:
-
The method’s body can be determined at compile time (i.e., it has been declared
@Final
) and it lacks usages ofthis
orsuper
. This is true for instance and static methods. -
The method is the constructor.
-
The following code demonstrates problems arising when methods are assigned to variables in terms of function expressions. Given are two classes and instances of each class as follows: |
class C {
m(): void { }
static k(): void {}
}
class D extends C {
@Override m(): void { this.f()}
f(): void {}
@Override static k(): void { this.f()}
static f(): void {}
}
var c: C = new C();
var d: C = new D(); // d looks like a C
Assigning an instance method to a variable could cause problems, as the method assumes this to be bound to the class in which it is defined. This may work in some cases, but will cause problems in particular in combination with method overriding:
var v1: {@This(C)function():void} = c.m;
var v2: {@This(C)function():void} = d.m;
v1.call(c);
v2.call(c);
Calling c.m
indirectly via v1
with c
as this object will work.
However, it won’t work for v2
: the method is overridden in D
, and the method in expects other methods available in D
but not in C
.
That is, the last call would lead to a runtime error as method f
which is called in D.m
won’t be available.
The same scenario occurs in case of static methods if they are retrieved polymorphically via the variables of type constructor{C}
:
var ctor: constructor{C} = C;
var dtor: constructor{C} = D;
var v3: {@This(constructor{C})function():void} = ctor.k;
var v4: {@This(constructor{C})function():void} = dtor.k;
In both cases, the problem could be solved by restricting these kinds of assignments to final methods only. In the static case, the problem would also be solved by accessing the static method directly via the class type (and not polymorphically via the constructor). Both restrictions are severe but would be necessary to avoid unexpected runtime problems.
The following example shows a problem with breaking the encapsulation of a class.
class C {
x: any = "";
f(): void { this.g(this); }
g(c: C): void { c.h(); }
h(): void {}
}
class D extends C {
@Override f(): void {
this.g(this.x);
}
@Override g(c: any) {
// do nothing, do not call h())
}
}
var c = new C();
var d = new D();
var v5: {@This(C)function():void} = c.f;
var v6: {@This(C)function():void} = d.f;
v5.call(c)
v6.call(c)
In D
, method g
is overridden to accept more types as the original method defined in C
.
Calling this new method with receiver type C
(as done in the last line) will cause problems, as in D
not only f
has been adapted but also g
.
Eventually, this would lead to a runtime error as well.
5.2.3.4. Final Methods
By default, methods can be overridden.
To prevent a method from being overridden, it must be annotated with @Final
.
Of course, a method cannot be declared both abstract and final (cf. [Req-IDE-46]). Private methods are implicitly declared final. Because static methods can be overridden in subclasses (which is different to Java), they also can be marked as final.
Default methods in interfaces, cf. Default Methods in Interfaces, may also be declared @Final
.
If a method in an interface is provided with a body, it may be declared final. This will ensure that the given method’s body will be in effect for all instances of the interface. Note that this means that;
-
a class implementing that interface must not define a method with the same name and
-
a class inheriting a method of that name cannot implement this interface.
The latter case is illustrated here:
interface I {
@Final m(): void {}
}
class C1 {
m(): void {}
}
// error at "I": "The method C1.m cannot override final method I.m."
class C2 extends C1 implements I {
}
5.2.3.5. Abstract Methods
A method can be declared without defining it, i.e. without providing a method body, and is then called an abstract method.
Such methods must be declared with modifier abstract
and have their property set to true.
Constraints for abstract methods are covered in [Req-IDE-46] (see Abstract Classes).
In interfaces, methods are always abstract by default and they do not have to be marked as abstract. If a method in an interface provides a body, then this is the default implementation. See Implementation of Members about how the default implementation may be mixed in the consumer.
5.2.3.6. Generic Methods
Methods of generic classes can, of course, refer to the type variables defined by type parameters of the generic class. These type variables are used similarly to predefined or declared types. Additionally, methods may be declared generic independently from their containing class. That is to say that type parameters (with type variables) can be defined for methods as well, just like for generic functions (see Generic Functions).
Req. IDE-55: Type variable names for generic methods (ver. 1)
For a given generic method M
of a class C
, the following
constraint must hold:
Since type variables can be used similarly to types in the scope of a generic class, a generic method may refer to a type variable of its containing class.
class C {
<T> foo(p: T p): T { return p;}
};
If a generic type parameter is not used as a formal parameter type or the return type, a warning is generated unless the method overrides a member inherited from a super class or interface.
5.2.4. Default Methods in Interfaces
If a method declared in an interface defines a body, then this is the so-called default implementation and the method is called a default method. This will be mixed into an implementor of the interface if, and only if, neither the implementing class nor any of its direct or indirect superclasses already provides an implementation for this method; for details see Member Consumption. Since the implementor is not known, some constraints exist for the body. I.e., no access to super is possible, cf. [Req-IDE-124].
In order to declare an interface to provide a default implementation in a definition file, annotation @ProvidesDefaultImplementation
can be used, cf. [Req-IDE-167].
When a method in an interface is provided with a default implementation, it may even be declared @Final
, see Final Methods.
5.2.4.1. Asynchronous Methods
N4JS implements the async/await concept proposed for ECMAScript 7, which provides a more convenient and readable syntax for writing asynchronous code compared to using built-in type Promise directly. This concept can be applied to methods in exactly the same way as to declared functions. See Asynchronous Functions and Asynchronous Arrow Functions for details.
5.2.5. Constructors
A constructor is a special function defined on a class which returns an instance of that class. The constructor looks like a normal method with name "constructor". The constructor can be defined explicitly or implicitly and every class has an (implicit) constructor.
For a given a class C
, the constructor is available via two properties:
-
the explicitly defined constructor (if any).
-
the explicit or implicit constructor.
If C
is provided with an explicit constructor, we have and .
Note that in all cases.
The return type of the constructor of a class C
is C
.
If C
has type parameters , then the return type is . The constructor is called with the operator.
Since the return type of a constructor is implicitly defined by the class, it is to be omitted.
By this definition, a constructor looks like the following:
class C {
public constructor(s: string) {
// init something
}
}
Constructors define a variable execution environment and therefore provide access to the actual passed-in parameters through the implicit variable inside of their bodies (c.f. Arguments Object).
Req. IDE-56: Defining and Calling Constructors (ver. 1)
For a constructor of a class C
, the following conditions
must hold:
-
must neither be abstract nor static nor final and it must not be annotated with
@Override
. -
If a class does not explicitly define a constructor then the constructor’s signature of the superclass constructor is assumed.
-
If a class defines a constructor with formal parameters then this constructor has to be called explicitly in constructors defined in subclasses.
-
If a super constructor is called explicitly, this call must be the only expression of an expression statement which has to be the first statement of the body.
-
Constructors may appear in interfaces, but some restrictions apply:
-
constructors in interfaces must not have a body.
-
constructors in interfaces or their containing interface or one of its direct or indirect super interfaces must be annotated with
@CovariantConstructor
.
-
-
A constructor must not have an explicit return type declaration.
-
The implicit return type of a constructor is
this?
. -
A constructor must not have any type parameters.
Properties of object literals may be called constructor
.
However they are not recognized as constructors in these cases.
Req. IDE-57: Initialization of Final Fields in the Constructor (ver. 1)
-
Required attributes must be initialized:
Note on syntax: ECMAScript 6 defines constructors similarly, [ECMA15a(p.S13.5)]. In ECMAScript 6 the super constructor is not called automatically as well.
The super literal used in order to call super methods is further described in The super Keyword.
5.2.5.1. Structural This Type in Constructor
The use of a structural this reference as a formal parameter type is possible only in constructors.
This parameter can be annotated with @Spec
which causes the compiler to generate initialization code.
Simply using this
as a type in the constructor causes the constructor to require an object providing all public fields of the class for initialization purposes.
The fields have to be set manually as shown in the following code snippet.
class A{
public s: string;
public constructor(src: ~~this) {
this.s = src.s;
}
}
Remarks:
-
The type of the formal parameter
~~this
refers to the structural field type, see Structural Typing for details on structural typing. It contains all public fields of the type. -
Subclasses may override the constructor and introduce additional parameters. They have to call the super constructor explicitly, however, providing a parameter with at least all required attributes of the superclass. Usually the type
this
is replaced with the actual subclass, but in the case of asuper()
call thethis
type of structural formal parameters is replaced with thethis
type of the superclass, hence only required fields of the superclass must be present.
As with other structural references, it is possible to add the structural reference with additional structural members, which can be used to initialize private fields which become not automatically part of the structural field type. For example:
class A{
public s: string;
private myPrivateNumber: number;
public constructor(src: ~~this with { x: number; }) {
this.s = src.s;
this.myPrivateNumber = src.x;
}
}
Defining additional members may become a problem if a subclass defines public fields with the same name, as the ~~this
type will contain these fields in the subclass.
This is marked as an error in the subclass.
Req. IDE-58: Names of additional members of structural this type in constructor (ver. 1)
If the structural this type is used in a constructor of a class C
, and if this structural reference contains an additional structural member , the following constraints must hold true:
-
For any subclass
S
ofC
, with (the subclass does not define its own constructor),S
must not contain a public member with same name as : -
C
itself must not contain a public member with same name as :
The situation described in [Req-IDE-58] is demonstrated in the following code fragment:
class A {
private myPrivateNumber: number;
public constructor(src: ~~this with { x: number; }) {
this.myPrivateNumber = src.x;
}
}
class B extends A {
public x: number; // will cause an error message
}
5.2.5.2. @Spec Constructor
The tedious process of copying the members of the parameter to the fields of the class can be automated via the @Spec
annotation if the argument has ~i~this
structural initializer field typing. More details about this typing can be
found in Structural Read-only, Write-only and Initializer Field Typing. This can be used as shown in the following listing:
class A {
public field: string;
public constructor(@Spec spec: ~i~this) {}
}
let a = new A({field: 'hello'});
console.log(a.field); // prints: hello
The code for initializing the public field of A
is automatically generated, thanks to the @Spec
annotation being
given in the constructor.
Req. IDE-59: @Spec Constructor (ver. 1)
-
Annotation
@Spec
may only appear on a formal parameter of a constructor. Such a formal parameter is then called @Spec parameter or simply spec parameter and its owning constructor is referred to as a @Spec constructor or spec constructor. An argument to the spec parameter is called spec object. -
Only a single formal parameter of a constructor may be annotated with
@Spec
. -
If a formal parameter is annotated with
@Spec
, the parameter’s type must be~i~this
(i.e. a use-site structural initializer field type ofthis
, see Structural Read-only, Write-only and Initializer Field Typing). -
Using the data provided in the spec object, i.e. in the argument to the spec parameter, a spec constructor will automatically initialize
-
all owned data fields and owned setters of the containing class, and
-
all data fields and setters from interfaces implemented by the containing class
if and only if those members are also part of the spec parameter’s structural initializer field type.
-
-
Fields explicitly added to the spec parameter, e.g.
@Spec spec: ~i~this with {name:string}
, are used for initialization if a non-public field of the same name exists in the class, either as an owned member or from an implemented interface. The type of such an additional field must be a subtype of the declared type of the field being initialized:
-
Even if the
@Spec
annotation is used, the super constructor must be invoked explicitly (as usual).
It follows from no. 4 above that
-
non-public data fields and setters are never initialized (because they will never be part of the spec parameter’s structural initializer field type),
-
properties provided in the spec object but not defined in the parameter’s structural initializer field type, are not used for initialization, even if a (protected or private) field of the same name exists in the class,
-
data fields and setters inherited from a super class are never initialized by a spec constructor (instead, this will happen in the spec constructor of the super class).
The last of these implications will be detailed further at the end of the coming section.
@Spec Constructors and Inheritance
Spec constructors are inherited by subclasses that do not have a constructor and, when creating instances of the subclass, will then require properties for writable public fields of the subclass in the spec object and include initialization code for them.
class A {
public fa;
public constructor(@Spec spec: ~i~this) {}
}
class B extends A {
public fb;
}
const b = new B({fa: 'hello', fb: 'world'}); // requires & initializes fb too!
console.log(b.fa); // prints: hello
console.log(b.fb); // prints: world
Public writable fields from implemented interfaces are included as well, i.e. required as property in spec object and
initialized by auto-generated code in the @Spec
constructor:
interface I {
public fi;
}
class B implements I {
public fb;
public constructor(@Spec spec: ~i~this) {}
}
const a = new B({fb: 'hello', fi: 'world'}); // requires & initializes fi too!
console.log(a.fb); // prints: hello
console.log(a.fi); // prints: world
When having a spec constructor in a class B
that extends a super class A
without an owned or inherited spec
constructor, it should be noted that the ~i~this
type will require properties for public writable fields of A
,
but the initialization code automatically generated due to the @Spec
annotation will not initialize those members.
For public writable fields from an interface I
implemented by B
, however, both a property will be required by
~i~this
and initialization code will be generated in the @Spec
constructor. This is illustrated in the
following code example.
class A {
public fa;
}
interface I {
public fi;
}
class B extends A implements I {
public fb;
public constructor(@Spec spec: ~i~this) { // <- fa, fi, fb required in spec object
// Constructor is responsible for initializing fa, fi, fb.
// The @Spec annotation will generate initialization code
// for fb and fi, but not for fa!
}
}
let b = new B({
fa: 'hello', // <- fa is required (removing it would be a compile error)
fi: 'world',
fb: '!!'
});
console.log(b.fa); // undefined
console.log(b.fi); // world
console.log(b.fb); // !!
The rationale for this different handling of fields from super classes and implemented interfaces is
1. fields from an implemented interface are not seen as inherited but rather implemented by implementing class, so from
the @Spec
annotation’s perspective the field is a field of the implementing class, and
2. in case of a field inherited from a super class the correct way of initialization may depend on details of the super
class and has to be taken care of by custom code in the constructor of the subclass (usually by invoking the non-@Spec
constructor of the superclass with super
).
Special Use Cases
The following examples illustrate further details of other use cases of spec constructors.
The base class A
in the examples redefines the constructor already defined in N4Object
. This is not
generally necessary and is only used here to make the example legible.
class A {
public s: string;
public constructor(@Spec spec: ~i~this) {
// initialization of s is automatically generated
}
}
class B extends A {
public t: string;
private n: number;
public constructor(spec: ~~this with {n: number;}) {
super(spec); // only inherited field s is set in super constructor
}
}
class A1 {
public s: string;
public n: number;
public constructor(@Spec spec: ~i~this) {}
}
class B extends A1 {
public constructor() {
super({s:"Hello"}); // <-- error, n must be set in object literal
}
}
class C extends A1 {
public constructor() {
super({s:"Hello"}); // <-- error, n must be set in object literal
this.n = 10; // <-- this has no effect on the super constructor!
}
}
class A2 {
public s: string;
public n: number?; // now n is optional!
public constructor(@Spec spec: ~i~this) {}
}
class D extends A2 {
public constructor() {
super({s:"Hello"}); // and this is ok now!
this.n = 10; // this explains why it is optional
}
}
class A3 {
public s: string;
public n: number = 10; // now n is not required in ~~this
public constructor(@Spec spec: ~i~this) {}
}
class E extends A3 {
public constructor() {
super({s:"Hello"}); // and this is ok now!
}
}
The last case (class E) demonstrates a special feature of the typing strategy modifier in combination with the this
type, see Structural Typing for details.
The constructor in class B
contains an error because the super constructor expects all required attributes in A1
to be set.
The additional initialization of the required field A1.n
as seen in C
does not change that expectation.
In this example, the field n
should not have been defined as required in the first place.
Optional fields like n?
in class A2
or fields with default values like n=10
in class A3
are not required to be part of the spec
object.
Each non- field has to be set in the constructor via the to the parameter otherwise properties are not used to set non- fields.
class C {
public s: string;
n: number;
constructor(@Spec spec: ~i~this) {}
}
// n is ignored here
new C( { s: "Hello", n: 42 });
// but:
var ol = { s: "Hello", n: 42 };
// "ol may be used elsewhere, we cannot issue warning here" at "ol"
new C(ol) ;
// of course this is true for all superfluous properties
// weird is not used in constructor
new C( { s: "Hello", weird: true } );
- Restriction when initializing interface fields via @Spec constructor
-
In most cases, interface definitions in
n4jsd
files simply declare functions and fields that are supposed to be provided by the runtime environment. As a result, there are restrictions as to whether fields of interfaces defined inn4jsd
files can initialized via@Spec
constructors or not. In particular, fields of an interface declared in an4jsd
file cannot be initialized via @Spec constructor if the interface-
is a built-in or
-
does not have an
@N4JS
annotation
-
The following example illustrates this restriction.
export external interface I {
public m: string;
}
@N4JS
export external interface J {
public n: string;
}
import { I } from "Inf";
// I is an external interface WITHOUT @N4JS annotation
class C implements I {
constructor(@Spec spec:~i~this) {}
}
// J is an external interface with @N4JS annotation
class D implements J {
constructor(@Spec spec:~i~this) {}
}
// XPECT warnings --> "m is a property of built-in / provided by runtime / external without @N4JS annotation interface I and can not be initialized in Spec constructor." at "m"
let c:C = new C({m: "Hello"});
// XPECT nowarnings
let d:D = new D({n: "Bye"});
console.log(c.m)
console.log(d.n)
/* XPECT output ---
<==
stdout:
undefined
Bye
stderr:
==>
--- */
In this example, the interface I
is defined in the Inf.n4jsd
file without the @N4JS
annotation. As a result, its field m
cannot be initialized via the @Spec
constructor and hence the output of console.log(c.m)
is undefined
. On the other hand, since the interface J
is declared with the annotation @N4JS
, it is possible to initialize its field n
in the @Spec
constructor. That’s why the result of console.log(d.n)
is Bye
.
5.2.5.4. Covariant Constructors
Usually, the constructor of a subclass need not be override compatible with the constructor of its super class.
By way of annotation @CovariantConstructor
it is possible to change this default behavior and enforce all subclasses to have constructors with override compatible signatures.
A subclass can achieve this by either inheriting the constructor from the super class (which is usually override compatible,
with the special case of @Spec
constructors) or by defining a new constructor with a signature compatible to the inherited constructor.
The same rules as for method overriding apply.
The @CovariantConstructor
annotation may be applied to the constructor, the containing classifier, or both.
It can also be used for interfaces; in fact, constructors are allowed in interfaces only if they themselves or the interface is annotated with @CovariantConstructor
(see [Req-IDE-60]).
Definition: Covariant Constructor
A classifier C
is said to have a covariant constructor
if and
only if one of the following applies:
-
C
has a direct super class and is annotated with@CovariantConstructor
or has a constructor annotated with@CovariantConstructor
. -
C
has a directly implemented interfaceI and `I
is annotated with@CovariantConstructor
orI
has a constructor annotated with@CovariantConstructor
. -
C
has a direct super class or directly implemented interface thathas a covariant constructor
(as defined here).
Note that C
does not need to have an owned(!) constructor; also a constructor inherited from a super class can be declared covariant.
The following rules apply to covariant constructors.
Req. IDE-60: Covariant Constructors (ver. 1)
-
Annotation
@CovariantConstructor
may only be applied to classes, interfaces, and constructors. Annotating a constructor with this annotation, or its containing classifier, or both have all the same effect. -
Given a class
C
with an owned constructor and a super class that has a covariant constructor (owned or inherited, see Covariant Constructors), then must be accessible fromC
,-
must be override compatible with :
This constraint corresponds to [Req-IDE-72] except for the
Override
annotation which is not required here.
-
-
Given a classifier
C
implementing interfaceI
andI
has a covariant constructor (owned or inherited, see Covariant Constructors), we require-
must be accessible from
C
, -
an implementation-compatible constructor must be defined in C with
This constraint corresponds to [Req-IDE-74] except for the
@Override
annotation, which is not required, here. -
Given a classifier
C
without an owned constructor and an extended class or interface that has a covariant constructor (owned or inherited, see Covariant Constructors), we require the inherited constructor ofC
within the context ofC
to be override compatible to itself in the context of . Using notation to denote that a memberM
is to be treated as defined in container typeT
, which means the this-binding is set toT
, we can write:This constraint does not correspond to any of the constraints for the redefinition of ordinary members.
-
The following example demonstrates a use case for covariant constructors.
It shows a small class hierarchy using covariant constructors, Cls
and Cls2
, together with a helper function createAnother
that creates and returns a new instance of the same type as its argument value
.
class A {}
class B extends A {}
@CovariantConstructor
class Cls {
constructor(p: B) {}
}
class Cls2 extends Cls {
constructor(p: A) { // it's legal to generalize the type of parameter 'p'
super(null);
}
}
function <T extends Cls> createAnother(value: T, p: B): T {
let ctor = value.constructor;
return new ctor(p);
}
let x = new Cls2(new A());
let y: Cls2;
y = createAnother(x, new B());
In the code of Covariant Constructors, we would get an error if we changed the type of parameter p
in the constructor of Cls2
to some other type that
is not a super type of B
, i.e. the type of the corresponding parameter of Cls
’s constructor.
If we removed the @CovariantConstructor
annotation on Cls
, we would get an error in the new expression inside function createAnother
.
The next example illustrates how to use @CovariantConstructor
with interfaces and shows a behavior that might be surprising at first sight.
@CovariantConstructor
interface I {
constructor(p: number)
}
class C implements I {
// no constructor required!
}
class D extends C {
// XPECT errors --> "Signature of constructor of class D does not conform to overridden constructor of class N4Object: {function(number)} is not a subtype of {function()}." at "constructor"
constructor(p: number) {}
}
Interface I
declares a covariant constructor expecting a single parameter of type number
.
Even though class C
implements I
, it does not need to define an owned constructor with such a parameter.
According to [Req-IDE-60], it is enough for C
to have a constructor, either owned or inherited, that is override compatible with the one declared by I
.
Class C
inherits the default constructor from N4Object
, which does not have any arguments and is thus override compatible to I
’s constructor.
In addition, subclasses are now required to have constructors which are override compatible with the constructor of class C
, i.e. the one inherited from N4Object
.
Covariant Constructors in Interfaces shows that this is violated even when repeating the exact same constructor signature from interface I
,
because that constructor now appears on the other side of the subtype test during checking override compatibility.
5.2.6. Data Fields
A data field is a simple property of a class.
There must be no getter or setter defined with the same name as the data field.
In ECMAScript 6, a class has no explicit data fields.
It is possible, however, to implicitly define a data field by simply assigning a value to a variable of the this element (e.g. this.x = 10
implicitly defines a field x
).
Data fields in N4JS are similar to these implicit fields in ECMAScript 6 except that they are defined explicitly in order to simplify validation and user assistance.
5.2.6.1. Syntax
N4FieldDeclaration <Yield>:
{N4FieldDeclaration}
FieldDeclarationImpl<Yield>
;
fragment FieldDeclarationImpl <Yield>*:
(declaredModifiers+=N4Modifier)* BogusTypeRefFragment?
declaredName=LiteralOrComputedPropertyName<Yield>
(declaredOptional?='?')?
ColonSepTypeRef?
('=' expression=Expression<In=true,Yield>)?
Semi
;
5.2.6.2. Properties
Fields have the following properties which can be explicitly defined:
declaredOptional
-
Tells whether the accessor was declared optional.
typeRef
-
Type of the field; default value is .
expr
-
Initializer expression, i.e. sets default value.
static
-
Boolean flag set to true if field is a static field.
const
-
Boolean flag set to true if field cannot be changed. Note that const fields are automatically static. Const fields need an initializer. Also see Assignment Modifiers.
is not the (reversed) value of the property descriptor as the latter is checked at runtime while const may or may not be checked at runtime. |
The following pseudo properties are defined via annotations for setting the values of the property descriptor:
enumerable
-
Boolean flag reflecting the property descriptor , set via annotation
@Enumerable(true|false)
. The default value is .[32] declaredWriteable
-
Boolean flag reflecting the property descriptor , set via annotation
@Writeable(true|false)
. The default value is .[33] final
-
Boolean flag making the field read-only, and it must be set in the constructor. Also see Assignment Modifiers.
Derived values for fields
readable
-
Always true for fields.
abstract
-
Always false for fields.
writeable
-
Set to false if field is declared const or final. In the latter case, it may be set in the constructor (cf. Assignment Modifiers).
5.2.6.2.1. Semantics
Req. IDE-61: Attributes (ver. 1)
For any attribute if a
class C
, the following constraints must hold:
-
A required data field must not define an initializer:
-
There must be no other member with the same name of a data field
f
. In particular, there must be no getter or setter defined with the same name:
If a subclass should set a different default value, this has to be done in the constructor of the subclass.
For the relation of data fields and field accessors in the context of extending classes or implementing interfaces see Redefinition of Members.
5.2.6.2.2. Type Inference
The type of a field is the type of its declaration:
The type of a field declaration is either the declared type or the inferred type of the initializer expression:
If the type contains type variables they are substituted according to type parameters which are provided by the reference:
5.2.6.3. Assignment Modifiers
Assignment of data fields can be modified by the assignment modifiers const
(similar to constant variable declarations, see Const) and @Final
.
Req. IDE-62: Const Data Fields (ver. 1)
For a data field f
marked as const
, the following constraints must hold:
-
An initializer expression must be provided in the declaration (except in n4jsd files):
-
A constant data field is implicitly static and must be accessed only via the classifier type. It is not possible, therefore, to use the
this
keyword in the initializer expression of a constant field:
-
A constant data field must not be annotated with
@Final
:
-
Constant data fields are not writeable (cf. [Req-IDE-68]):
Req. IDE-63: Final Data Fields (ver. 1)
For a data field f
marked as @Final
, the following constraints must hold:
-
A final data field must not be modified with
const
orstatic
:
-
A final data field is not writeable:
A final field may, however, be set in the constructor. See [Req-IDE-68] for details. -
A final data field must be either initialized by an initializer expression or in the constructor. If the field is initialized in the constructor, this may be done either explicitly or via a spec style constructor.
5.2.6.4. Field Accessors (Getter/Setter)
Instead of a simple data field, a field can be defined by means of the getter and setter accessor methods. These accessor methods are similar to the accuser methods in object literals:
5.2.6.4.1. Syntax
N4GetterDeclaration <Yield>:
=> ({N4GetterDeclaration}
(declaredModifiers+=N4Modifier)*
GetterHeader<Yield>)
(body=Block<Yield>)? ';'?
;
fragment GetterHeader <Yield>*:
BogusTypeRefFragment? 'get' -> declaredName=LiteralOrComputedPropertyName<Yield>
(declaredOptional?='?')?
'(' ')'
ColonSepTypeRef?
;
N4SetterDeclaration <Yield>:
=>({N4SetterDeclaration}
(declaredModifiers+=N4Modifier)*
'set'
->declaredName=LiteralOrComputedPropertyName <Yield>
)
(declaredOptional?='?')?
'(' fpar=FormalParameter<Yield> ')' (body=Block<Yield>)? ';'?
;
Notes with regard to syntax: Although ECMAScript 6 does not define fields in classes, it defines getter and setter methods similarly (cf. [ECMA15a(p.S13.3, p.p.209)]).
The getter and setter implementations usually reference data fields internally. These are to be declared explicitly (although ECMAScript allows creating fields on the fly on their first usage). The following example demonstrates a typical usage of getter and setter in combination with a data field. The getter lazily initializes the field on demand. The setter performs some notification.
class A {}
class C {
private _data: A = null;
public get data(): A {
if (this._data==null) {
this._data = new A();
}
return this._data;
}
public set data(data: A) {
this._data = data;
this.notifyListeners();
}
notifyListeners(): void {
// ...
}
}
5.2.6.4.2. Properties
Properties for field accessors:
declaredOptional
-
Tells whether the accessor was declared optional.
readable
-
Derived value: true for getters and false for setters.
writable
-
Derived value: false for getters and true for setters.
5.2.6.4.3. Semantics
There must be no field or method with the same name as a field accessor (follows from [Req-IDE-52]). In addition, the following constraints must hold:
Req. IDE-64: Field Accessors (ver. 1)
-
The return type of a getter must not be
void
. -
The type of the parameter of a setter must not be
void
. -
If a getter is defined or consumed (from an interface) or merged-in (via static polyfill) in a class
C
and a setterS
with is inherited byC
from one of its super classes, thenC
must define a setter with [34]. -
A setter must have exactly one formal parameter, i.e. variadic or default modifiers are not allowed.
The same applies to setters, accordingly.
-
[Req-IDE-72], [Req-IDE-73], and [Req-IDE-74] apply to field accessors accordingly (getter / setter overriding).
A getter and setter with the same name need not have the same type, i.e. the getter’s return type need not be the same as a subtype of the type of the setter’s parameter (the types can be completely unrelated).[35] |
Getters and setters – like functions – define a variable execution environment and therefore provide access to the actual passed-in parameters through the implicit arguments
variable inside of their bodies (c.f. Arguments Object).
5.2.6.5. Optional Fields
Data fields and field accessors of a classifier C can be declared optional, meaning that a structural subtype of C need not provide this field, but if it does, the field must be of correct type. However, to ensure overall type safety, the scope of application of this optionality is limited to a small number of specific use cases, as described in the following.
5.2.6.5.1. Syntax
To denote a data field or accessor as optional, a question mark is placed right after the name:
class C {
public field?: string;
public get getter?(): number {
return 42;
}
public set setter?(value: number) {}
}
5.2.6.5.2. Semantics
It is important to note that the optionality of a field is, by default and in most cases, ignored and has an effect only in certain special cases.
The effect of a field being optional is defined by the following requirement.
Req. IDE-240500: Optional Fields (ver. 1)
By default, a data field, getter, or setter that is declared optional is handled in the exact same way as if no optionality were involved (i.e. by default, optionality is ignored).
Optionality has an effect only in case of structural subtype checks in which the left-hand side is one of the following:
-
an object literal.
-
a new expression.
-
an instance of a final class, i.e. the type of the value on left-hand side must be nominal and refer to a final class.
-
a reference to a const variable if its initializer expression is one of the following:
-
an object literal.
-
a new expression.
-
an instance of a final class (as explained above).
-
an ternary expression
-
and then
-
in cases 1 and 4a, both fields and accessors (getters and setters) are optional. That means, an optional data field, getter, or setter of needs not be present in .
-
in cases 2, 3, 4b, and 4c, only getters are optional, setters are not optional. That means, an optional getter of needs not be present in and an optional field of requires only a setter in . Note that these cases are more restricted than the cases 1 and 4a.
Moreover, optionality has an effect in case of ternary expression in which the left-hand side is a ternary expression, e.g. l = b? trueExpr : falseExpr
whose trueExpr
or falseExpr
possibly recursively contains an expression of the kind mentioned above. In this case, the optionality effect is the more restricted optinality of trueExpr
and falseExpr
.
If, according to these rules, a data field / getter / setter of need not be present in but a member with the same name and access is actually present in , that member in must be a data field / getter / setter of the same type / a subtype / a super type, respectively. In other words, if a not actually required member is present in the subtype, ordinary rules for member compatibility apply as if no optionality were involved (cf. general subtyping rules for structural types).
In other words, in object literals (cases 1 and 4a) neither optional getters, optional setters, nor optional data fields are required. However, in case of new expressions and instances of final classes (cases 2, 3, 4b, 4c) only optional getters are not required in a subtype; optional setters are required as normal (i.e. optionality ignored) and optional data fields require at least a setter.
The following table summarizes the most common cases and shows how this relates to the different forms of structural typing.
Δ |
Case |
Comment |
|||||
|
|
|
|
|
|||
may have setter |
never has setter |
||||||
|
✓ |
✓ |
✓ |
✓ |
✓ |
1 |
nothing mandatory |
|
✓ |
✓ |
2 |
setters mandatory |
|||
|
✓ |
✓ |
2 |
setters mandatory |
|||
|
✓ |
✓ |
✓ |
✓ |
✓ |
2 |
setters mandatory |
|
none |
D0 not final |
|||||
|
none |
fooSF0() not nominal |
|||||
|
✓ |
✓ |
3 |
setters mandatory |
In the table, a "✓" means that the particular example is valid; in all other cases an error would be shown in N4JS source code. Here are the classes and functions used in the above table:
class C {
public field?: string;
}
class D0 {}
class DG {
public get field(): string { return "hello"; }
}
class DS {
public set field(value: string) {}
}
@Final class F0 {}
function fooD0(): D0 { return new D0(); }
function fooSF0(): ~F0 { return new F0(); }
function fooF0(): F0 { return new F0(); }
It follows from the above definitions in Requirements [Req-IDE-240500] that cases 4a and 4b are not
transitive across a chain of several const
variables, whereas case 4c is transitive. For example:
class C {
public get getter?(): string {return null;}
}
class D {}
@Final class F {}
let c: ~C;
// no transitivity via several const variables in use case "object literal":
const ol1 = {};
const ol2 = ol1;
// XPECT errors --> "~Object is not a structural subtype of ~C: missing getter getter." at "ol2"
c = ol2;
// no transitivity via several const variables in use case "new expression":
const new1 = new D();
const new2 = new1;
// XPECT errors --> "D is not a structural subtype of ~C: missing getter getter." at "new2"
c = new2;
// BUT: we do have transitivity via several const variables in use case "final nominal type":
const finalNominal1 = new F();
const finalNominal2 = finalNominal1;
// XPECT noerrors -->
c = finalNominal1;
// XPECT noerrors --> "transitivity applies in this case"
c = finalNominal2;
The following example demonstrates how optionality behaves in ternay expressions.
interface ~I {
public m?: int;
}
class ~C { }
@Final class F { }
let b: boolean;
const cc: C = {}
let f1 = new F();
let f2: ~F = {};
// True expression is a const object literal, so both fields and accessors in I are optional.
// False expression is a new expression, so only getters in I are optionals.
// As a result, only getters in I are optional.
// XPECT errors --> "C is not a structural subtype of I: missing field m." at "b? cc : new C()"
var te1: I = b? cc : new C()
// No errors because both true and false expressions are object literal constants and hence
// Both fields and accessors in I are optional.
// XPECT noerrors
var te2: I = b? cc : {}
5.2.6.5.3. Background
The following example illustrates why optionality of fields has to be restricted to the few special cases defined above (i.e. object literals, new expressions, etc.).
class C {
public field?: string = "hello";
}
class D {}
class DD extends D {
public field: number = 42;
}
let c: ~C;
let d: D;
d = new DD();
c = d; // without the restrictive semantics of optional fields, this assignment would be allowed (but shows compile-time error in N4JS)
console.log(c.field); // prints 42 even though the type is string
c.field.charAt(0); // exception at runtime: c.field.charAt is not a function
In the last line of the above example, c.field
is actually 42
but the type systems claims it is of type string
and
thus allows accessing member charAt
of type string
which is undefined at runtime the actual value 42
.
The next example shows why cases 2 and 3 (i.e. new expressions and instances of final classes) have to be handled in a more restrictive manner than case 1 (i.e. object literals).
class C {
public field?: string;
}
class D {}
let c: ~C;
c = new D(); // error: new expression but D is missing setter
c.field = "hello";
In the previous code, if c = new D()
were allowed, we would add a new property field
to the instance of class
D
in the last line, which N4JS aims to avoid in general, unless unsafe language features such as dynamic types
are being employed.
5.2.7. Static Members
Static data fields, field accessors and methods are quite similar to instance members, however they are not members of instances of the type but the type itself.
They are defined similarly to instance members except that they are specified with the modifier static
.
Since they are members of the type, the this
keyword is not bound to instances of the class, but again to the type itself.
This is similar as in ECMAScript 6 ([ECMA15a(p.14.5.15)]).
Since static members are not instance but type members, it is even possible that a static member has the same name as an instance member.
Note that static members are not only allowed in classes but also in interfaces, but there are important differences (for example, no inheritance of static members of interfaces, cf. Section Static Members of Interfaces).
Req. IDE-65: Static member not abstract (ver. 1)
For a static field accessor or method S
, the following constraint must hold:
Like instance methods, static methods of classes are inherited by subclasses and it is possible to override static methods in subclasses. The very same override constraints are valid in this case as well.
5.2.7.1. Access From and To Static Members
Req. IDE-66: Accessing Static Members (ver. 1)
Let M
be a static member of class C
. Except for write-access to
fields, which will be explained later, you can access M
via:
-
The class declaration instance, i.e. the classifier or constructor type,
constructor{C}
, i.e.C.m
-
The class declaration instance of a subtype, i.e. the classifier or constructor type, i.e.
D.m
, ifD
is a subclass ofC
. -
v.m
, ifv
is a variable of typeC
(i.e. classifier type as defined in Constructor and Classifier Type) or a subtype thereof. -
this.m
inside the body of any static method declared inC
or any sub-class ofC
. -
Via a type variable
T
which upper bound is a subclassofC
e.g.,function <T extends C> f(){T.m}
Req. IDE-67: Static Member Access (ver. 1)
It is not possible to access instance members from static members. This is true in particular for type variables defined by a generic classifier.
Req. IDE-68: Write-access to static data fields and static setter (ver. 1)
For static data fields and static setter f
the following constraint must hold:
-
For every assign expression with .
-
For every writing unary expression with .
In the special case of m
being a static data field, write-access is only possible via the defining type name C.m
.
In the list above, only the first line can be used when assigning values to a field. Note that this only applies to fields and set-accessors.[36]
It is even possible to call a static field accessor or method of a class using dynamic polymorphism, as demonstrated in the following example:
class A {
static m(): void { console.log('A#m'); }
static foo(): void { console.log('A#foo'); }
static bar(): void {
this.foo();
}
}
class B extends A {
@Override
static foo(): void { console.log('B#foo'); }
}
A.m(); // will print "A#m"
B.m(); // will print "A#m" (m is inherited by B)
var t: type{A} = A;
t.foo(); // will print "A#foo"
t = B;
t.foo(); // will print "B#foo"
// using 'this':
A.bar(); // will print "A#foo"
B.bar(); // will print "B#foo"
This is quite different from Java where static methods are not inherited and references to static methods are statically bound at compile time depending on the declared type of the receiver (and not its value):
// !!! JAVA CODE !!!
public class C {
static void m() { System.out.println("C#m"); }
public static void main(String[] args) {
final C c = null;
c.m(); // will print "C#m" (no NullPointerException at runtime)
}
}
5.2.7.2. Generic static methods
It is not possible to refer to type variables of a generic class, as these type variables are never bound to any concrete types. A static method can, however, be declared generic. Generic static methods are defined similarly to generic instance methods. Since they cannot refer to type variables of a generic class, the constraint to avoid type variables with equal names (see [Req-IDE-55]) does not need to hold for generic static methods.
5.2.7.3. Static Members of Interfaces
Data fields, field accessors and methods of interfaces may be declared static. A few restrictions apply:
Req. IDE-69: Static Members of Interfaces (ver. 1)
-
Static members of interfaces may only be accessed directly via the containing interface’s type name (this means, of the four ways of accessing static members of classes defined in [Req-IDE-66] above, only the first one applies to static members of interfaces).
-
The
this
literal may not be used in static methods or field accessors of interfaces and it may not be used in the initializer expression of static fields of interfaces. See [Req-IDE-173]. -
The
super
literal may not be used in static methods or field accessors of interfaces (in fact, it may not be used in interfaces at all, cf. [Req-IDE-123]).
Note that the this
type as a return type for methods is only allowed for instance methods and as an argument type only in constructors (structurally typed).
There is no need to disallow these cases for static interface methods in the constraints above.
In general, static members may not be abstract, cf. [Req-IDE-46], which applies here as well. Static methods and field accessors of interfaces, therefore, always have to provide a body.
Static members of interfaces are much more restricted than those of classes. Compare the following example to Static Polymorphism for classes above:
interface I {
static m(): void { console.log('I#m'); }
}
interface J extends I {}
I.m(); // prints "I#m"
J.m(); // ERROR! (m is not inherited by J)
var ti: type{I} = I;
ti.m(); // ERROR! (access to m only allowed directly via type name I)
ti = J;
ti.m(); // ERROR! (access to m only allowed directly via type name I)
The last line in is the reason why access to static members has to be restricted to direct access via the type name of the containing interfaces.
5.2.8. Redefinition of Members
Members defined in classes or interfaces can be redefined by means of being overridden or implemented in subclasses, sub-interfaces, or implementing classes. Fields and methods with default implementation defined in interfaces can be consumed by the implementor, but certain restrictions apply.
Req. IDE-70: Override Compatible (ver. 1)
A member M
is override compatible to a member S
if and only if the
following constraints hold:
-
The name and static modifiers are equal:
-
The metatypes are compatible:
-
The overridden member must not be declared final:
-
Overridden member declared const can only be overridden (redefined) by const members:
-
It is not possible to override a non-final / non-const field or a setter with a final / const field:
-
It is not possible to override a non-abstract member with an abstract one:
-
The types are compatible:
4 -
The access modifier is compatible:
We define a relation accordingly.
Members overriding or implementing other members must be declared as override. If a member does not override another, however, it must not be declared as override.
Req. IDE-71: Non-Override Declaration (ver. 1)
If and only if a member M
of a class C
(extending a class S
and interfaces ) does not override or implement another member, then it must not be declared as override.
That is the following constraint must hold:
5.2.8.1. Overriding of Members
In general, the N4JS platform supports overriding members by redefining them in sub-classes. This definition allows for overriding of static methods, but it does not apply to constructors because .
Req. IDE-72: Overriding Members (ver. 1)
Given a class C
and a superclass .
If for an instance or static member M
defined in C
a member S
exists with null
then we call M
the overriding member and S
the overridden member.
In that case the following constraints must hold:
-
S
must be accessible fromC
-
M
must be override compatible withS
:
-
If
S
is a field andM
is an accessor, then an additional accessor must exists so that are an accessor pair forS
:
-
M
must be declared as override:
M.override
Remarks:
-
An overridden method, getter, or setter may called via
super
. Note that this is not possible for fields. -
There is no ’hiding’ of fields as in Java, instead there is field overriding.
-
It is not possible to override a field with a consumed getter and an overridden setter, because the getter is not consumed if there exists a field in a superclass. In this case, the consuming and extending class needs to define the accessor pair explicitly. The same is true for other combination of accessors and fields.
-
Overriding a field usually makes only sense if the visibility of the field is to be increased.
5.2.8.2. Implementation of Members
Definition: Interface and Class Member Sets
For the following constraints, we define two helper sets and as follows:
Given a C
, and interface , implemented by C
, with
Note that these sets already contain only non-private data fields.
5.2.8.2.1. Member Consumption
Definition: Member Consumption and Implementation
A member M
defined in an interface I
is consumed by an implementor C
, if it becomes a member of the class, that is, .
A member M
is consumed if there is no member defined in the implementor with the same name and if there is no non-private,
non-abstract member with that name inherited by the implementor from its superclass. [37]
If the implementor defines the member itself, then the member is implemented rather than consumed.
The concrete rules are described in the following;
It is not always possible to directly consume a member. In general, a rather conservative strategy is used: if two implemented interfaces define the same (non-abstract) member then the implementor must redefine the member in order to solve conflicts. Even if the two conflicting members have the same types, the implementor must redefine them as we generally assume semantic differences which the consumer has to be aware of. Data fields defined in interfaces, in particular, are assumed to be concrete. It is not, therefore, possible to consume a field defined in two implemented interfaces.
Req. IDE-73: Consumption of Interface Members (ver. 1)
Given a classifier C
[38], and interfaces implemented (or extended) by C
, and sets and as defined in [interface_and_class_member_sets].
A non-static member M
defined in any interface is merged into the consumer (C
), if for all other (possible) members of C
the following constraints hold:
-
The other member’s meta type matches the meta type of the merge candiate:
-
The other member is abstract and not owned by the consumer:
-
The merge candidate’s access modifier is not less than the modifier of the other member:
-
The merge candidate’s type compatible with the other member:
5.2.8.2.2. Member Implementation
Req. IDE-74: Implementation of Interface Members (ver. 1)
For any non-static abstract member M
defined in an interface I implemented (or extended) by a classifier `C
, M
must be accessible
from C
and one or two member(s) in C
must exist which are implementation-compatible with M
.
The implementing member(s) must be declared as override if they are directly defined in the consumer.
-
M
must be accessible fromC
. -
An implementation-compatible member must exist in
C
:-
if
M
is not a field:
-
if
M
is a field, then either an implementation-compatible field or accessor pair must exist:
-
Methods defined in interfaces are automatically declared abstract if they do not provide a default implementation.
This can also be expressed explicitly via adding the abstract
modifier.
If a class implementing an abstract interface does not implement a method declared in the interface, the class needs to be declared abstract (cf. Abstract Classes).
Consequences for method implementation:
-
It may be require the implementor to explicitly define a method in order to solve type conflicts produced by methods of different interfaces with same name but different signatures.
-
Methods in an implementor cannot decrease the accessibility of methods from implemented interfaces, that is
-
Methods in the implementor must be a supertype [39] of methods from implemented interfaces. That is to say the implemented methods are override-compatible.
-
There may be several methods defined in different implemented interfaces and a single owned method in . In this case, the above constraints must hold for all methods. In particular, ’s signature must conform to all conflicting methods’ signatures. This is possible by using union types for the arguments and an intersection type as return type. Such a method is said to resolve the conflict between the implemented (and also inherited) methods.
-
Since abstracts methods may become part of the implementor methods, the implementor must either define these methods or it must be declared abstract itself. Since interfaces are abstract by default, responsibility for implementing abstract methods is passed on to any implementor of interfaces.
-
If two implemented interfaces provide (non-abstract) members with the same name, they are not automatically consumed by the implementor even if the types would be similar. In these cases, the implementor has to redefine the members in order to be aware of possible semantic differences.
There is currently no separate annotation to indicate that methods are implemented or overridden in order to solve conflicts.
We always use the @Override
annotation.
Consumption of methods shows simple examples of above rules.
Assuming that class C
extends super class S
and implements interface I1
and I2
:
class C extends S implements I1, I2 {...}
The columns describe different scenarios in which a method (with same name) is defined in different classifiers.
We assume that the defined methods are always non-abstract (i.e. have default implementations), non-private and have the same signature.
The last row shows which method will be actually used in class C
.
If the method is defined in class C
, and if this method is printed bold, then this means that the method is required to be defined in C
in order to solve conflicts.
Interface |
MI1 |
MI1 |
MI1 |
MI1 |
MI1 |
MI1 |
---|---|---|---|---|---|---|
Interface |
MI2 |
MI2 |
MI2 |
|||
class |
MS |
MS |
MS |
|||
class |
MC |
MC |
MC |
|||
MI1 |
MC |
MC |
MS |
MS |
MC |
- Consuming Field Initializers
-
Aside from the fields themselves, an implementor always consumes the field initialization if the field is consumed – this is how the consumption is noticed at runtime.
/* XPECT output ~~~
<==
stdout:
s: C , t: D ,u: I1 ,v: I2
stderr:
==>
~~~ */
interface I0 {
v: string = "I0";
}
interface I1 {
s: string = "I1";
t: string = "I1";
u: string = "I1";
}
interface I2 extends I1, I0 {
@Override
t: string = "I2";
@Override
v: string = "I2";
}
class C {
s: string = "C";
}
class D extends C implements I1, I2 {
@Override
t: string = "D";
}
var d = new D();
console.log(
"s:", d.s, ", t:", d.t, ",u:", d.u, ",v:", d.v
)
We expect the following output (for each field):
-
d.s = "C"
:s
: is inherited fromC
, so it is not consumed fromI1
(orI2
). Consequently, the initializer ofs
inC
is used. -
d.t = "D"
:t
is defined inD
, solving a conflict stemming from the definition oft
inI1
andI2
. Thus, the initializer oft
inD
is used. -
d.u = "I1"
:u
is only defined inI1
, thus the initializer defined inI1
is used. -
d.v = "I2"
:v
is overridden inI2
, so is the field initializer. This is whyd.v
must be assigned toI2
and notI0
.
5.3. Structural Typing
In general, N4JS uses nominal typing. This is to say that a duck is a duck only if it is declared to be a duck. In particular when working with external APIs, it is more convenient to use structural or duck typing. That is, a thing that can swim and quacks like a duck, is a duck.
Interfaces or classes can be used for this purpose with a typing strategy modifier.
Given a type , the simple ~
(tilde) can be added to its declaration (on definition-site) or in a reference (on use-site) to indicate that the type system should use structural typing
rather than nominal typing.[40]
This means that some other type must provide the same members as type to be deemed a structural subtype.
However, the operator cannot be used anymore with the type or reference as this operator relies on the declaration information (or at least the closest thing available at runtime).
In this case, is, therefore, always a structural subtype of ~T.
Sometimes it is convenient to refer only to the fields of a classifier, for example when the initial field values are to be provided in a variable passed to the constructor.
In that case, the type can be modified with ~~
(two tildes). This is only possible on use-site, i.e. on type references.
Furthermore, only on the use-site, it is possible to consider only either readable or writable or fields by using the read-only ~r~
or write-only ~w~
structural field typing.
For initialization blocks, it is even possible to use structural initializer field typing via the ~i~
operator.
5.3.1. Syntax
Structural typing is specified using the typing strategy modifier. There are two modifiers defined; one for definition-site and one for use-site structural typing.
TypingStrategyUseSiteOperator returns TypingStrategy:
'~' ('~' | STRUCTMODSUFFIX)?;
TypingStrategyDefSiteOperator returns TypingStrategy:
'~';
terminal STRUCTMODSUFFIX:
('r' | 'i' | 'w') '~'
;
ParameterizedTypeRefStructural returns ParameterizedTypeRefStructural:
definedTypingStrategy=TypingStrategyUseSiteOperator
declaredType=[Type|TypeReferenceName]
(=> '<' typeArgs+=TypeArgument (',' typeArgs+=TypeArgument)* '>')?
(=> 'with' '{' astStructuralMembers+=TStructMember* '}')?
;
ThisTypeRefStructural returns ThisTypeRefStructural:
definedTypingStrategy=TypingStrategyUseSiteOperator
'this'
('with' '{' astStructuralMembers+=TStructMember* '}')?
;
5.3.2. Definition Site Structural Typing
An interface or class can be defined to be used with structural typing by adding the structural modifier to its definition (or, in case of external classes, to the declaration).
This changes the default type system strategy from nominal to structural typing for that type.
That means that all types with the same members as the specified type are subtypes of that type, except for subtypes of N4Object
.
In the latter case, programmers are enforced to nominal declare the type relation.
If a type is declared as structural at its definition, is true.
Req. IDE-75: Definition Site Structural Typing (ver. 1)
-
The structurally defined type cannot be used on the right hand side of the
instanceof
operator: -
A type is a subtype of a structurally defined type either
-
if it is not a subtype of
N4Object
[41] but it contains all public, non-static members of that type
or -
if it is a subtype of
N4Object
which explicitly extends or implements the structurally defined type. -
A structurally defined type is implicitly derived from
Object
if no other type is specified. In particular, a structurally defined type must not be inherited from
-
The following snippet demonstrates the effect of definition-site structural types by comparing them to nominal declared types:
interface ~Tilde { x; y; }
interface Nominal { x; y; }
class C { public x; public y;}
class D extends C implements Tilde { }
function f(p: Tilde) {}
function g(p: Nominal) {}
f(new C()); // error: nominal typing, C does not implement ~Tilde
f(new D()); // ok, D is a nominal subtype (as it implements Tilde)
f({x:10,y:10}); // ok: Tilde is used with structural typing for non-N4-classifiers
Definition site structural typing may lead to unexpected results. For example;
class C{}
class ~E extends C{}
It may be unexpected, but E
is not a subtype of C
, i.e.
! E.g., instanceof
won’t work with E
, while it works
with C
!
5.3.3. Use-Site Structural Typing
Use-site structural typing offers several typing strategy modifiers to define the accessability of public properties of classes and interfaces.
They can be used e.g. on variable declarations like this: var c : ~C
.
The table Available Fields of Structural Types shows which properties of structural types can be accessed in the different type strategies.
For example, when using the write-only structural strategy (i.e. ), only the writeable fields, can be accessed for writing.
In the table, the term field to both, public datafields and accessors of type .
Non-public properties are never accessable in use-site structural types.
In any field-structural type, methods of the referenced nominal type are not available.
The initializer structural typing provides readable fields for every writeable field of the references type.
Property of | |||||
---|---|---|---|---|---|
public method |
|||||
public writable field |
|||||
public readable field |
writable fields |
Multiple structural typing strategies can be nested when there are multiple use sites, like in the example Nested Structural Typing Strategies below at the locations ST1 and ST2.
In the example, the datafield a.field
has the nested structural type ~r~ ~i~ A
and thus the datafield a.field.df
is readable.
Nested structural types are evaluated on the fly when doing subtype checks.
class A {
public df : string;
}
interface I<T> {
public field : ~r~T; // ST1
}
var a : ~i~A; // ST2
The following example demonstrates the effect of the structural type modifiers:
Let’s assume the type defined on the left. The following pseudo code snippets explicitly list the type with its members virtually created by a structural modifier. Note that this is pseudo code, as there are no real or virtual types created. Instead, only the subtype relation is defined accordingly:
Effect of structural type modifiers on use-site
Effect of structural type modifiers on use-site |
||
---|---|---|
|
|
|
Type |
Structural Type |
Structural Field Type |
|
|
|
Structural Read-only Field Type |
Structural Write-only Field Type |
Structural Initializer Field Type |
---|
Note that even if a type is defined without the structural modifier, it is not possible to use instanceof
for variables declared as structural, as shown in the next example:
|
|
|
Type |
Structural Type |
Structural Field Type |
---|
Req. IDE-78701: Nominal and structural typing spec attributes (ver. 1)
Within this spec, we define the following attributes of a type reference :
-
If a type is referenced with the structural type modifier
~
, the property is true. -
If a type is referenced with the structural field type modifier
~~
, the property is true. -
If a type is referenced with the structural read-only field type modifier
~r~
, the property is true. -
If a type is referenced with the structural write-only field type modifier
~w~
, then the property is true. -
If a type is referenced with the structural initializer field type modifier
~i~
, then the property is true. -
We use to simply refer any structural typing, i.e.+ T.refStructuralReadOnlyField \lor T.refStructuralWriteOnlyField || T.refStructuralInitField || T.defStructural$
-
We use as the opposite of , i.e.
We call the following:
-
the (nominal) type T,
-
the structural version of ,
-
the structural field version of ,
-
the structural read-only field,
-
the structural write-only field and
-
the structural initializer field version of .
Req. IDE-76: Use-Site Structural Typing (ver. 1)
-
The structural version of a type is a supertype of the nominal type:
-
The structural field version of a type is a supertype of the structural type:
-
The structural read-only field version of a type is a supertype of the structural field type:
-
The structural write-only field version of a type is a supertype of the structural field type:
-
The structural (field) version of a type cannot be used on the right hand side of the
instanceof
operator:
That is, the following code will always issue an error:
x instanceof ~T
[42]. -
A type is a subtype of a structural version of a type , if it contains all public, non-static members of the type : [43]
-
A type is a subtype of a structural field version of a type , if it contains all public, non-static fields of the type . Special cases regarding optional fields are described in Optional Fields.
-
A type is a subtype of a structural read-only field version of a type , if it contains all public and non-static readable fields of the type . Special cases regarding optional fields are described in Optional Fields.
-
A type is a subtype of a structural write-only field version of a type , if it contains all public and non-static writable fields of the type . Special cases regarding optional fields are described in Optional Fields.
-
A type is a subtype of a structural field version of a type , if it contains all public, non-static fields, either defined via data fields or field get accessors, of the inferred type of
this
. Special cases regarding optional fields are described in Optional Fields.
-
A structural field type is a subtype of a structural type , if only contains fields (except methods inherited from
Object
) and if . -
Use-site structural typing cannot be used for declaring supertypes of classes or interfaces. That is to say that structural types cannot be used after
extends
,implements
orwith
in type declarations [44].
Note that all members of N4Object
are excluded.
This implies that extended reflective features (cf. Reflection meta-information ) are not available in the context of structural typing.
The instanceof
operator is still working as described in Relational Expression, in that it can be used to check the type of an instance.
If a type is a (nominal) subtype of T, it is, of course, also a subtype of :
This is only a shortcut for the type inference defined above.
Req. IDE-77: Definition and Use-Site Precedence (ver. 1)
If a type is structurally typed on both definition and use-site, the rules for use-site structural typing ([Req-IDE-76]) are applied.
The following example demonstrates the effect of the structural (field) modifier, used in this case for function parameters.
interface I { public x: number; public foo()};
class C { public x: number; public foo() {}};
function n(p: I) {}
function f(p: ~I) {}
function g(p: ~~I) {}
n(new C()); // error: nominal typing, C does not implement I
f(new C()); // ok: C is a (structural) subtype of ~I
f({x:10}); // error, the object literal does not provide function foo()
g({x:10}); // ok: object literal provides all fields of I
It is possible to use a variable typed with a structural version of a type on the left hand side of the instanceof
operator, as demonstrated in this example:
class C {
public x;
public betterX() { return this.x||1;}
}
function f(p: ~~C) {
if (p instanceof C) {
console.log((p as C).betterX);
} else {
console.log(p.x||1);
}
}
The following table describes the member availability of X
in various
typing scenarios. Such as ~~X
, ~r~X
, ~w~X
, ~i~X
.
Member of type X | ~~X |
~r~X |
~w~X |
~i~X |
---|---|---|---|---|
|
— |
— |
— |
— |
|
write |
— |
write |
read |
|
read |
read |
— |
|
|
read/write |
read |
write |
read |
|
read/write |
read |
write |
read ? |
|
read?/write |
read? |
write |
read |
|
read |
read |
||
|
read |
read |
read |
|
|
read/write |
read |
write |
read |
|
5.3.4. Structural Read-only, Write-only and Initializer Field Typing
Structural read-only, write-only and initializer field typings are extensions of structural field typing.
Everything that is defined for the field structural typing must comply with these extension field typings.
For the read-only type, readable fields (mutable and @Final
ones) and setters are considered, for the write-only type; only the setters and mutable fields are considered.
Due to the hybrid nature of the initializer type it can act two different ways.
To be more precise, a type (structural initializer field type) is a supertype of (structural initializer field type) if for each public, non-static, non-optional writable field (mutable data field of setter) of , there is a corresponding, public, non-static readable field of .
All public member fields with @Final
annotation are considered to be mandatory in the initializer field typing @Spec
constructors.
The already-initialized @Final
fields can be either omitted from, or can be re-initialized via, an initializer field typing @Spec
style constructor.
class A1 {
public s: string;
}
class A2 {
public set s(s: string) { }
public get s(): string { return null; }
}
class A3 {
@Final public s: string = null;
}
class A4 {
public get s(): string { return null; }
}
class A5 {
public set s(s: string) { }
}
A1 |
~A1 |
~~A1 |
~r~A1 |
~r~A2 |
~r~A3 |
~r~A4 |
~r~A5 |
~w~A1 |
~w~A2 |
~w~A3 |
~w~A4 |
~w~A5 |
~i~A1 |
~i~A2 |
~i~A3 |
~r~A4 |
~r~A5 |
|
A1 |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
~A1 |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
|
~~A1 |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
|
~r~A1 |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
||||||
~r~A2 |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
||||||
~r~A3 |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
||||||
~r~A4 |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
||||||
~r~A5 |
✓ |
✓ |
✓ |
✓ |
✓ |
|||||||||||||
~w~A1 |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
||||||||||
~w~A2 |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
||||||||||
~w~A3 |
✓ |
✓ |
✓ |
✓ |
✓ |
|||||||||||||
~w~A4 |
✓ |
✓ |
✓ |
✓ |
✓ |
|||||||||||||
~w~A5 |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
||||||||||
~i~A1 |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
||||||
~i~A2 |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
||||||
~i~A3 |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
||||||
~r~A4 |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
||||||
~r~A5 |
✓ |
✓ |
✓ |
✓ |
✓ |
5.3.5. Public Setter Annotated With ProvidesInitializer
Public setters with ProvidesInitializer
annotation can declare optional fields implemented by means of field accessors instead of data fields.
Data fields with an initializer are treated as optional in the initializer field type.
It is important to note that it is valid to use the ProvidesInitializer
annotation for setters in n4js
files and not only definition files.
@ProvidesInitializer
treated as optionalclass C {
private _y: int = 1;
public get y() { return this._y; }
@ProvidesInitializer
public set y(v: int) { this._y = v; }
public constructor(@Spec spec: ~i~this) { }
}
console.log(new C({}).y); // 1
console.log(new C({y: 42}).y); //24
5.3.6. Structural Types With Optional Fields
Public optional fields become a member of the structural (field) type as well. To ensure the overall type safety, the semantics of optionality (e.g. on or off) depends on the context, in which the optional fields are currently being used (See Optional Fields).
5.3.7. Structural Types With Access Modifier
The access modifier of the subtype have to provide equal or higher visibility.
class C {
public s: number;
}
class D {
project s: number;
}
function f(c: ~C) {}
f(new D()); // error: D is no (structural) subtype of ~C, as visibility of s in D is lower
function g(d: ~D) {}
g(new C()); // ok: C is a (structural) subtype of ~D, as visibility of s in C is greater-than-or-equal to s in D
5.3.8. Structural Types With Additional Members
It is possible to add additional members when structurally referencing a declared type.
5.3.8.1. Syntax
TStructMember:
TStructGetter | TStructGetterES4 | TStructSetter | TStructMethod | TStructMethodES4 | TStructField;
TStructMethod:
=>
({TStructMethod} ('<' typeVars+=TypeVariable (',' typeVars+=TypeVariable)* '>')?
returnTypeRef=TypeRef name=TypesIdentifier '(')
(fpars+=TAnonymousFormalParameter (',' fpars+=TAnonymousFormalParameter)*)? ')'
';'?;
TStructMethodES4 returns TStructMethod:
=>
({TStructMethod} ('<' typeVars+=TypeVariable (',' typeVars+=TypeVariable)* '>')?
name=TypesIdentifier '(')
(fpars+=TAnonymousFormalParameter (',' fpars+=TAnonymousFormalParameter)*)? ')'
(':' returnTypeRef=TypeRef)?
';'?;
TStructField:
(
typeRef=TypeRef name=TypesIdentifier
| name=TypesIdentifier (':' typeRef=TypeRef)?
)
';';
TStructGetter:
=> ({TStructGetter}
declaredTypeRef=TypeRef
'get'
name=TypesIdentifier)
'(' ')' ';'?;
TStructGetterES4 returns TStructGetter:
=> ({TStructGetter}
'get'
name=TypesIdentifier)
'(' ')' (':' declaredTypeRef=TypeRef)? ';'?;
TStructSetter:
=> ({TStructSetter}
'set'
name=TypesIdentifier)
'(' fpar=TAnonymousFormalParameter ')' ';'?;
TAnonymousFormalParameter:
typeRef=TypeRef variadic?='...'? name=TIdentifier?
| variadic?='...'? (=> name=TIdentifier ':') typeRef=TypeRef;
5.3.8.1.1. Semantics
Req. IDE-78: Additional structural members (ver. 1)
It is only possible to add additional members to a type if use-site structural typing is used.
The following constraints must hold:
-
For all additional members defined in a structural type reference, the constraints for member overriding ([Req-IDE-72]) apply as well.
-
All additional members have the access modifier set to
public
. -
Type variables must not be augmented with additional structural members.
Additional fields may be declared optional in the same way as fields in classes. The rules given in Structural Types With Optional Fields apply accordingly. Consider the following example:
class C {
public f1: number;
}
var c1: ~C with { f3: string; } c1;
var c2: ~C with { f3: string?; } c2;
c1 = { f1:42 }; // error: "~Object with { number f1; } is not a subtype of ~C with { string f3; }."
c2 = { f1:42 }; // ok!!
Augmenting a type variable T with additional structural members can cause collisions with another member of a type argument for T. Hence, type variables must not be augmented with additional structural members like in the following example.
interface I<T> {
public field : ~T with {prop : int} // error "No additional structural members allowed on type variables."
}
Using an additional structural member on a type variable T could be seen as a constraint to T. However, constraints like this should be rather stated using an explicit interface like in the example below.
interface ~J {
prop : int;
}
interface II<T extends J> {
}