Last Updated: 2019-08-08

Authors:
Jens von Pilgrim, Jakub Siberski, Mark-Oliver Reiser, Torsten Krämer, Ákos Kitta, Sebastian Zarnekow, Lorenzo Bettini, Jörg Reichert, Kristian Duske, Marcus Mews, Minh Quang Tran, Luca Beurer-Kellner

Abstract

This document contains the N4JS Specification.

1. Introduction

This specification defines the N4JS language.

In general, the N4JS JavaScript dialect used is identical to the standard ECMAScript as defined in the 6th edition of ECMA-262, also known as ECMAScript 2015, referred to as [ECMA15a].

1.1. Notation

1.1.1. Grammar Notation

For the specification of the syntax and structure of elements, we use a slightly augmented similar to the grammar language of Xtext Grammar Language.

Similar to [ECMA11a], we define types with properties only for the purpose of explanation and usage within this specification. We use the Xtext notation style to assign values to meta-properties. Particularly, we use the Xtext notation for collection (+=) and boolean (?=) values. These properties are written in italics. Enumerations are defined similar to Xtext. In order to allow the specification of default values, which are often defined by omitting the value, we always define the literal explicitly if it can be defined by the user.

The following lists informally defines the grammar:

Terminal

Terminals (or terminal strings) are enclosed in single quotes, e.g., terminal.

Enumerations

Rules which contain only terminals used as values for properties are marked with enum for enumeration.

Properties

Values of non-terminals, e.g., other rules, can be assigned to properties. The property name and the assignment are not part of the original syntax and only used for the meta description. E.g., name=Identifier.

Collection Properties

If a property is a collection, values are added to that list via +=. E.g.,property+=Value .

Boolean Properties

Boolean properties are set to false by default, if the value (usually a terminal) is found, the boolean value is set to true. Often, the name of the property is similar to the terminal. E.g., final?='final'?.

Properties of a non-terminal are sometimes listed again below the grammar. In that case, often pseudo properties are introduced which are derived from other properties and which are only used for simplification.

1.1.2. Type Judgments and Rules and Constraints Notation

1.1.2.1. Typing Rules and Judgments

Definition: Rule

We use the common notation for rules such as type inference rules [1], that is

premisesconclusionrule name

premises is the rule’s premises (e.g., the expression to be inferred), conclusion the result of the rule. rulename is an optional condition which may be omitted.

Both parts of the rule may contain multiple expressions, which are concatenated via 'and'.

For example, the following

P1P2P3C

can be read as

if P1, P2, and P3 are all true, then C is true as well.

The following judgments (with relation symbols) are used:

subtype <

-

type :

in which the left hand side is a declaration or expression, and the right hand side a type. We also use [[...]] as a function returning the (inferred) type of an expression.

expectedTypeIn :

a relation with three arguments: containerexpression:type means, that expression is expected to be a subtype of type inside container

The following statement, for example, defines transitivity of subtypes (in a simplified manner):

ΓB<:AΓC<:BΓC<:A

is the context containing (bound) type variables etc., can be read as entails. Thus, the rule can be read as follows:

if the type B is a subtype of type A in context Γ (i.e. with constraints on type variables specified in Γ), and if type C is a subtype of B, then C is also a subtype of A in context Γ.

In rules, we sometimes omit the environment if it is not needed. New information is sometimes added to the environment, in particular, substitutions (that is binding type variables to a type). The set of substitutions is written with θ (theta). If new substitutions are explicitly added to that set, we write θVT (V is substituted with type T). Often, these bindings are computed from a parameterized type reference which declares type arguments which are bound to the type variables of the generic declaration. In this case we simply write θp, in which p is the parameterized type declaration. As these new substitutions must become part of a (newly) created environment, we then usually write Γθp. These substitutions are usually omitted.

1.1.2.2. Types of an Element

A variable or other typed element may be associated with three types:

  1. Declared type: the type explicitly specified in the code, e.g., var s: string.

  2. Inferred type: the type inferred by the type inferencer, e.g., var s = "Hello" infers the type of s to string. I.e. Γs:string will be true, or [[s]]<:string. If an element is annotated with a type ,i.e. it has a declared type, the inferred type will always be the declared type.

  3. Actual type: the actual type of a variable during runtime. This type information is not available at compile time and ignored in this specification.

These types are not type declarations but type references, in fact, as they may be parameterized. For the sake of simplicity, we often omit the Ref suffix to shorten formulas. Consequently, we define the following properties and pseudo properties for typed elements such as variables:

declaredTypeRef

The explicitly declared type, this is usually a real property of the construct. Not all elements allow the specification of a declared type, such as expressions.

inferredTypeRef or [[...]]

This pseudo property is the inferred type computed by the type inferencer.

typeRef

A pseudo property for elements with a declaredType property. It is similar to the inferred type, i.e. e.type=[[e]]

1.2. Auxiliary Functions

This section describes some auxiliary functions required for definition of type inference rules later on.

1.2.1. Binding

Binding an identifier (variable reference) to a variable declaration (or variable definition) is not part of this specification as this is standard ECMAScript functionality. However, some valid ECMAScript bindings are permitted due to visibility constraints.

Definition: Binding Relation

We define a pseudo relation

bind:VariableReference×VariableDeclaration

which binds a reference, i.e. an identifier, to a declaration (e.g.,variable declaration).

Binding of variable references to declaration is defined by ECMAScript already. Type references only occur in type expressions, how these are handled is explained in Type Expressions.

We usually omit this binding mechanism in most rules and use the reference similarly to the declaration or definition it is bound to. If a variable reference r, for example, is bound to a variable declaration D, i.e. bindrD, we simply write r.type instead of bindrD,D.type to refer to the type expression (of the variable).[2]

A DeclaredType references the type declaration by its simple name that has been imported from a module specifier. We define the method bind for declared types as well:

We define a pseudo relation

bind:DeclaredType×Class|Interface|Enum

which binds a type reference, i.e. a simple name, to the type declaration.

1.2.2. Merging Types

In some cases we have to merge types, e.g., types of a union type or item types of an array. For that purpose, we define a method merge as follows.

Definition: Merge Function

We define a pseudo function

merge:Type×...×TypePType

The idea of this function is to remove duplicates. For example; if a union type contains two type expressions te1 and tek, and if τte1=τte2, then mergeτte1τte2 contains only one element. The order of the elements is lost, however.

1.2.2.1. Logic Formulas

In general, we use a pragmatic mixture of pseudo code, predicate logic, and OCL. Within constraints (also within the inference rules), the properties defined in the grammar are used.

In some rules, it is necessary to type the rule variables. Instead of explicitly checking the metatype (via μX=:MetaType), we precede the variable with the type, that is: MetaTypeX.

Instead of "type casting" elements, often properties are simply accessed. If an element does not define that element, it is either assumed to be false or null by default.

If a property p is optional and not set, we write p=null to test its absence. Note that p=null is different from p=Null, as the latter refers to the null type. Non-terminals may implicitly be subclasses. In that case, the concrete non-terminal, or type, of a property may be subject for a test in a constraint.

1.2.3. Symbols and Font Convention

Variables and their properties are printed in italic when used in formulas (such as rules). A dot-notation is used for member access, e.g. v.name. Also defined functions are printed in italic, e.g., accrD. Properties which define sets are usually ordered and we assume 0-indexed access to elements, the index subscripted, e.g., v.methodsi.

We use the following symbols and font conventions:

, , , ¬

Logical and, or, exclusive or (xor), and not.

, , if..., then..., else...

Logical implication, if and only if, and if-then-else.

true, false, null,

Boolean true, boolean false, null (i.e., not specified, e.g., v.sup= means that there are is no sup (super class) specified), empty set.

, , , , |x|

Element of, not an element of, union set, intersection set, cardinality of set x.

PX

Power set of X, i.e. PX=U:UX.

, ,

Exists, not exists, for all; we write x,...,z:Px...z and say

"there exists x,...,z such that predicate P is true".

Note that x:Pxx:¬Px.

μ...

(mu) read "metatype of"; metatype of a variable or property, e.g.,

if...μx=:Classthen...else...
x¯

Sequence of elements x1,...,xn. E.g., if we want to define a constraint that the owner of a members of a class C is the class, we simply write

C.members¯.owner=C

instead of

mC.members:m.owner=C

or even more complicated with index variables.

Sequences are 1-based, e.g., a sequence s with length |s|=n, has elements s1,...,sn.

2. Grammar

2.1. Lexical Conventions

As a super language on top of ECMAScript, the same lexical conventions are supported as described in [ECMA11a(p.S7)] within strict mode. Some further constraints are defined, however, restricting certain constructs. These constraints are described in the following.

2.1.1. Identifier Names and Identifiers

As a reminder, identifiers are defined as follows in the ECMAScript specification:

IdentifierName: IdentifierStart* IdentifierPart;
IdentifierStart : UnicodeLetter | '_';
                \ UnicodeEscapeSequence

N4JS supports a limited form of computed-names for member declarations:

N4JSPropertyComputedName:
    '[' (SymbolLiteralComputedName | StringLiteralComputedName) ']'
;

SymbolLiteralComputedName: N4JSIdentifier '.' N4JSIdentifier ;

StringLiteralComputedName: STRING ;

As can be seen, a computed-name must be either

  • a symbol reference, e.g., Symbol.iterator

  • a string literal, i.e., a compile time known constant. This notation is useful when interoperating with libraries that define members whose names contain special characters (e.g., a field name starting with commercial-at)

In N4JS, identifiers are further constrained in order to avoid ambiguities and to make code more readable. Some of these constraints will lead to errors, others only to warnings. They do not apply for identifiers declared in definitions file (n4jsd) in order to enable declaration of external entities.

Req. IDE-1: N4JS Identifier Restrictions (ver. 1)

  1. If the following constraints do not hold, errors are created.

    1. Leading $ (dollar sign) character is prohibited for any variable name such as fields, variables, types functions and methods.

    2. Leading _ (underscore) character is not allowed for identifying any functions or methods.

Req. IDE-2: N4JS identifier recommendations (ver. 1)

  1. If the following constraints do not hold, warnings are created.

  2. Variable names should, in general, be constructed form the 26 ASCII upper and lower case alphabetic letters (a..z, A..Z), from the 10 decimal digits (0..9) and from the _ (underscore). Although the usage of the international characters are allowed (according to the ECMAScript specification) it is discouraged because these characters may not be read or understood well in every circumstance [3].

    1. Type (and Type Variable) Identifiers

      TypeIdentifier: [_A-Z][_a-zA-Z0-9]*
      TypeVariableIdentifier: [_A-Z][_a-zA-Z0-9]*
    2. Package Identifiers

      PackageIdentifier: [_a-z][._a-zA-Z0-9]*; // i.e. the folder names, must not end with .
    3. Member Identifiers and Enum Literals

      InstanceFieldIdentifier: [_a-z][_a-zA-Z0-9]*
      StaticFieldIdentifier: [_A-Z][_A-Z0-9]*([_A-Z0-9]+)*
      EnumLiteral: [_A-Z][_A-Z0-9]*([_A-Z0-9]+)*
    4. Variable and Parameter Names

      VariableIdentifier: [_a-zA-Z0-9]*
      􏰀ParameterIdentifier: [_a-z][_a-zA-Z0-9]*
    5. Methods

      MethodIdentifier: [_a-z][_a-zA-Z0-9]*;
    6. Annotations

      AnnotationIdentifier: [_A-Z][_a-zA-Z0-9]*

The following rules describe how fully qualified names of elements are created. Note that these fully qualified names cannot be used in N4JS directly. Though they may be shown in error messages etc. to identify elements.

TypeIdentifier:         [A-Z][a-zA-Z0-9]*;
PackageIdentifier:      [a-z][a-zA-Z0-9]*;
FQNType:                (PackageIdentifier '.')+ TypeIdentifier;

2.1.4. Automatic Semicolon Insertion

ASI is supported by the parser, however warnings are issued.

2.1.5. JSDoc

JSDoc are comments similar to JavaDoc in Java for documenting types, functions and members. There is no semantic information expressed in JSDoc, that is, the behavior of a program must not change if all the JSDoc is removed. The JSDoc tags and overall syntax is a mixture of tags defined by the Google Closure Compiler, Java’s JavaDoc tool and N4-specific tags.

JSDoc comments are multiline comments, starting with /** (instead of simple multiline comments, starting with /*).

MultiLineComment: '/*' MultiLineCommentChars? '*/'  // from ECMAScript specification
JSDoc:            '/**' MultiLineCommentChars? '*/'

In general, JSDoc comments are placed directly before the annotated language element. In some cases, this is slightly different, such as for method parameters, for example, where it is then explicitly specified.

The content of JSDoc comments will be covered in more detail in upcoming chapters. For documentation purposes, multi- and single-line descriptions are used in several constructs.

MLVALUE:         ([^@]+[^\n]+)+;
SLVALUE:         ([^\n]+);
MLVALUE

short for multi-line value. This is usually only used for the general description of types or members.

SLVALUE

short for single-line value. This is a description which ends at the end of a line. It is usually used in combination with other tags, e.g., to further describe a parameter of a method.

3. Names

Visibility defines the scope in which a declaration is visible, that is in which context references can be bound to the declaration. Access control defines the extent to which types and members are accessible beyond their immediate context. Access control may, therefore, restrict the visibility of a declaration by limiting its scope.

Extensibility refers to whether a given type can be subtyped, or in the case of members, whether they can be overridden. Access control is a prerequisite for extensibility which is further explained in N4JS Specific Classifiers

3.1. Access Control

Types from one project may or may not be made accessible to another project. Likewise, members from a given type may or may not be made accessible to members existing outside that type. For example, if a developer writes an application which uses a library, which types within that library can the application see? Given a type that is set as visible, which members of that type can the application see?

Accessing a type or member actually means that a reference is bound to a declaration with the same identifier.

We distinguish the following contexts from which an element is accessed as follows:

  1. Module or type: access from elements in the same module or type.

  2. Subtype: access from a subtype.

  3. Project: access from the same project.

  4. Vendor: access from different project of the same vendor.

  5. World: access from anything else.

Accessibility is defined by modifiers on types and members, e.g public, protected, project, private, via the export statement, and by the @Internal annotation. Extensibility is defined by the @Final annotation respectively.

3.2. Accessibility of Types, Top-Level Variables and Function Declarations

We define types (classes, interfaces, enums) whereby each type has members (fields and methods, depending on the kind of type). When we define a type, we need to define whether it is visible only for the specifying module, project or whether that type should be accessible from outside of that project.

The same is true for variable declarations and function declarations defined as top-level elements of a module.

The following type access modifiers are supported by N4JS:

enum TypeAccessModifier:      project
                            | public;

If a type is not exported, its visibility is private. If a type has declared visibility public, it may additionally be marked as internal via the annotation @Internal. Thus, we have the following set of type access modifiers:

TAM = private project public@Internal public

That is, in N4JS, only the type access modifiers and are available. The redundant project modifier serves only documentation purpose and can be synthesized if the export modifier is preset.

All other modifiers used here are synthesized as shown in the next example:

Synthesized Type Access Modifiers in N4JS
class C0 {}                         // private
export class C1 {}                  // project
export project class C1 {}          // project
export @Internal public class C1 {} // public@Internal
export public class C2 {}           // public

var v0;                             // private
export var v1;                      // project
export project var v1;              // project
export @Internal public var v3;     // public@Internal
export public var v2;               // public


function f0() {}                    // private
export function f1() {}             // project
export project function f1() {}     // project
export @Internal public function f3() {} // public@Internal
export public function f2() {}      // public

The access control levels are defined as listed in Type Access Control.

Table 1. Type Access Control
Type Access

Modifier

Module

Project

Vendor

World

private

yes

no

no

no

project

yes

yes

no

no

public@Internal

yes

yes

yes

no

public

yes

yes

yes

yes

TAM is a totally ordered set:

private<project<public@Internal<public

Req. IDE-3: Type Access Modifiers (ver. 1)

The following constraints for type access modifiers for a given type T (which may be a classifier declaration, a function or a variable) must hold:

  • It is an error if T is not exported but defined as project, public or public@Internal.

  • It is an error if an annotation @Internal is present on a module private or project visible type.

  • The type modifier for all built-in ECMAScript types is public.

  • The default modifier for user declared exported declarations is project. That is, this modifier is assumed if no modifier is explicitly specified.

Definition: Type Accessibility T

The function αT:TypeReference×TypeDeclarationBoolean computes whether a given type, (top-level) variable or function reference can access the declaration that it references. αT is defined with Type Access Control.

Formally, we define αT for a given reference r and a module top level variable, function or type declaration D as follows [4]:

D.acc=publicαTrD
D.acc=public@Internalr.vendor=D.vendorαTrD
D.acc=projectr.project=D.projectαTrD
D.acc=privater.module=D.moduleαTrD

If the type of the arguments is clear from the context, we simply write αrD instead of αTrD.

Accessibility for types is only checked for types that manifest themselves in the concrete syntax of the N4JS file. Types that do not have to be written to concrete syntax may be used even if they are generally not accessible. This is illustrated by Implicit, allowed type references in N4JS:

Example 1. Implicit, allowed type references in N4JS
export public class D {
    public takeC(): C { .. }
    public acceptC(c: C): void { .. }
}
/* private */ class C {}
var d: D = new D()
d.acceptC( d.takeC() )

3.2.1. Accessibility of Members

Accessibility at the member level is only applicable when the type itself is accessible. If you cannot access the type, you cannot access any of its members. Note that inherited members (from an interface or class) become members of a class. For example, if B extends A, and if A is not accessible to some client C but B is, then the members of A are indirectly accessible to C in so far as they are accessed via B. This is true in particular for interfaces, as their properties are possibly merged into the consuming class (cf. Implementation of Members).

The following member access modifiers are supported by N4JS:

enum MemberAccessModifier:    private
                            | project
                            | protected
                            | public;

The modifiers protected and public may be annotated with @Internal. Thus, we can define the following set of member access modifiers:

MAM={private,protected@Internal,protected,project,public@Internal,public}

protected@Internal and public@Internal are synthesized tags and were introduced as shorthand notation for the @Internal annotation together with protected or public access modifiers. The project modifier is the default one and it can be omitted. As with the type access modifiers, not all member access modifiers are available in N4JS. Instead, they are synthesized from different construct as shown in the next example.

Example 2. Synthesized Member Access Modifiers in N4JS
export @Internal public class C {

    private f0;                 // private
    f1;                         // project
    project f2;                 // project
    @Internal protected f3;     // protected@Internal
    protected f4;               // protected
    @Internal public f5;        // public@Internal
    public f6;                  // public

    private m0() {}             // private
    m1() {}                     // project
    project m2() {}             // project
    @Internal protected m3() {} // protected@Internal
    protected m4() {}           // protected
    @Internal public m5() {}    // public@Internal
    public  m6() {}             // public
}

MAM does not define a totally ordered set. However, its subset

MAM\public@Internal

is a totally ordered set [5] :

private<project<protected@Internal<protected<public

Member Access Control shows which members are accessible from where.

Table 2. Member Access Control
Access Modifier Inside Module Inside Project Vendor Vendor Subtypes Other Projects Everywhere

private

yes

no

no

no

no

no

project

yes

yes

no

no

no

no

protected@Internal

yes

yes

yes

no

no

no

protected

yes

yes

yes

no

yes

no

public@Internal

yes

yes

yes

yes

no

no

public

yes

yes

yes

yes

yes

yes

We define the relation

=:TAM×MAM

as follows:

=::={privateprivate,projectproject,public@Internalpublic@Internal,publicpublic}

We further define the relation :TAM×MAM as follows:

mam'MAM:tam=mam'mam'mamtammam

Less, greater then etc. are defined accordingly.

Definition: Member Accessibility

The function

αm:MemberReference×MemberDeclarationBoolean

computes if a given reference can access the member declaration that it references.

Note that αm and bind are different functions. A reference can only bind to a declaration if it can access the declaration. However, bind requires more condition to work (correct metatypes, no shadowing etc).

Formally, we define αm for a given reference r and member declaration M as follows: [6] [7]

M.acc=publicαmrM
r.vendor=M.vendorM.acc=public@InternalαmrM
r.ownerr.receiver.super*M.acc=protectedαmrM
r.ownerr.receiver.super*r.vendor=M.vendorM.acc=protected@InternalαmrM
r.project=M.projectM.acc=projectαmrM
r.module=r.moduleM.acc=privateαmrM

If the type of the arguments is clear from the context, we simply write αrM instead of αmrM.

Although private members are accessible inside a module, it is not possible to redefine (override etc.) these members (see Redefinition of Members).

Req. IDE-4: Default Member Access Modifiers (ver. 1)

The following constraints for member access modifiers must hold:

  1. The default modifier for members of user-declared classes is project.

  2. The default modifier for members of interfaces is the same as the visibility of the interface itself, except for private interfaces. For private interfaces, the default modifier for members is project.

  3. The modifier for enum literals is always public.

  4. Private members of a classifier are visible and accessible within a module, i.e. you can access the private method of a class, for instance, when the use of the class as receiver is in the same module where the class has been defined. In case of inheritance, private members are visible if the host (e.g. the class) is in the same module as the provider (the extended class). This also means that abstract members of a class are allowed to be defined private as they may be overridden within a module.

Example 3. Type and Member Access Modifiers
export project interface I {
    project foo();
}

// This interface may be used publicly, but since the inherited method foo() is project visible only,
// it is not possible to implement that interface in other projects.
export public interface J extends I {
}

// Since the visibility of foo is set to public here, it is possible to implement this interface in other projects.
export public interface K extends I {
    @Override public foo();
}

// Since foo is private, it is not possible to subclass the class in other modules. Still, it
// is possible to use it in other projects.
// XPECT noerrors -->
export public abstract class C {
    private abstract foo();

    public static C instance() {
        // return some default instance
        ...
    }
}

As demonstrated in the following snippet, class C can be used but not subclassed in other modules:

import C from "C"

// XPECT errors --> "Cannot extend class C: cannot implement one or more non-accessible abstract members: method C.foo." at "C"
export public abstract class Sub extends C {
}

// XPECT noerrors -->
var c: C = C.instance();

Members of non-visible types are, in general, not visible for a client. Members may become visible, however, if they are accessed via a visible type which inherits these members. The following examples demonstrate two different scenarios:

Example 4. Declaring type vs receiver type

It is especially noteworthy that the declaring type of a member is generally not considered for the accessibility of that member but only the receiver type is relevant.

class Base {
    public m(b: Base): void {}
}
export public class ApiType extends Base {
}
import * as N from "Base";

var t = new N.ApiType();
// member can be accessed although type Base is not exported:
t.m(t);

The property access to the member m is valid because it fulfills the constraints for accessibility. The receiver of the property access is t of type ApiType. That type is exported and accessible. Therefore, the inherited member m is also considered valid since it is also defined public.

This rule allows for defining a common functionality in module or project visible types that becomes accessible via exported, visible subtypes.

Example 5. Member Access and Type Access Interplay

The following example demonstrates the behavior when non-visible types are used as return types. In this case, all the members of the non-visible types are not accessible, even if they have a public access modifier.

class A {
    foo(): void{}
}
export public class C {
    public getHidden(): A { return new A() };
}
import * as Nfrom "A"

class Client {
    f(): void {
        var c = new N.C();
        // XPECT noerrors --> Getting an instance the hidden type is possible
        var hidden = c.getHidden();
        // XPECT errors --> "The method foo is not visible." at "foo"
        hidden.foo();
    }
}

3.2.2. Valid Names

For identifier and property names, the same constraints as in ECMAScript [ECMA11a(p.S7.6)] [ECMA11a(p.S7.6.1.2)] [ECMA11a(p.S11.6)] are applied.

Identifier names in N4JS are defined similar to [ECMA11a(p.S11.6)], making it possible to even use reserved words (keywords etc.). For some element types, errors or warnings are issued in order to prevent problems when using these names.

Req. IDE-5: Forbidden Identifier Names in N4JS (ver. 1)

In N4JS mode, errors are generated in the following cases:

  1. A name of a type equals

    1. an access modifier

    2. set or get

    3. an ECMAScript keyword

    4. a boolean literal

    5. the name of a base type

  2. The name of a function or function expression equals (but not the method)

    1. an ECMAScript keyword

    2. a reserved future ECMAScript word

Req. IDE-6: Undesired Identifier Names in N4JS (ver. 1)

In N4JS mode, warnings are generated in the following cases:

  1. The name of a member (of a non external type)

    1. equals the name of a base type [8] but the type of the variable is different from that type

    2. is not static nor const but starts with an upper case letter

  2. The name of a non-external N4 types (class, interface, enum) starts with a lower case letter

  3. The name of a variable (incl. formal parameter or catch variable and fields)

    1. equals an N4JS keyword

    2. equals the name of a base type but the type of the variable is different from that type

    3. is not const but starts with an upper case letter

3.2.3. Qualified Names

In N4JS source code, types can only be referenced using their simple name. There is no such thing as a fully-qualified type name in N4JS or ECMAScript. Types are uniquely identified by their simple name, maybe together with an import and the module specifier given there. Clashes between simple names of imported type and locally declared types can be resolved by importing the type under an alias.

In some cases, however, we need to define references to types or even members. For example, if we want to reference certain members in JSDoc comments or for unambiguous error messages. For this reason, we formally define qualified names even if they cannot occur in source code.

Different forms of module and type specifiers. shows the different names of a given type C, defined in a module M.n4js, defined in a package p of a project MyProject.

Simple type names are used throughout N4JS code in order to refer to types. The different forms of module specifiers are only used in import declarations in the string following the from keyword.

Table 3. Different forms of module and type specifiers.
Name Example

Simple Type Name

C

(Plain) Module Specifier

p/M

Complete Module Specifier

MyProject/p/M

Complete Type Specifier

MyProject/p/M.C

3.2.4. Name Duplicates

There might be cases where two (or more) scopes created by different entities with the same (simple) name overlap. Those situations can be referred to as shadowing, hiding, or obscuring. While they are not the same, many of those cases are not allowed in N4JS. For simplicity we refer to them all as shadowing or duplication (see below). Rule of thumb is that N4JS allows everything that is allowed in JavaScript StrictMode.

3.2.4.1. Lexical Environment

N4JS handles scopes similar to ECMAScript, so that function scope is applied to variables declared with var (and parameters), and block scope for variables is declared with let or const. In general, ECMAScript defines Lexical Environments as a specification type used to define the association of Identifiers to specific variables and functions based upon the lexical nesting structure of ECMAScript code [ECMA11a(p.10.2)].

Elements that introduce lexical environments:

FunctionDefinition, VariableDeclaration, CatchBlock, WithStatement, ImportDeclaration

N4JS specific declarations:

N4ClassDeclaration, N4InterfaceDeclaration, N4EnumDeclaration, N4MethodDeclaration.

Additionally, a built-in lexical environment that defines global scope exists for every Script.

Since N4JS is extended (and a bit more strict) JS strict mode, Object environment records created by WithStatement are not taken into account when resolving duplicates. This applies to both N4JS mode and JS strict mode. In unrestricted JS the WithStatement is allowed but duplicates are not validated.

In case of names introduced by ImportDeclarations only NamedImportSpecifierss are taken into account (their import name or its alias if available). WildcardImportSpecifierss are not taken into account. Potential optimizations by compiler or user annotation are also not currently taken into account during analysis.
3.2.4.2. Duplicates and Shadowing

Two elements with the same name declared in the same lexical environment (cf. [ECMA11a(p.S10.2.2.1)] are called duplicates. An element defined in an environment shadows all elements with the same name in outer environments.

In class hierarchies, a member with the same name as a member defined in a supertype is said to override the latter. Overriding is discussed in Redefinition of Members.

For the following constraints, we make the following assumptions:

  • Names of function expressions or declarations are handles similar to locally declared elements in the function. Function declarations are additionally declaring a name in their outer scope.

  • The implicit formal parameter arguments is treated similar to declared formal parameters.

  • Formal parameters are defined in the lexical environment of a function, that is, they are defined in the same lexical environment as local var-variables or other declarations in that function.

  • The "global" environment contains objects globally defined by the execution environment.

Req. IDE-7: Forbidden Duplicates (ver. 1)

There must be no two elements defined in the same lexical environment with the same name, that is, there must be no duplicates.

Req. IDE-8: Forbidden Shadowing (ver. 1)

In general, shadowing is allowed in N4JS. But it is not allowed in the following cases:

  1. No element defined in the standard global scope must be shadowed.

  2. There must be no function shadowing another function.

  3. Elements defined in catch blocks must not shadow elements defined all parent non-catch-block environments.

Req. IDE-9: Forbidden Names (ver. 1)

  1. In the script environment, it is not allowed to use the name ’arguments’.[9]

    Forbidden Shadowing shows nested lexical environments with named elements declared inside (all named x here), the forbidden cases are marked with arrows (the numbers at the left side refer to the numbers in [Req-IDE-8].

shadowing
Figure 1. Forbidden Shadowing

Rationale:

  • We expect only few named nested functions. Since this is expected to be a rare case, no shadowing should occur there as this is maybe not expected by the programmer.

  • It is typical that nested environments define local variables. In particular helper variables (such as i: number i or s: string ) are expected to be used quite often. Since this is a typical case, we allow shadowing for local variables.

  • Function declarations may shadow type declarations. However, both entities are to be handled completely differently, so that an error will occur if the shadowing is ignored by the programmer anyway.

4. Types

4.1. Overview

N4JS is essentially ECMAScript with the inclusion of types. In the following sections we will describe how types are defined and used in N4JS.

Besides standard JavaScript types, the following metatypes are introduced:

  • Classifiers, that is class or interface (see Classifiers)

  • Enum

Classifiers, methods and functions may be declared generic.

Types are related to each other by the subtype relation.

Definition: Subtype Relation

We use subtype for the general subtype relation or type conformance.

In nominal typing, T<:S means that S is a (transitive) supertype of T. Generally in structural typing, this means that T conforms to S. <: is defined transitive reflexive by default.

We write < to refer to the transitive non-reflexive relation, that is T<ST<:STS

Whether nominal or structural typing is used depends on the declaration of the type or the reference. This is explained further in Structural Typing.

For convenience reasons, we sometimes revert the operator, that is T<:SS:>T. We write T:S if T is not type conforming to S. (cf. [Gosling12a(p.S4.10)])

Join and meet are defined as follows:

Definition: Join and Meet

A type J is called a join (or least common supertype, ) of a pair of types S and T, written ST=J, if

S<:J
T<:J
L:(S<:L)(T<:L)J<:L

Similarly, we say that a type M is a meet (or greatest common subtype, ) of S and T, written ST=M, if

M<:S
M<:T
L:(L<:S)(L<:T)L<:M

Note that this declarative definition needs to be specified in detail for special cases, such as union and intersection types. Usually, the union type of two types is also the join.

Predefined Types Hierarchy summarizes all predefined types, that is primitive and built-in ECMAScript and N4JS types. Specific rules for the subtype relation are defined in the following sections. This type hierarchy shows any and undefined as the top and bottom type (cf. [Pierce02a(p.15.4)]) We define these types here explicitly:

Definition: Top and Bottom Type

We call Top the top type, if for all types T the relation T<:Top is true. We call Bot the bottom type, if for all types T the relation Bot<:T is true. In N4JS, Top=any, the bottom type Bot=undefined.

null is almost similar to Bot, except that it is not a subtype of undefined.

cdPredefinedTypesHierarchy
Figure 2. Predefined Types Hierarchy

For every primitive type there is a corresponding built-in type as defined in [ECMA11a], e.g. string and String. There is no inheritance supported for primitive types and built-in types – these types are final.

Although the diagram shows inheritance between void and undefined, this relationship is only semantic: void is a refinement of undefined from a type system viewpoint. The same applies to the relation of Object as well as the subtypes shown for string and String.

Example 6. Type Examples, Class Hierarchy

In the following examples, we assume the following classes to be given:

// C <: B <: A
class A{}
class B extends A{}
class C extends B{}

// independent types X, Y, and Z
class X{} class Y{} class Z{}

// interfaces I, I1 <: I, I2 <: I, I3
interface I
interface I1 extends I {}
interface I2 extends I {}
interface I3 {}

// class implementing the interfaces
class H1 implements I1{}
class H12 implements I1,I2{}
class H23 implements I2,I3{}

// a generic class with getter (contra-variance) and setter (co-variance)
class G<T> {
    get(). T;
    set(x: T): void;
}

4.2. Type Expressions

In contrast to ECMAScript, N4JS defines static types. Aside from simple type references, type expressions may be used to specify the type of variables.

4.2.1. Syntax

The listing EBNF Type Expression Grammar summarizes the type expression grammar. Depending on the context, not all constructs are allowed. For example, the variadic modifier is only allowed for function parameters.

References to user-declared types are expressed via ParameterizedTypeRef. This is also true for non-generic types, as the type arguments are optional. See Parameterized Types for details on that reference.

For qualified names and type reference names, see Qualified Names

The type expressions are usually added to parameter, field, or variable declarations as a suffix, separated with colon (:). The same is true for function, method, getter or setter return types. Exceptions in the cases of object literals or destructuring are explained later on.

Example 7. Type Annotation Syntax

The following two listings show the very same code and type annotations are provided on the left hand side. For simplicity, string is always used as type expression.[10]

var x: string;
var s: string = "Hello";
function f(p: string): string {
    return p;
}
class C {
    f: string;
    s: string = "Hello";
    m(p: string): string {
        return p;
    }
    get x(): string {
        return this.f;
    }
    set x(v: string) {
        this.f = v;
    }
}
var x;
var s = "Hello";
function f(p) {
    return p;
}
class C {
    f;
    s = "Hello";
    m(p) {
        return p;
    }
    get x() {
        return this.f;
    }
    set x(v) {
        this.f = v;
    }
}

The code on the right hand side is almost all valid ECMAScript 2015, with the exception of field declarations in the class. These are moved into the constructor by the N4JS transpiler.

4.2.2. Properties

Besides the properties indirectly defined by the grammar, the following pseudo properties are used for type expressions:

Properties of TypeExpression:

var

If true, variable of that type is variadic. This is only allowed for parameters. Default value: false.

opt

If true, variable of that type is optional. This is only allowed for parameters and return types. This actually means that the type T actually is a union type of Undef|T. Default value: false.

optvar

optvar=varopt, reflect the facts that a variadic parameter is also optional (as its cardinality is 0...n).

entity

Pseudo property referencing the variable declaration (or expression) which owns the type expression.

4.2.3. Semantics

The ECMAScript types undefined and null are also supported. These types cannot be referenced directly, however. Note that void and undefined are almost similar. Actually, the inferred type of a types element with declared type of void will be undefined. The difference between void and undefined is that an element of type void can never have another type, while an element of type undefined may be assigned a value later on and thus become a different type. void is only used for function and method return types.

Note that not any type reference is allowed in any context. Variables or formal parameters must not be declared void or union types must not be declared dynamic, for example. These constraints are explained in the following section.

The types mentioned above are described in detail in the next sections. They are hierarchically defined and the following list displays all possible types. Note that all types are actually references to types. A type variable can only be used in some cases, e.g., the variable has to be visible in the given scope.

ECMAScript Types
Predefined Type

Predefined types, such as String, Number, or Object; and .

Array Type

Array Object Type.

Function Type

Described in Functions, Function Type.

Any Type

Any Type.

N4Types
Declared Type

(Unparameterized) Reference to defined class Classes or enum Enums.

Parameterized Type

Parameterized reference to defined generic class or interface; Parameterized Types.

This Type

This Type.

Constructor and Type Type

Class type, that is the meta class of a defined class or interface, Constructor and Classifier Type.

Union Types

Union of types, Union Type.

Type Variable

Type variable, Type Variables.

Type expressions are used to explicitly declare the type of a variable, parameter and return type of a function or method, fields (and object literal properties).

4.3. Type Inference

If no type is explicitly declared, it is inferred based on the given context, as in the expected type of expressions or function parameters, for example. The type inference rules are described in the remainder of this specification.

Definition: Default Type

In N4JS mode , if no type is explicitly specified and if no type information can be inferred, any is assumed as the default type.

In JS mode, the default type is any+.

Once the type of a variable is either declared or inferred, it is not supposed to be changed.

Given the following example.

Variable type is not changeable
var x: any;
x = 42;
x-5; // error: any is not a subtype of number.

Type of x is declared as any in line 1. Although a number is assigned to x in line 2, the type of x is not changed. Thus an error is issued in line 3 because the type of x is still any.

At the moment, N4JS does not support type guards or, more general, effect system (cf. [Nielson99a]).

4.4. Generic and Parameterized Types

Some notes on terminology:

Type Parameter vs. Type Argument

A type parameter is a declaration containing type variables. A type argument is a binding of a type parameter to a concrete type or to another type parameter. Binding to another type parameter can further restrict the bounds of the type parameter.

This is similar to function declarations (with formal parameters) and function calls (with arguments).

4.4.1. Generic Types

A class declaration or interface declaration with type parameters declares a generic type. A generic type declares a family of types. The type parameters have to be bound with type arguments when referencing a generic type.

4.4.2. Type Variables

A type variable is an identifier used as a type in the context of a generic class definition, generic interface definition or generic method definition. A type variable is declared in a type parameter as follows.

Syntax
TypeVariable:
	(declaredCovariant?='out' | declaredContravariant?='in')?
	name=IdentifierOrThis ('extends' declaredUpperBound=TypeRef)?
;
Example 8. Type Variable as Upper Bound

Note that type variables are also interpreted as types. Thus, the upper bound of a type variable may be a type variable as shown in the following snippet:

class G<T> {
    <X extends T> foo(x: X): void { }
}
Properties

A type parameter defines a type variable, which type may be constrained with an upper bound.

Properties of TypeVariable:

name

Type variable, as type variable contains only an identifier, we use type parameter instead of type variable (and vice versa) if the correct element is clear from the context.

declaredUpperBound

Upper bound of the concrete type being bound to this type variable, i.e. a super class.

Semantics

Req. IDE-10: Type Variable Semantics (ver. 1)

  1. Enum is not a valid metatype in declaredUpperBounds.

  2. Wildcards are not valid in declaredUpperBounds.

  3. Primitives are not valid in declaredUpperBounds.

  4. Type variables are valid in declaredUpperBounds.

A type variable can be used in any type expression contained in the generic class, generic interface, or generic function / method definition.

Example 9. F bounded quantification

Using a type variable in the upper bound reference may lead to recursive definition.

class Chain<C extends Chain<C, T>, T> {
    next() : C { return null; }
    m() : T { return null; }
}
Type Inference

In many cases, type variables are not directly used in subtype relations as they are substituted with the concrete types specified by some type arguments. In these cases, the ordinary subtype rules apply without change. However, there are other cases in which type variables cannot be substituted:

  1. Inside a generic declaration.

  2. If the generic type is used as raw type.

  3. If a generic function / method is called without type arguments and without the possibility to infer the type from the context.

In these cases, an unbound type variable may appear on one or both sides of a subtype relation and we require subtype rules that take type variables into account.

It is important to note that while type variables may have a declared upper bound, they cannot be simply replaced with that upper bound and treated like existential types. The following example illustrates this:

Example 10. Type variables vs. existential types
class A {}
class B extends A {}
class C extends B {}

class G<T> {}

class X<T extends A, S extends B> {

    m(): void {

        // plain type variables:
        var t: T;
        var s: S;

        t = s;  // ERROR: "S is not a subtype of T." at "s" (1)

        // existential types:
        var ga: G<? extends A>;
        var gb: G<? extends B>;

        ga = gb;  (2)
    }
}
1 Even though the upper bound of S is a subtype of T’s upper bound (since B<:A), we cannot infer that S is a subtype of T, because there are valid concrete bindings for which this would not be true: for example, if T were bound to C and S to B.
2 This differs from existential types (see ga and gb and line 21):
G<? extends B> <: G<? extends A> ).

We thus have to define subtype rules for type variables, taking the declared upper bound into account. If we have a subtype relation in which a type variable appears on one or both sides, we distinguish the following cases:

  1. If we have type variables on both sides: the result is true if and only if there is the identical type variable on both sides.

  2. If we have a type variable on the left side and no type variable on the right side:
    the result is true if and only if the type variable on the left has one or more declared upper bounds.
    intersectionleft.declaredUpperBounds<:right
    This is the case for

    TextendsB<:A

    in which T is an unbound type variable and A, B two classes with B<:A.

  3. In all other cases the result is false.
    This includes cases such as

    B<:TextendsA

    which is always false, even if B<:A or

    TextendsA<:SextendsB

    which is always false, even if A=B.

We thus obtain the following defintion:

For two types T,S of which at least one is a type variable, we define

  • if both T and S are type variables:

    T=ST<:S
  • if T is a type variable and S is not:

    T.declaredUpperBounds.size>0  tT.declaredUpperBounds:t<:ST<:S

4.4.3. Parameterized Types

References to generic types (cf. Classes) can be parameterized with type arguments. A type reference with type arguments is called parameterized type.

Syntax
ParameterizedTypeRef:
    ParameterizedTypeRefNominal | ParameterizedTypeRefStructural;

ParameterizedTypeRefNominal:
    declaredType=[Type|TypeReferenceName]
    (=> '<' typeArgs+=TypeArgument (',' typeArgs+=TypeArgument)* '>')?;

ParameterizedTypeRefStructural:
    definedTypingStrategy=TypingStrategyUseSiteOperator
    declaredType=[Type|TypeReferenceName]
    (=>'<' typeArgs+=TypeArgument (',' typeArgs+=TypeArgument)* '>')?
    ('with' TStructMemberList)?;

TypeArgument returns TypeArgument:
    Wildcard | TypeRef;

Wildcard returns Wildcard:
    '?'
    (
          'extends' declaredUpperBound=TypeRef
        | 'super' declaredLowerBound=TypeRef
    )?
;
Properties

Properties of parameterized type references (nominal or structural):

declaredType

Referenced type by type reference name (either the simple name or a qualified name, e.g. in case of namespace imports).

typeArgs

The type arguments, may be empty.

definedTypingStrategy

Typing strategy, by default nominal, see Structural Typing for details

structuralMembers

in case of structural typing, reference can add additional members to the structural type, see Structural Typing for details.

Pseudo Properties:

importSpec

The ImportSpecifier, may be null if this is a local type reference. Note that this may be a NamedImportSpecifier. See Import Statement for details for details.

moduleWideName

Returns simple name of type, that is either the simple name as declared, or the alias in case of an imported type with alias in the import statement.

Semantics

The main purpose of a parameterized type reference is to simply refer to the declared type. If the declared type is a generic type, the parameterized type references defines a substitution of the type parameters of a generic type with actual type arguments. A type argument can either be a concrete type, a wildcard or a type variable declared in the surrounding generic declaration. The actual type arguments must conform to the type parameters so that code referencing the generic type parameters is still valid.

Req. IDE-11: Parameterized Types (ver. 1)

For a given parameterized type reference R with G=R.declaredType, the following constraints must hold:

  • The actual type arguments must conform to the type parameters, that is:

    |G.typePars|=|R.typeArgs|
     i,0<i<|R.typeArgs|:[[R.typeArgsi]]<:[[R.typeParsi]]

We define type erasure similar to Java [Gosling12a(p.S4.6)] as 'mapping from types (possibly including parameterized types and type variables) to types (that are never parameterized types or type variables)'. We write To for the erasure of type T.[11]

Definition: Parameterized Type

A parameterized type reference R defines a parameterized type T, in which all type parameters of R.declaredType are substituted with the actual values of the type arguments. We call the type T0, in which all type parameters of R.declaredType are ignored, the raw type or erasure of T.

We define for types in general:

  • The erasure Go of a parameterized type G<T1,...,Tn> is simply G.

  • The erasure of a type variable is the erasure of its upper bound.

  • The erasure of any other type is the type itself.

This concept of type erasure is purely defined for specification purposes. It is not to be confused with the real type erasure which takes place at runtime, in which almost no types (except primitive types) are available.

That is, the type reference in var G<string> gs; actually defines a type G<string>, so that [[gs]]=G<string>. It may reference a type defined by a class declaration class G<T>. It is important that the type G<string> is different from G<T>.

If a parameterized type reference R has no type arguments, then it is similar to the declared type. That is, [[R]]=T=R.declaredType if (and only if) |R.typeArgs|=0.

In the following, we do not distinguish between parameter type reference and parameter type – they are both two sides of the same coin.

Example 11. Raw Types

In Java, due to backward compatibility (generics were only introduced in Java 1.5), it is possible to use raw types in which we refer to a generic type without specifying any type arguments. This is not possible in N4JS, as there is no unique interpretation of the type in that case as shown in the following example. Given the following declarations:

class A{}
class B extends A{}
class G<T extends A> { t: T; }
var g: G;

In this case, variable g refers to the raw type G. This is forbidden in N4JS, because two interpretations are possible:

  1. g is of type G<? extends>

  2. g is of type G<A>

In the first case, an existential type would be created, and g.t = new A(); must fail.

In the second case, g = new G<B>(); must fail.

In Java, both assignments work with raw types, which is not really safe. To avoid problems due to different interpretations, usage of raw types is not allowed in N4JS. [12]

Calls to generic functions and methods can also be parameterized, this is described in Function Calls. Note that invocation of generic functions or methods does not need to be parameterized.

Definition: Type Conformance

We define type conformance for non-primitive type references as follows:

  • For two non-parameterized types T0 and S0,

    S0T0.sup*T0.interfaces*T0<:S0
  • For two parameterized types T<T1,...,Tn> and S<S1,...,Sm>

    T0<:S0T<:S
    (n=0m=0(n=mi:
    Ti.upperBound<:Si.upperBound
    Ti.lowerBound:>Si.lowerBound))}

Example 12. Subtyping with parameterized types

Let classes A, B, and C are defined as in the chapter beginning (C<:B<:A). The following subtype relations are evaluated as indicated:

G<A> <: G<B>                        -> false
G<B> <: G<A>                        -> false
G<A> <: G<A>                        -> true
G<A> <: G<?>                        -> true
G<? extends A> <: G<? extends A>    -> true
G<? super A> <: G<? super A>        -> true
G<? extends A> <: G<? extends B>    -> false
G<? extends B> <: G<? extends A>    -> true
G<? super A> <: G<? super B>        -> true
G<? super B> <: G<? super A>        -> false
G<? extends A> <: G<A>              -> false
G<A> <: G<? extends A>              -> true
G<? super A> <: G<A>                -> false
G<A> <: G<? super A>                -> true
G<? super A> <: G<? extends A>      -> false
G<? extends A> <: G<? super A>      -> false
G<?> <: G<? super A>                -> false
G<? super A> <: G<?>                -> true
G<?> <: G<? extends A>              -> false
G<? extends A> <: G<?>              -> true

The figure Cheat Sheet: Subtype Relation of Parameterized Types shows the subtype relations of parameterized types (of a single generic type), which can be used as a cheat sheet.

cdVarianceChart
Figure 3. Cheat Sheet: Subtype Relation of Parameterized Types
Example 13. Subtyping between different generic types

Let classes G and H be two generic classes where:

class G<T> {}
class H<T> extends G<T> {}

Given a simple, non-parameterized class A, the following subtype relations are evaluated as indicated:

G<A> <: G<A>                        -> true
H<A> <: G<A>                        -> true
G<A> <: H<A>                        -> false
Type Inference

Type inference for parameterized types uses the concept of existential types (in Java, a slightly modified version called capture conversion is implemented).

The general concept for checking type conformance and inferring types for generic and parameterized types is described in [Igarashi01a] for Featherweight Java with Generics.

The concept of existential types with wildcard capture (a special kind of existential type) is published in [Torgersen05a], further developed in [Cameron08b] (further developed in [Cameron09a] [Summers10a], also see [Wehr08a] for a similar approach). The key feature of the Java generic wildcard handling is called capture conversion, described in [Gosling12a(p.S5.1.10)]. However, there are some slight differences to Java 6 and 7, only with Java 8 similar results can be expected. All these papers include formal proofs of certain aspects, however even these paper lack proof of other aspect

The idea is quite simple: All unbound wildcards are replaced with freshly created new types [13], fulfilling the constraints defined by the wildcard’s upper and lower bound. These newly created types are then handled similar to real types during type inference and type conformance validation.

Example 14. Existential Type

The inferred type of a variable declared as

var x: G<? extends A>;,

that is the parameterized type, is an existential type E1, which is a subtype of A. If you have another variable declared as

var y: G<? extends A>;

another type E2 is created, which is also a subtype of A. Note that E1E2! Assuming typical setter or getter in G, e.g. set(T t) and T get(), the following code snippet will produce an error:

y.set(x.get())

This is no surprise, as x.get() actually returns a type E1, which is not a subtype of E2.

The upper and lower bound declarations are, of course, still available during type inference for these existential types. This enables the type inferencer to calculate the join and meet of parameterized types as well.

Req. IDE-12: Join of Parameterized Types (ver. 1)

The join of two parameterized types G<T1,...,Tn> and H<S1,...,Sm> is the join of the raw types, this join is then parameterized with the join of the upper bounds of of type arguments and the meet of the lower bounds of the type arguments.

For all type rules, we assume that the upper and lower bounds of a non-generic type, including type variables, simply equal the type itself, that is for a given type T, the following constraints hold:
upperT=lowerT=T

Example 15. Upper and lower bound of parameterized types

Assuming the given classes listed above, the following upper and lower bounds are expected:

G<A>            -> upperBound = lowerBound = A
G<? extends A>  -> lowerBound = null, upperBound = A
G<? super A>    -> lowerBound = A, upperBound = any
G<?>            -> lowerBound = null, upperBound = any

This leads to the following expected subtype relations:

(? extends A) <: A  -> true
(? super A) <: A    -> false
A <: (? extends A)  -> false
A <: (? super A)    -> true

Note that there is a slight difference to Java: In N4JS it is not possible to use a generic type in a raw fashion, that is to say without specifying any type arguments. In Java, this is possible due to backwards compatibility with early Java versions in which no generics were supported.

In case an upper bound of a type variable shall consist only of a few members, it seems convenient to use additional structural members, like on interface I2 in the example Use declared interfaces for lower bounds below. However, type variables must not be constrained using structural types (see constraint [Req-IDE-76]). Hence, the recommended solution is to use an explicitly declared interface that uses definition site structural typing for these constraints as an upper bound (see interface in Use declared interfaces for lower bounds).

Example 16. Use declared interfaces for lower bounds
interface I1<T extends any with {prop : int}> { // error
}

interface ~J {
    prop : int;
}
interface I2<T extends J> {
}

4.5. Primitive ECMAScript Types

N4JS provides the same basic types as ECMAScript [ECMA11a(p.p.28)].

In ECMAScript, basic types come in two flavors: as primitive types [ECMA11a(p.S8Types, p.p.28)] and as Objects [ECMA11a(p.S15, p.p.102)]. In N4JS, primitive types are written with lower cases, object types with first case capitalized. For example, String is the primitive ECMAScript string type, while String is an object.

The following ECMAScript primitive types are supported, they are written with lower case letters::

Although Object is a primitive type in [ECMA11a(p.S8.5)], it is interpreted here as an object type and described in Object Type.

Please note that primitive types are values (= no objects) so they have no properties and you cannot inherit from them.

4.5.1. Undefined Type

As a built-in type, the type undefined cannot be declared explicitly by the user by means of a type expression. Note in ECMAScript there are three distinct use cases of undefined:

  • undefined as type (as used here)

  • undefined as value (the only value of the undefined type)

  • undefined is a property of the global object with undefined (value) as initial value. Since ECMAScript 5 it is not allowed to reassign this property but this is not enforced by all ECMAScript/JavaScript engines.

The type undefined will be inferred to false in a boolean expression. It is important to note that something that is not assigned to a value is undefined but not null.

The type undefined is a subtype of all types. That is,

Γundefined<:T

is an axiom and true for all types T.

Whenever an expression E has an inferred type of undefined, which means it will always evaluate to value undefined at runtime, a warning is shown, unless E is …​

  • a void expression

  • the direct child expression of a void expression,

  • the direct child expression of an expression statement,

  • the undefined literal (i.e. the literal representing the undefined value),

  • the this literal.

4.5.2. Null Type

The null type cannot be declared explicitly by the user. Only the keyword null is inferred to type null.

Semantics

In contrast to undefined, it expresses the intentional absence of a value.

The null type can be assigned to any other type. That is, the type null is a subtype of all other types except undefined:

rightundefinedΓnull left<:Typeright

Please note that

  • null==undefined evaluates to true

  • null===undefined evaluates to false

  • typeof null evaluates to object

Only the null keyword is inferred to type null. If null is assigned to a variable, the type of the variable is not changed. This is true, in particular, for variable declarations. For example in

var x = null;

the type of variable x is inferred to any (cf. Variable Statement).

The type null will be inferred to false in a boolean expression.

The call typeof null will return ’object’.

4.5.3. Primitive Boolean Type

Represents a logical entity having two values, true and false.

Please note that a boolean primitive is coerced to a number in a comparison operation so that

Source Result
var a = true; console.log(a == 1)

prints true

var b = false; console.log(b == 0)

prints true

Semantics

The type boolean is subtype of any:

boolean<:any

Variables of type boolean can be auto-converted (coerced) to Boolean, as described in Autoboxing and Coercing.

4.5.4. Primitive String Type

A finite sequence of zero or more 16-bit unsigned integer values (elements). Each element is considered to be a single UTF-16 code unit.

Also string as primitive type has no properties, you can access the properties available on the object String as string will be coerced to String on the fly but just for that property call, the original variable keeps its type:

var a = "MyString"
console.log(typeof a) // string
console.log(a.length) // 8
console.log(typeof a) // string

You can handle a primitive string like an object type String but with these exceptions:

  • typeof "MyString" is 'string' but typeof new String("MyString") is 'object'

  • "MyString" instanceof String or instanceof Object will return false, for new String("MyString") both checks evaluate to true

  • console.log(eval("2+2")) returns 4, console.log(eval(new String("2+2"))) returns string "2+2"

This marks a difference to Java. In JavaScript, Unicode escape sequences are never interpreted as a special character.

Semantics

The string type is a subtype of any:

string<:any

It is supertype of the N4JS primitive type pathselector, typeName and i18nKey. Primitive Pathselector and I18nKey

However, variables of type string can be auto-converted (coerced) to string, as described in Autoboxing and Coercing.

4.5.5. Primitive Number Type

In ECMAScript numbers are usually 64-bit floating point numbers. For details see [ECMA11a(p.8.5)]. The prefix 0 indicates that the number is octal-based and the prefix 0x marks it as hexadecimal-based.

NaN can be produced by e.g. ‘0 / 0’' or ‘1 - x’. typeof NaN will return number.

Semantics

The type number is subtype of any:

number<:any

However, variables of type number can be auto-converted (coerced) to Number, as described in Integer Literals .

4.5.6. Primitive Type int

Actually ECMAScript defines an internal type int32. A number of this type is returned by the binary or operation using zero as operand, e.g. ECMAScript’s internal type int32 can be represented in N4JS by a built-in primitive type called int. For details on how numeric literals map to types number and int, refer to Integer Literals.

for the time being, built-in type int is synonymous to type number. This means one can be assigned to the other and a value declared to be of type int may actually be a 64-bit floating point number.[14]

4.5.7. Primitive Symbol Type

The primitive type symbol is directly as in ECMAScript 6. Support for symbols is kept to a minimum in N4JS. While this primitive type can be used without any restrictions, the only value of this type available in N4JS is the built-in symbol Symbol.iterator. Other built-in symbols from ECMAScript 6 and the creation of new symbols are not supported. For more details, see Symbol.

4.6. Primitive N4JS Types

Additionally to the primitive ECMAScript types, the following N4JS-specific primitive types are supported:

any

enables ECMAScript-like untyped variable declarations

void

almost similar to undefined, except it can be used as a return type of functions and methods

unknown

inferred in case of a type inference error

pathSelector<T>, i18nKey

subtypes of string

4.6.1. Any Type

Any type is the default type of all variables for without a type declaration. It has no properties. A value of any other type can be assigned to a variable of type any, but a variable declared any can only be assigned to another variable declared with the type any.

4.6.1.1. Semantics

any is supertype of all other types. That is,

ΓTypeleft<:any

is an axiom and true for all types.

4.6.1.2. Type Inference

If a variable is explicitly declared as type any, the inferred type of that variable will always be any.

4.6.1.2.1. Default Type of Variables

If a type annotation is missing and no initializer is provided, then the type of a variable is implicitly set to any.

In that case, the inferred type of that variable will always be any as well. If an initializer is provided, the declared type of the variable will be set to the inferred type of the initializer. Therefore in the latter case, the inferred type of the variable will always be the type of the initializer (cf. Variable Statement).

If a variable is declared as type , it can be used just as every variable can be used in raw ECMAScript. Since every property can be get and set, the types of properties is inferred as as well. This is formally expressed in Identifier.

4.6.2. Void Type

The type void is used to denote that there is no value at all, as opposed to type undefined which denotes that there is a value, but it is always undefined.

The only valid use of type void is to declare that a function or method does not return anything. In particular, this means:

  • void is disallowed as type argument,

  • void is disallowed as upper/lower bound of type parameters and wild cards,

  • when used as return type of functions or methods, void may not be nested, i.e.

    function foo(): void {}  // ok
    function bar(): any|void {}  // error

In all the above disallowed cases, type undefined should be used instead of void.

4.6.2.1. Semantics

Req. IDE-13: Void Type (ver. 1)

  • The type void may only be used as the immediate return type of a function or method.

  • If a function f is declared to return void, an error is created if a return statement contains an expression:

    f.returnType=void
    r,μr=ReturnStatement,r.containingFunction=f:r.expression=null

  • If a function f is declared to return void, an error is issued if the function is called in any statement or expression but an expression statement directly:

    f.returnType=void
    e,bindef:μe.container=ExpressionStatement

The following type hierarchy is defined: void is only a subtype of itself but not of any other type and no other type is a subtype of void.

void<:void

Since void cannot be used as the type of variables, fields, formal parameters, etc., a function or method with a return type of void cannot be used on the right-hand side of an assignment or in the argument list of a call expression (note the difference to plain JavaScript).

The ECMAScript void operator (see Unary Expressions) has a type of undefined, not void, because it evaluates to value undefined and can be used on the right-hand side of assignments, etc.

4.6.3. Unknown Type

Internally N4JS defines the type unknown. This type cannot be used by the user. Instead, it is inferred in case of errors. unknown behaves almost similar to any+. However no error messages once a variable or expression has been inferred to unknown in order to avoid consequential errors.

4.6.4. Primitive Pathselector and I18nKey

N4JS introduces three new types which are subtypes of string. These types are, in fact, translated to strings and do not add any new functionality. They are solely defined for enabling additional validation.

  • pathSelector<T> is a generic type for specifying path selector expressions. PathSelectors are used to specify a path to a property in a (JSON-like) model tree.

  • The type variable T defines the context type (or type of the root of the tree) in which the selector is to be validated. A path selector is defined as a string literal that has to conform to the path selector grammar. The context type is then used to perform a semantic

  • i18nKey is a string which refers to an internationalization key. The i18nKey type is used to reference resource keys specified in resource files. In a project p, the i18nKey type defines the transitive set of all resource keys accessible from p. Since resource keys are specified as strings, this means that the i18nKey type defines a subset of all string literals that can be assigned to a variable of type i18nKey in the current project. That means that an assignment of a string literal to a variable of type i18nKey is only valid if that string literal is contained in the set defined by i18nKey. Resource keys are declared in the properties files of a project and all resource keys from a project are accessible to any project depending on it.

I18nkeys are not yet validated

4.6.4.1. Semantics

The N4JS primitive types pathSelector<T>, i18nKey and pathSelector<T> are basically only marker types of strings for enabling additional validation. Thus, they are completely interchangeable with string types:

typeName<T><:stringstring<:typeName<T>
i18nKey<:stringstring<:i18nKey
pathSelector<T><:stringstring<:pathSelector<T>

As special literals for these N4JS types do not exist, the type has to be explicitly specified in order to enable the additional validation. Note that this validation cannot be applied for more complicated expressions with parts which cannot be evaluated at compile time. For example, "some.path."segment".prop" cannot be evaluated at compile time.

4.7. Built-in ECMAScript Object Types

N4JS supports all built-in ECMAScript objects [ECMA11a(p.S15)], interpreted as classes. Some of these object types are object versions of primitive types. The object types have the same name as their corresponding primitive type, but start with an upper case letter.

The following types, derived from certain ECMAScript predefined objects and constructs, are supported by means of built-in types as they are required by certain expressions.

All other ECMAScript types ([ECMA11a(p.S15)], such as Math, Date, or Error are supported by means of predefined classes. ECMAScript 2015 types are defined in the ECMAScript 2015 runtime environment. Since they are defined and used similar to user defined classes, they are not explained in further detail here. These predefined objects are kind of subtypes of Object.

4.7.1. Semantics

It is not possible to inherit from any of the built-in ECMAScript object types except for Object and Error, that is, to use one of these types as supertype of a class. From the N4JS language’s point of view, these built-in types are all final.

4.7.2. Object Type

Object [ECMA11a(p.S8.6)] is the (implicit) supertype of all declared (i.e., non-primtive) types, including native types. It models the ECMAScript type Object, except that no properties may be dynamically added to it. In order to declare a variable to which properties can be dynamically added, the type Object+ has to be declared (cf. Type Modifiers).

4.7.3. Function-Object-Type

The built-in object type Function, a subtype of Object, represents all functions, regardless of how they are defined (either via function expression, function declaration, or method declaration). They are described in detail in Function-Object-Type.

Since Function is the supertype of all functions regardless of number and types of formal parameters, return type, and number and bounds of type parameters, it would not normally be possible to invoke an instance of Function. For the time being, however, an instance of Function can be invoked, any number of arguments may be provided and the invocation may be parameterized with any number of type arguments (which will be ignored), i.e.  [Req-IDE-101] and [Req-IDE-102] do not apply.

4.7.4. Array Object Type

The Array type is generic with one type parameter, which is the item type. An array is accessed with the index operator, the type of the index parameter is Number. The type of the stored values is typeArgs[0] (cf. Array Literal). Due to type erasure, the item type is not available during runtime, that is to say there are no reflective methods returning the item type of an array.

Req. IDE-14: Array Type (ver. 1)

For an array type A, the following conditions must be true:

  • |A.typeArgs|=1

4.7.5. String Object Type

Object type version of string. It is highly recommend to use the primitive version only. Note that is is not possible to assign a primitive typed value to an object typed variable.

4.7.6. Boolean Object Type

Object type version of boolean. It is highly recommend to use the primitive version only. Note that is is not possible to assign a primitive typed value to an object typed variable.

4.7.7. Number Object Type

Object type version of number. It is highly recommend to use the primitive version only. Note that is is not possible to assign a primitive typed value to an object typed variable.

4.7.8. Global Object Type

This is the globally accessible namespace which contains element such as undefined, and in case of browsers, window. Depending on the runtime environment, the global object may has different properties defined by means of dynamic polyfills.

4.7.9. Symbol

The symbol constructor function of ECMAScript 2015. Support for symbols is kept to a minimum in N4JS:

  • creating symbols with var sym = Symbol("description") is not supported.

  • creating shared symbols with var sym = Symbol.for("key") is not supported. Also the inverse Symbol.keyFor(sym) is not supported.

  • retrieving built-in symbols via properties in Symbol is supported, however, the only built-in symbol available in N4JS is the iterator symbol that can be retrieved with Symbol.iterator.

The rationale for this selective support for symbols in N4JS is to allow for the use (and custom definition) of iterators and iterables and their application in the for…​of loop with as little support for symbols as possible.

4.7.10. Promise

Promise is provided as a built-in type as in ECMAScript 2015. Also see Asynchronous Functions for asynchronous functions.

4.7.11. Iterator Interface

A structurally typed interface for iterators as defined by theECMAScript 6 iterator protocol.

Iterable in N4JS
// providedByRuntime
export public interface ~Iterator<T>  {
    public next(): IteratorEntry<T>
}

// providedByRuntime
export public interface ~IteratorEntry<T> {
    public done: boolean;
    public value: T?;
}

Interface IteratorEntry was introduced mainly as a workaround; this interface could be removed and replaced with a corresponding structural type reference as return type of method next()

4.7.12. Iterable Interface

A structurally typed interface for objects that can be iterated over, i.e. iterables as defined by the ECMAScript 6 iterator protocol.

// providedByRuntime
export public interface ~Iterable<T> {
    public [Symbol.iterator](): Iterator<T>
}

Note that this interface’s method is special in that a symbol is used as identifier. You can use the ordinary syntax for computed property names in ECMAScript 6 for overriding / implementing or invoking this method.

4.8. Built-In N4JS Types

N4JS additionally provides some built-in classes which are always available with the need to explicitly import them.

4.8.1. N4Object

Although N4Object is a built-in type, it is not the default supertype. It is a subtype of Object.

4.8.1.1. Semantics
N4Object<:Object

4.8.2. N4Class

The type N4Class is used for extended reflection in N4JS.

Add further docs for this type

4.8.3. IterableN

Work in progress.

Currently there are built-in types Iterable2<T1,T2>…​Iterable9<T1,…​,T9>. They are mainly intended for type system support of array destructuring literals.

This is not documented in detail yet, because we want to gain experience with the current solution first, major refinement may be incoming.

4.9. Type Modifiers

Type expressions can be further described with type modifiers. The type modifiers add additional constraints to the type expression which are then used to perform a stricter validation of the source code. Type modifiers can not be used in type arguments.

The general type modifiers nullable, nonnull and dynamic can be used for variables, attributes, method parameters and method types. Optional and variadic modifiers can only be applied for formal parameters.

4.9.1. Dynamic

The dynamic type modifier marks a type as being dynamic. A dynamic type behaves like a normal JavaScript object, so you can read/write any property and call any method on it. The default behavior for a type is to be static, that is no new properties can be added and no unknown properties can be accessed.

T <: T+ and T+ <: T is always true. Using dynamically added members of a dynamic type is never type safe. Using the delete operator on a subtype of N4Object is not allowed.

Req. IDE-15: Non-Dynamic Primitive Types (ver. 1)

  1. All primitive types except any must not be declared dynamic.

  2. Only parameterized type references and this type reference can be declared dynamic.[15]

4.9.2. Optional Return Types

Only formal parameters and return types can be marked as optional.

An optional return type indicates that the function / method need not be left via a return statement with an expression; in that case the return value is undefined. For constraints on using the optional modifier, see Function-Object-Type.

4.10. Union and Intersection Type (Composed Types)

Given two or more existing types, it is possible to compose a new type by forming either the union or intersection of the base types. The following example gives a small overview of common use cases of union and intersection types.

Example 17. Composed types

This example shows how union and intersection types affect the types of their field members in case the fields have different types. It is for illustration purposes only. The type of the composed field depends on the access type: When reading, the field type of an intersection/union also resolves to the intersection/union. In contrast, when writing a field, the field type of an intersection/union resolves to the union/intersection respectively.

interface A { f : int = 1; }
interface B { f : string = "h"; }

class CA implements A {}
class CB implements B {}

let aub : A|B; // union type
let aib : A&B; // intersection type

function u() {
    aub = (catIsAlive)? new CA() : new CB(); // catIsAlive is boolean
    let x = aub.f; // x = {1 | "h"}
    aub.f = undefined; // undefined can be assigned to int and string types
}
function i() {
    let a = aib as A;
    let b = aib as B;
    a.f = 23;
    b.f = "text";
    let x = aib.f; // x = {23 & "text"} which is impossible
}
// type of 'aub.f' --> int|string
let fu = aub.f;
// type of 'aub.f' --> int&string
aub.f = undefined;
// type of 'aib.f' --> int&string
let fi = aib.f;
// type of 'aib.f' --> int|string
aib.f = undefined;

Note that no instance aib of intersection type A&B can be instantiated, since the instance’s class would have to define a field f which would have to comply to both of the interfaces A and B. Still the function i() shows in general how variables of intersection types can be casted and accessed.

The following sections define these union and intersection types in detail.

4.10.1. Union Type

Union type reflect the dynamic nature of JavaScript. Union types can be used almost everywhere (e.g., in variable declarations or in formal method parameters). The type inferencer usually avoids returning union types and prefers single typed joins or meets. The most common use case for union types is for emulating method overloading, as we describe later on.[16]

4.10.1.1. Syntax

For convenience, we repeat the definition of union type expression:

UnionTypeExpression: 'union' '{' typeRefs+=TypeRefWithoutModifiers (',' typeRefs+=TypeRefWithoutModifiers)* '}';
4.10.1.2. Semantics

An union type states that the type of a variable may be one or more types contained in the union type. In other words, a union type is a kind of type set, and the type of a variable is contained in the type set. Due to interfaces, a variable may conform to multiple types.

Req. IDE-18: Union Type (ver. 1)

For a given union type U=unionT1...Tn, the following conditions must hold:

  1. Non-empty: At least one element has to be specified:
    U.typeRefs (n1)

  2. Non-dynamic: The union type itself must not be declared dynamic:
    ¬U.dynamic

  3. Non-optional elements:
    TU.typeRefs¬T.opt

Req. IDE-19: Union Type Subtyping Rules (ver. 1)

Let U be an union type.

  • The union type is a common supertype of all its element types:

    TU.typeRefsT<:U
  • More generally, a type is a subtype of a union type, if it is a subtype of at least one type contained in the union:

    TU.typeRefs:S<:TS<:U
  • A union type is a subtype of a type S, if all types of the union are subtypes of that type. This rule is a generalization of the sub typing rules given in [Igarashi07a(p.p.40)]

    TU.typeRefs:T<:SU<:S
  • Commutativity: The order of element does not matter:

    unionA,B=unionB,A
  • Associativity: unionA,unionB,C=unionunionA,B,C

  • Uniqueness of elements: A union type may not contain duplicates (similar to sets):

    1i<kn,unionT1,...,Tn:TiTk

Let U be an union type. The following simplification rules are always automatically applied to union types.

  • Simplification of union type with one element: If a union type contains only one element, it is reduced to the element:

    unionTT
  • Simplification of union types of union types: A union type U containing another union type V is reduced to a single union type W, with W.typeRefs=U.typeRefsV.typeRefs:

    unionS1,...,Sk-1,unionT1,...,Tm,Sk+1,...,SnunionS1,...,Sk-1,T1,...,Tm,Sk+1,...,Sn
  • Simplification of union type with undefined or null: Since undefined is the bottom type, and null is kind of a second button type, they are removed from the union:

    unionT1,...,Tk-1,Tk,...,TnunionT1,...,Tk-1,undefined,Tk,...,Tn
    unionT1,...,Tk-1,Tk,...,TnunionT1,...,Tk-1,null,Tk,...,Tn

Simplification rules for union types with one element are applied first.
  • The structural typing strategy is propagated to the types of the union:

    ~unionT1,...,Tnunion~T1,,~Tn

Remarks:

  • The simplification rules may be applied recursively.

  • For given types B<:A, and the union type U=unionA,B, UB. The types are equivalent, however: A<:=U and U<:=A.[17]

Example 18. Subtyping with union type

Let A, B, and C be defined as in the chapter beginning (C<:B<:A)

The following subtyping relations with union types are to be evaluated as follows: [18]

A <: union{A}                                   -> true
A <: union{A,B}                                 -> true
B <: union{A,B}                                 -> true
C <: union{A,B}                                 -> true
A <: union{B,C}                                 -> false
B <: union{B,C}                                 -> true
C <: union{B,C}                                 -> true
union{A} <: A                                   -> true
union{B} <: A                                   -> true
union{B,C} <: A                                 -> true
union{A,B} <: B                                 -> false
union{X,Z} <: union{Z,X}                        -> true
union{X,Y} <: union{X,Y,Z}                      -> true
union{X,Y,Z} <: union{X,Y}                      -> false

The simplification constraints are used by the type inferrer. It may be useful, however, to define union types with superfluous elements, as the next example demonstrates

Example 19. Superfluous elements in union type
class A{}
class B extends A{}
class C extends A{}

function foo(p: union{A,B}) {..}

Although B is superfluous, it may indicate that the function handles parameters of type differently than one of type A or C.

Although a union type is a LCST of its contained (non-superfluous) types, the type inferrer usually does not create new union types when computing the join of types. If the join of types including at least one union type is calculated, the union type is preserved if possible. The same is true for meet.

For the definition of join and meet for union types, we define how a type is added to a union type:

Req. IDE-21: Union of union type (ver. 1)

The union of union types is defined similar to the union of sets. The union is not simplified, but it contains no duplicates.

If a type A is contained in a union type, then the union type is a common supertype, and (since it is the union itself) also the LCST of both types. This finding is the foundation of the definition of join of a (non-union) type with a union type:

Req. IDE-22: Join with Union Type (ver. 1)

The join J of a union type U with a type T is the union of both types:

J=UTUT=J

Remarks:

  • Joining a union type with another type is not similar to joining the elements of the union type directly with another type. That is

    AjoinunionB,CAjoinBjoinC
  • The computed join is simplified according to the constraints defined above.

Req. IDE-23: Meet with Union Type (ver. 1)

The meet of union types is defined as the meet of the elements. That is

T1S...TnSunionT1,...,TnST1S1,...,T1Sm,...,TnS1,...,TnSmunionT1,...,TnunionS1,...,Sm

Remarks:

  • The meet of a union type with another type is not a union type itself. This gets clear when looking at the definition of meet and union type. While for a given U=unionA,B, A<:U and B<:U, the opposite U<:A is usually not true (unless U can be simplified to A). So, for AU, usually U cannot be the meet.

The upper and lower bound of a union type U is a union type U' containing the upper and lower bound of the elements of U:

upperunionT1,...,Tn:=unionupperT1,...,upperT1lowerunionT1,...,Tn:=unionlowerT1,...,lowerT1
4.10.1.3. Warnings

In case the any type is used in a union type, all other types in the union type definition become obsolete. However, defining other typers along with the any type might seem reasonable in case those other types are treated specifically and thus are mentioned explicitly in the definition. Nevertheless the use of the any type produces a warning, since its use can indicate a misunderstanding of the union type concept and since documentation can also be done in a comment.

Req. IDE-25: Any type in union types (ver. 1)

No union type shall conatin an type:

anyU.typeRefs

Similar to the documentary purpose of using specific classes along with the any type is the following case. When two types are used, one of them a subtype of the other, then this subtype is obsolete. Still it can be used for documentary purposes. However, a warning will be produced to indicate unecessary code. The warning is only produced when both of the types are either classes or interfaces, since e.g. structural types are supertypes of any classes or interfaces.

Req. IDE-26: Redundant subtypes in union types (ver. 1)

Union types shall not contain class or interface types which are a subtype of another class or interface type that also is contained in the union type.

TTU.typeRefs:TU.typeRefs:(TT<:TisClassOrInterfaceTisClassOrInterfaceTT)

4.10.2. Intersection Type

Intersection type reflects the dynamic nature of JavaScript, similar to union type. As in Java, intersection type is used to define the type boundaries of type variables in type parameter definitions. They are inferred by the type inferencer for type checking (as a result of join or meet). In contrast to Java, however, intersection type can be declared explicitly by means of intersection type expression.[19]

4.10.2.1. Syntax

For convenience, we repeat the definition of intersection type expression and of type variables in which intersection types can be defined as in Java:

InterSectionTypeExpression: 'intersection' '{' typeRefs+=TypeRefWithoutModifiers (',' typeRefs+=TypeRefWithoutModifiers)* '}';

TypeVariable:   name=IDENTIFIER ('extends' declaredUpperBounds+=ParameterizedTypeRefNominal ('&' declaredUpperBounds+=ParameterizedTypeRefNominal)*)?
4.10.2.2. Semantics

An intersection type may contain several interfaces but only one class. It virtually declares a subclass of this one class and implements all interfaces declared in the intersection type. If no class is declared in the intersection type, the intersection type virtually declares a subclass of an N4Object instead. This virtual subclass also explains why only one single class may be contained in the intersection.

Req. IDE-27: Intersection Type (ver. 1)

For a given intersection type I, the following conditions must hold:

  1. The intersection must contain at least one type:

    I.typeRefs
  2. Only one nominally typed class must be contained in the intersection type:

    CI.typeRefs:μC=ClassC.isStructuralTI.typeRefsC:μT=ClassT.isStructural

    A warning is produced when more than one nominal class is contained in the intersection type, since only undefined (or null) can be assigned to a type reference of this type.

  3. Non-optional elements:

    TI.typeRefs¬T.opt
  4. If the intersection contains multiple references to the same generic type, a warning is produced if only undefined (or null) can be assigned to a type reference of this type. There are some rare cases in which this does not happen. This is true if for all type arguments one of the following conditions hold:

    • a type argument corresponding to a type parameter without def-site variance is a wildcard with an upper bound (use "extends" or no bound) or a type argument not defining an upper bound corresponds to a covariant (out) type parameter, and this constraint (IDE-27) holds for an intersection created from the upper bounds of the type argument (or the lower bound of the type parameter).

    • a type argument is a wildcard with lower bounds (since Object would be a solution)

Definition of structural typing attributes see [Req-ID-78701].

The combination of intersection types and generics is a bit tricky. The following example demonstrates that:

Example 20. Intersection and generics

Given the following types:

class G<T> {
     private T: t
	set(t: T) { this.t = t;}
	get(): T { return this.t; }
}
class C { public y; }
class D { public x; }
interface I {}

We use the generic with the getter and setter here only to demonstrate co- and contra variance effects.

Let

let g1: G<C> & G<D>;

be a variable. We can only assign undefined to g1, since any other value would not be confirm to the intersection. If we for example would assign

let gc = new G<C>()
g1 = gc;

we would run into contra-variance problems:

gc.set(new C());

This would implicitly also set a C in g1, which would not be compatible with D. This would lead to a problem in the following lines:

let gd: G<D> = g1;
let d: D = gd.get();

This is the typical contra variance problem.

Similar problems arise even with structural types.

Note that in theory more warnings could be produced, in particular in combination with structural types (and the fact that N4JS classes must explicitly implement even structural interfaces). We omit these kind of warnings for two reasons:

  • performance

  • anticipated slight changes in semantics (e.g. we may remove the requirement of explicitly implementing structural interfaces)

Since problems caused by not instanceable type references will be detected by programmers before runtime anyway, we do not need to be strict here. They are merely convenience features and they do not affect the correctness of the type system.

Req. IDE-175: Intersection Type Subtyping Rules (ver. 1)

Let I be an intersection type.

  • An intersection type is a subtype of another type, if at least one of its contained types is a subtype of that type: [20]

TI.typeRefs:T<:SI<:S
  • A type is a subtype of an intersection type, if it is a subtype of all types contained in the intersection type: [21]

TI.typeRefs:S<:TS<:I
  • Non-optional elements: TI.typeRefs¬T.opt

Let I be an intersection type. The following simplification rules are always automatically applied to intersection types.

  • The structural typing strategy is propagated to the types of the intersection:

    ~intersectionT1,...,Tnintersection~T1,,~Tn

These subtyping rules are similar to Ceylon. [22]

During validation, intersection types containing union or other intersection types may be inferred. In this case, the composed types are flattened. The aforementioned constraints must hold. We also implicitly use this representation in this specification.

Example 21. Subtyping with intersection type

Let A, B, and C be defined as in the chapter beginning (C<:B<:A)

The following subtyping relations with intersection types are to be evaluated as follows: [23]

A <: intersection{A}                            -> true
A <: intersection{A,A}                          -> true
intersection{A,X} <: A                          -> true
intersection{X,A} <: A                          -> true
A <: intersection{A,X}                          -> false
intersection{A,X} <: intersection{X,A}          -> true
H12 <: intersection{I1,I2}                      -> true
intersection{I1,I2} <: H12                      -> false
H1 <: intersection{I1,I2}                       -> false
H23 <: intersection{I1,I2}                      -> false
B <: intersection{A}                            -> true
intersection{I1,I2} <: I                        -> true
H12 <: intersection{I,I2}                       -> true
A <: intersection{A,Any}                        -> true
intersection{A,Any} <: A                        -> true

The join of intersection types is defined as the join of the elements. That is:

T1S...TnSintersectionT1,...,TnS
T1S1,...,T1Sm,...,TnS1,...,TnSmintersectionT1,...,TnintersectionS1,...,Sm

The meet of intersection types is defined over their elements. That is:

intersectionT1S,...,TnSintersectionT1,...,TnS
intersectionT1S1,...,T1Sm,...,TnS1,...,TnSmintersectionT1,...,TnintersectionS1,...,Sm

The upper and lower bound of an intersection type I is a union type I' containing the upper and lower bound of the elements of I:

upperintersectionT1,...,Tn:=intersectionupperT1,...,upperT1
lowerintersectionT1,...,Tn:=intersectionlowerT1,...,lowerT1

4.10.2.3. Warnings

Using any types in intersection types is obsolete since they do not change the resulting intersection type. E.g. the intersection type of A, B and any is equivialent to the intersection type of A and B. However, using the any type is no error because it can be seen as a neutral argument to the intersection. Nevertheless the use of the any type produces a warning, since its use can indicate a misunderstanding of the intersection type concept and since it always can be omitted.

Req. IDE-32: Any type in intersection types (ver. 1)

No intersection type shall contain an type:

anyI.typeRefs

The use of the any type in an intersection type is similar to the following case. When two types are used, one of them a supertype of the other, then this supertype is obsolete. Hence, a warning will be produced to indicate unecessary code. The warning is only produced when both of the types are either classes or interfaces, since e.g. structural types are supertypes of any classes or interfaces.

Intersection types shall not contain class or interface types which are a supertype of another class or interface type that also is contained in the intersection type.

TI.typeRefs:TTI.typeRefs: (TT<:TisClassOrInterfaceTisClassOrInterfaceTT)

4.10.3. Composed Types in Wildcards

Composed types may appear as the bound of a wildcard. The following constraints apply: [24]

A composed type may appear as the upper or lower bound of a wildcard. In the covariant case, the following subtype relations apply:

union{ G<? extends A>, G<? extends B> }  \subtype  G<? extends union{A,B}>
G<? extends intersection{A,B}>  \subtype  intersection{ G<? extends A>, G<? extends B> }

In the contra variant case, the following subtype relations apply:

union{ G<? super A>, G<? super B> }  \subtype  G<? super intersection{A,B}>
G<? super union{A,B}>  \subtype  intersection{ G<? super A>, G<? super B> }

4.10.4. Property Access for Composed Types

It is possible to directly access properties of union and intersection types. The following sections define which properties are accessible.

4.10.4.1. Properties of Union Type

As an (oversimplified) rule of thumb, the properties of a union type U=T1|T2 are simply the intersection of the properties U.properties=T1.propertiesT2.properties. In other words, a property 'p' in the union type is the least common denominator of all 'p' in T_{1} and T_{2}. It is not quite that simple, however, as the question of "equality" with regards to properties has to be answered.


For a given union type U=T1|T2, the following constraints for its members must hold:

 aU.attributes:

 k12: akTk.attributes:ak.acc>privatea.acc=mina1.acca2.acca.name=a1.name=a2.namea.typeRef=a1.typeRef=a2.typeRef

 mU.methods:

 m1T1.methods,m2T2.methods,
withp=m.fparsp'=m1.fparsp"=m2.fpars,WLOG|p'||p"|:

k12:mk.acc>privatem.acc=minm1.accm2.accm.name=m1.name=m2.namem.typeRef=m1.typeRef|m2.typeRef i<|p"|:pi existswithpi.name=p"i.namei|p'|p'i.name=p"i.namep'i.name+"_"+p"i.nameelsepi.typeRef=p'i.typeRef&p"i.typeRefi<|p'|p'|p'|-1.typeRef&p"i.typeRefi|p'|p'|p'|-1.varp"i.typeRefelsepi.opt=p'i.optp"i.opti<|p'|p"i.optelsepi.var=p'i.varp"i.vari<|p'|i=|p"|-1p"i.vari|p'|i=|p"|-1falseelse

(l=|p'|=|p"|¬p'l-1.optp"l-1.optvp'l-1p"l-1v.var:pl existswith
pl.name=v.name
pi.typeRef=v.typeRef
pi.opt=true
pi.var=true


The following table shows how non-method members of union types are merged. The resulting member type depends on whether the member is being accessed during a read (R) or write (W) operation. The type of a field, of a getter or of the parameter of a setter is indicated in brackets.

Table 4. Merged Members of Unions
Members S=T S≠T

R

W

R

W

field:S | field:T

field:S

getter:S|T

setter:S&T

getter:S | getter:T

getter:S

-

getter:S|T

-

setter:S | setter:T

-

setter:S

-

setter:S&T

field:S | getter:T

getter:S

-

getter:S|T

-

field:S | setter:T

-

setter:S

-

setter:S&T

getter:S | setter:T

-

-

-

-

4.10.4.1.1. Remarks on union type’s members:
  • Fields of the same type are merged to a composed field with the same type. Fields of different types are merged to a getter and setter.

  • The return type of a composed getter is the union type of the return types of the merged getters.

  • The type of a composed setter is the intersection type of the types of the merged setters.

  • Fields can be combined with getters and/or setters:

    • fields combined with getters allow read-access.

    • non-const fields combined with setters allow write-access.

    • non-const fields combined with getters and setters, i.e. each type has either a non-const field or both a getter and a setter of the given name, allow both read- and write-access.

      Again, types need not be identical; for read-access the union of the fields’ types and the getters’ return types is formed, for write-access the intersection of the fields’ types and the setters’ types is formed. In the third case above, types are combined independently for read- and write-access if the getters and setters have different types.

  • The name of a method’s parameter is only used for error or warning messages and cannot be referenced otherwise.

  • The return type of a composed method is the union type of the return types of the merged methods.

  • A composed method parameter’s type is the intersection type of the merged parameters types.

4.10.4.2. Properties of Intersection Type

As an (oversimplified) rule of thumb, the properties of an intersection type I=T1&T2 are the union of properties I.properties=T1.propertiesT2.properties. In other words, a property 'p' in the union type is the greates common denominator of all 'p' in T_{1} and T_{2}. It is not quite that simple, however, as the question of "equality” with regards to properties has to be answered.

Req. IDE-36: Members of an Intersection Type (ver. 1)

For a given intersection type I=T1&T2, the following constraints for its members must hold:

aI.attributes:

(a1T1.attributes,a1.acc>private)(a2T2.attributes,a2.acc>private)a.name=a1.namea1nulla2=nulla2.name=a1.namea2.nameelsea.acc=a1.acca1nulla2=nulla2.acca1.acca2.accelsea.typeRef=a1.typeRef&a2.typeRefa1nulla2nulla1.typeRefa1nulla2.typeRefelsea2null

mI.methods:

(m1T1.methods,m1.acc>private)(m2T1.methods,m2.acc>private):

withp=m.fparsif m1 exists p'=m1.fpars elsep'=,if m2 exists p"=m2.fpars elsep"=,WLOG|p'||p"|:m.name=m1.namem1nullm2=nullm2.name=m1.namem2.nameelsem.acc=m1.accm1nullm2=nullm2.accm1.accm2.accelsem.typeRef=m1.typeRef&m2.typeRefm1nullm2nullm1.typeRefm1nullm2.typeRefelsem2null i<|p"|:pi existswithpi.name=p"i.namei|p'|p"i.name=p'i.namep'i.name+"_"+p"i.nameelsepi.typeRef=p'i.typeRef|p"i.typeRefi<|p'|p'|p'|-1.typeRef|p"i.typeRefi|p'|p'|p'|-1.varp"i.typeRefelsepi.opt=kmin|p'|-1i:p'k.optki:p"k.optpi.var=pi.optp'i.varp"i.vari<|p'|i=|p"|-1p"i.vari|p'|i=|p"|-1falseelse

(l=|p'|=|p"|l>0¬pl-1.optvp'l-1p"l-1v.var:pl existswith
pl.name=v.name
pi.typeRef=v.typeRef
pi.opt=true
pi.var=true

The following table shows how non-method members of intersection types are merged. The resulting member type depends on whether the member is being accessed during a read (R) or write (W) operation. The type of a field, of a getter or of the parameter of a setter is indicated in brackets.

Table 5. Merged Members of Intersections
Members S=T S≠T

R

W

R

W

field:S & field:T

field:S

getter:S&T

setter:S|T

getter:S & getter:T

getter:S

-

getter:S&T

-

setter:S & setter:T

-

setter:S

-

setter:S|T

field:S & getter:T

field:S

getter:S&T

setter:S

field:S & setter:T

field:S

getter:S

setter:S|T

getter:S & setter:T

field:S

getter:S

setter:T

4.10.4.2.1. Remarks on intersection type’s methods:
  • The name of a method’s parameter is only used for error or warning messages and cannot be referenced otherwise.

  • The return type of a method is the intersection type of the return types of the merged methods.

  • A method parameter’s type is the union type of the merged parameters types.

4.11. Constructor and Classifier Type

A class definition as described in Classes declares types. Often, it is necessary to access these types directly, for example to access staticmembers or for dynamic construction of instances. These two use cases are actually slightly different and N4JS provides two different types, one for each use case: constructor and classifier type.[25] The constructor is basically the classifier type with the additional possibility to call it via new in order to create new instances of the declared type.

Both meta types are different from Java’s type Class<T>, as the latter has a defined set of members, while the N4JS metatypes will have members according to a class definition. The concept of constructors as metatypes is similar to ECMAScript 2015 [ECMA15a(p.14.5)].

4.11.1. Syntax

ConstructorTypeRef returns ConstructorTypeRef: 'constructor' '{' typeArg = [TypeArgument] '}';

ClassifierTypeRef returns ClassifierTypeRef: 'type' '{' typeArg = [TypeRef] '}';

4.11.2. Semantics

  1. Static members of a type T are actually members of the classifier type type{T}.

  2. The keyword this in a static method of a type T actually binds to the classifier type type{T}.

  3. The constructor type constructor{T} is a subtype of the classifier type type{T}:

    T:constructorT<:typeT
  4. If a class B is a subtype (subclass) of a class A, then the classifier type type{B} also is a subtype of type{A}:

    B<:AtypeB<:typeA
  5. If a class B is a subtype (subclass) of a class A, and if the constructor function of B is a subtype of the constructor function of A, then the classifier type constructor{B} also is a subtype of constructor{A} :

    B<:AB.ctor<:A.ctorconstructorB<:constructorA

    The subtype relation of the constructor function is defined in Function Type. In the case of the default N4Object constructor, the type of the object literal argument depends on required attributes.

    This subtype relation for the constructor type is enforced if the constructor of the super class is marked as final, see Constructor and Classifier Type for details.

  6. The type of a classifier declaration or classifier expression is the constructor of that class:

    μCclassifierDefinitionΓC:constructor[C]
  7. A class cannot be called as a function in ECMAScript. Thus, the constructor and type type are only subtype of Object:

    T:
    constructorT<:Object
    typeT<:Object

  8. If the type argument of the constructor is not a declared type (i.e., a wildcard or a type variable with bounds), the constructor cannot be used in a new expression. Thus, the constructor function signature becomes irrelevant for subtype checking. In that case, the following rules apply:

    S.upper<:T.upperT.lower<:S.lowerμTDeclaredTypeWithAccessModifierconstructorS<:constructorT

    Note that this is only true for the right hand side of the subtyping rule. A constructor type with a wildcard is never a subtype of a constructor type without a wildcard.

The figure Classifier and Constructor Type Subtype Relations shows the subtype relations defined by the preceding rules.

cdConstructorClassifierType
Figure 4. Classifier and Constructor Type Subtype Relations

Consequences:

  • Overriding of static methods is possible and by using the constructor or classifier type, polymorphism for static methods is possible as well.

    Example 22. Static Polymorphism
    class A {
        static foo(): string { return "A"; }
        static bar(): string { return this.foo(); }
    }
    class B extends A {
        @Override
        static foo(): string { return "B"; }
    }
    
    A.bar(); // will return "A"
    B.bar(); // will return "B", as foo() is called polymorphical
  • It is even possible to refer to the constructor of an abstract class. The abstract class itself cannot provide this constructor (it only provides a type..), that is to say only concrete subclasses can provide constructors compatible to the constructor.

    Example 23. Constructor of Abstract Class
    abstract class A {}
    class B extends A {}
    function f(ctor: constructor{A}): A { return new ctor(); }
    
    f(A); // not working: type{A} is not a subtype of constructor{A}.
    f(B); // ok

Allowing wildcards on constructor type references has pragmatic reasons. The usage of constructor references usually indicates very dynamic scenarios. In some of these scenarios, e.g., in case of dynamic creation of objects in the context of generic testing or injectors, arbitrary constructors may be used. Of course, it won’t be possible to check the correct new expression call in these cases – and using new expressions is prevented by N4JS if the constructor reference contains a wildcard. But other constraints, implemented by the client logic, may guarantee correct instantiation via more dynamic constructors, for example via the ECMAScript 2015 reflection API. In order to simplify these scenarios and preventing the use of any, wildcards are supported in constructors. Since a constructor with a wildcard cannot be used in a new expression anyway, using a classifier type is usually better than using a constructor type with wildcard.

Using wildcards on classifier types would have the same meaning as using the upper bound directly. That is, a type reference type{? extends C} can simply be replaced with type{c}, and type{?} with type{any}.

To conclude this chapter, let us compare the different types introduced above depending on whether they are used with wildcards or not:

  1. having a value of type constructor{C}, we know we have

    • a constructor function of {C} or a subclass of {C},

    • that can be used for instantiation (i.e. the represented class is not abstract),

    • that has a signature compatible to the owned or inherited constructor of {C}.

      This means we have the constructor function of class {C} (but only if is non-abstract) or the constructor function of any non-abstract subclass of {C} with an override compatible signature to that of {C}'s constructor function.

  2. having a value of type constructor{? extends C}, we know we have

    • a constructor function of {C} or a subclass of {C},

    • that can be used for instantiation (i.e. the represented class is not abstract).

      So, same situation as before except that we know nothing about the constructor function’s signature. However, if {C} has a covariant constructor, cf. Covariant Constructors, we can still conclude that we have an override compatible constructor function to that of {C}, because classes with covariant constructors enforce all their subclasses to have override compatible constructors.

  3. have a value of type type{? extends C} or type{C} (the two types are equivalent), we know we have:

    • an object representing a type (often constructor functions are used for this, e.g. in the case of classes, but could also be a plain object, e.g. in the case of interfaces),

    • that represents type {C} or a subtype thereof,

    • that cannot be used for instantiation (e.g. could be the constructor function of an abstract class, the object representing an interface, etc.).

Slightly simplified, we can say that in the first above case we can always use the value for creating an instance with new, in the second case only if the referenced type has a covariant constructor, cf. Covariant Constructors, and never in the third case.

4.11.3. Constructors and Prototypes in ECMAScript 2015

Constructors and prototypes for two classes A and B in ECMAScript 2015 (not N4JS!) for two classes A and B in ECMAScript 2015 shows the constructors, prototypes, and the relations between them for the ECMAScript 2015 code shown in Constructors and Prototypes.

Example 24. Constructors and Prototypes
class A {}
class B extends A {}

var b = new B();
Constructors and prototypes for two classes A and B in ECMAScript 2015 (not N4JS!) shows plain ECMAScript 2015 only. Also note that A is defined without an extends clause, which is what ECMAScript 2015 calls a base class (as opposed to a derived class). The constructor of a base class always has Function.prototype as its prototype. If we had defined A as class A extends Object {} in the listing above, then the constructor of A would have Object’s constructor as its prototype (depicted in as a dashed red arrow), which would make a more consistent overall picture.
ctorsProtosInES6
Figure 5. Constructors and prototypes for two classes A and B in ECMAScript 2015 (not N4JS!)

Base classes in the above sense are not available in N4JS. If an N4JS class does not provide an extends clause, it will implicitly inherit from built-in class N4Object, if it provides an extends clause stating Object as its super type, then it corresponds to what is shown in Constructors and prototypes for two classes A and B in ECMAScript 2015 (not N4JS!) with the red dashed arrow.

4.12. This Type

The this keyword may represent either a this literal (cf. This keyword and type in instance and static context) or may refer to the this type. In this section, we describe the latter case.

Typical use cases of the this type include:

  • declaring the return type of instance methods

  • declaring the return type of static methods

  • as formal parameter type of constructors in conjunction with use-site structural typing

  • the parameter type of a function type expression, which appears as type of a method parameter

  • the parameter type in a return type expression (type{this},constructor{this})

  • an existential type argument inside a return type expression for methods (e.g.ArrayList<? extends this> method(){…​})

The precise rule where it may appear is given below in [Req-IDE-37].

The this type is similar to a type variable, and it is bound to the declared or inferred type of the receiver. If it is used as return type, all return statements of the methods must return the this keyword or a variable value implicitly inferred to a this type (e.g. var x = this; return x;).

Simple This Type
class A {
    f(): this {
        return this;
    }
})
class B extends A {}

var a: A; var b: B;
a.f(); // returns something with the type of A
b.f(); // returns something with the type of B

this can be thought of as a type variable which is implicitly substituted with the declaring class (i.e. this type used in a class {A} actually means <? extends A>).

4.12.1. Syntax

ThisTypeRef returns ThisTypeRef:
    ThisTypeRefNominal | ThisTypeRefStructural;

ThisTypeRefNominal returns ThisTypeRefNominal:
    {ThisTypeRefNominal} 'this'
;

ThisTypeRefStructural  returns ThisTypeRefStructural:
        typingStrategy=TypingStrategyUseSiteOperator
        'this'
        ('with' '{' ownedStructuralMembers+=TStructMember* '}')?
;

The keyword this and the type expression this look similar, however they can refer to different types. The type always refers to the type of instances of a class. The this keyword refers to the type of instances of the class in case of instance methods, but to the classifier the of the class in case of static methods. See This Keyword for details.

Example 25. This keyword and type in instance and static context

Note that the following code is not working, because some usages below are not valid in N4JS. This is only to demonstrate the types.

class C {
    instanceMethod() {
        var c: this = this;
    }
    static staticMethod() {
        var C: type{this} = this;
    }
}

Structural typing and additional members in structural referenced types is described in Structural Typing.

4.12.2. Semantics

Req. IDE-37: This Type (ver. 1)

  • this used in the context of a class is actually inferred to an existential type ? extends A inside the class itself.

  • the this type may only be used

    • as the type of a formal parameter of a constructor, if and only if combined with use-site structural typing.

    • at covariant positions within member declarations, except for static members of interfaces.

Remarks

  • Due to the function subtype relation and constraints on overriding methods (in which the overriding method has to be a subtype of the overridden method), it is not possible to use the this type in formal parameters but only as return type. The following listing demonstrates that problem:

    class A {
        bar(x: this): void { ... } // error
        // virtually defines: bar(x: A): void
    }
    class B extends A {
        // virtually defines: bar(x: B): void
    }

    As the this type is replaced similar to a type variable, the virtually defined method bar in is not override compatible with bar in A.

    In case of constructors, this problem does not occur because a subclass constructor does not need to be override compatible with the constructor of the super class. Using this as the type of a constructor’s parameter, however, would mean that you can only create an instance of the class if you already have an instance (considering that due to the lack of method overloading a class can have only a single constructor), making creation of the first instance impossible. Therefore, this is also disallowed as the type of a constructor’s parameter.

  • The difference between the type this and the keyword this is when and how theactual type is set: the actual type of the this type is computed at compile(or validation) time and is always the containing type (of the member in which the type expression is used) or a subtype of that type – this isnot a heuristic, this is so by definition. In contrast, the actual typeof the keyword this is only available at runtime, while the type used at compilation time is only a heuristically-computed type, in other words,a good guess.

  • The value of the this type is, in fact, not influenced by any @This annotations. Instead of using this in these cases, the type expressions in the @This annotations can be used.

  • The this type is always bound to the instance-type regardless of the context it occurs in (non-static or static). To refer to the this-classifier (static type) the construct type{this} is used.

Example 26. This type in function-type-expression
class A {
    alive: boolean = true;
    methodA(func: {function(this)}): string {
       func(this);   // applying the passed-in function
       return "done";
    }
}

The use of this type is limited to situations where it cannot be referred in mixed co- and contra-variant ways. In the following example the problem is sketched up. [26]

Example 27. Problems with this type and type arguments
// Non-working example, see problem in line 15.
class M<V> {  public value: V;  }
class A {
    public store: M<{function(this)}>; // usually not allowed, but let's assume it would be possible----
}
class B extends A { public x=0; } // type of store is M<{function(B)}>

var funcA = function(a: A) {/*...something with a...*/}
var funcB = function(b: B) { console.log(b.x); }
var a: A = new A();  var b: B = new B();
b.store.value = funcA  // OK, since {function(A)} <: {function(B)}
b.store.value = funcB  // OK.

var a2: A = b; // OK, since B is a subtype of A
a2.store.value( a ) // RUNTIME ERROR, the types are all correct, but remember b.store.value was assigned to funcB, which can only handle subtypes of B!

4.13. Enums

Enums are an ordered set of literals. Although enums are not true classes, they come with built-in methods for accessing value, name and type name of the enum.

In N4JS, two flavours of enumerations are distinguished: ordinary enums (N4JS) and string based enums. Ordinary enums (or in short, enums) are used while programming in N4JS. String based enums are introduced to access enumerations derived from standards, mainly developed by the W3C, in order to access the closed set of string literals defined in webIDL syntax.

4.13.1. Enums (N4JS)

Definition and usage of an enumeration:

// assume this file to be contained in a package "myPackage"
enum Color {
    RED, GREEN, BLUE
}

enum Country {
    DE : "276",
    US : "840",
    TR : "792"
}

var red: Color = Color.RED;
var us: Country = Country.US;

console.log(red.name); // --> RED
console.log(red.value); // --> RED
console.log(red.n4class.fqn); // --> myPackage.Color
console.log(red.toString()); // --> RED

console.log(us.name); // --> US
console.log(us.value); // --> 840
console.log(us.n4classfqn); // --> myPackage.Country
console.log(us.toString()); // --> 840
4.13.1.1. Syntax
N4EnumDeclaration <Yield>:
	=>(	{N4EnumDeclaration}
		(declaredModifiers+=N4Modifier)*
		'enum' name=BindingIdentifier<Yield>? )
	'{'
		(literals+=N4EnumLiteral (',' literals+=N4EnumLiteral)*)?
	'}';

N4EnumLiteral: name=IdentifierName (':' value=STRING)?;
4.13.1.2. Semantics

The enum declaration E is of type type{E} and every enumeration is implicitly derived from N4Enum. There are similarities to other languages such as Java, for example, where the literals of an enum are treated as final static fields with the type of the enumeration and the concrete enumeration provides specific static methods including the literals. This leads to the following typing rules:

Req. IDE-38: Enum Type Rules (ver. 1)

For a given enumeration declaration E with literals L, the following type rules are defined:

  1. Every enumeration E is a subtype of the base type N4Enum:

    ΓE<:N4Enum

    which itself is a subtype of Object:

    N4Enum<:Object
  2. Every literal L of an enumeration E is of the type of the enumeration:

    LE.literalsΓL:E

This means that every literal is a subtype of N4Enum and Object:

LE.literalsL<:N4EnumL<:Object

The base enumeration type N4Enum is defined as follows:

/**
 * Base class for all enumeration, literals are assumed to be static constant fields of concrete subclasses.
 */
public object N4Enum {

    /**
     * Returns the name of a concrete literal
     */
    public get name(): string

    /**
     * Returns the value of a concrete literal. If no value is
     * explicitly set, it is similar to the name.
     */
    public get value(): string

    /**
     * Returns a string representation of a concrete literal, it returns
     * the same result as value()
     */
     public toString(): string

    /**
     * Returns the meta class object of this enum literal for reflection.
     * The very same meta class object can be retrieved from the enumeration type directly.
     */
    public static get n4type(): N4EnumType

    //IDE-785 this as return type in static

    /**
     * Returns array of concrete enum literals
     */
    public static get literals(): Array<? extends this>

    /**
     * Returns concrete enum literal that matches provided name,
     * if no match found returns undefined.
     */
    public static findLiteralByName(name: string): this

    /**
     * Returns concrete enum literal that matches provided value,
     * if no match found returns undefined.
     */
    public static findLiteralByValue (value: string): this
}

Req. IDE-39: Unique literal names (ver. 1)

  • i,j:literalsi.name=literalsj.namei=j

Literal names have to be unique.

Req. IDE-40: Enum Literals are Singletons (ver. 1)

Enum literals are singletons:

e1,e2,μe1=μe2=N4EnumLiteralΓe1=Γe2:e1==e2e1===e2
Example 28. Enumeration List

Due to the common base type N4Enum it is possible to define generics accepting only enumeration, as shown in this example:

enum Color { R, G, B}

class EList<T extends N4Enum> {
    add(t: T) {}
    get(): T { return null; }
}

var colors: EList<Color>;
colors.add(Color.R);
var c: Color = colors.get();

4.13.2. String-Based Enums

In current web standards [W3C:Steen:14:XL], definitions of enumerations are often given in webIDL syntax. While the webIDL-definition assembles a set of unique string literals as a named enum-entity, the language binding to ECMAScript refers to the usage of the members of these enumerations only. Hence, if an element of an enumeration is stored in a variable or field, passed as a parameter into a method or function or given back as a result, the actual type in JavaScript will be string. To provide the N4JS user with some validations regarding the validity of a statement at compile time, a special kind of subtypes of string are introduced: the string-based enum using the @StringBased annotation. (See also other string-based types like typename<T> pathSelector<T> and i18nKey in Primitive Pathselector and I18nKey.)

String-based enums do not have any kind of runtime representation; instead, the transpiler will replace each reference to a literal of a string-based enum by a corresponding string literal in the output code. Furthermore, no meta-information is available for string-based enums, i.e. the n4type property is not available. The only exception is the static getter literals: it is available also for string-based enums and has the same meaning. In case of string-based enums, however, there won’t be a getter used at runtime; instead, the transpiler replaces every read access to this getter by an array literal containing a string literal for each of the enum’s literals.

Req. IDE-41: String-Based Enum Type Rules (ver. 1)

For a string-based enum declaration ES with literals LS the following type rules are defined:

  1. Every string-based enumeration ES is a subtype of the base type N4StringBasedEnum:

    ΓtypeES<:N4StringBasedEnum

    which itself is not related to the standard enumeration type N4Enum

    N4StringBasedEnum:N4EnumN4Enum:N4StringBasedEnum
  2. N4StringBasedEnum is a subtype of string

    N4StringBasedEnum<:string
  3. Each literal in LS of a string-based enumeration ES is of the type of the string-based enumeration.

    lES.LSΓl<:ES
  4. [Req-IDE-39] also applies for N4StringBasedEnum.

  5. [Req-IDE-40] also applies for N4StringBasedEnum.

  6. References to string-based enums may only be used in the following places:

    1. in type annotations

    2. in property access expressions to refer to one of the enum’s literals

    3. in property access expressions to read from the static getter literals

      In particular, it is invalid to use the type of a string-based enum as a value, as in

          @StringBased enum Color { RED, GREEN, BLUE }
          var c = Color;
Example 29. WebIDL example
Gecko-Engine webIDL XMLHttpRequestResponseType as taken from [W3C:Steen:14:XL]
enum XMLHttpRequestResponseType {
  "",
  "arraybuffer",
  "blob",
  "document",
  "json",
  "text" //, ... and some mozilla-specific additions
}

Compatible Definition of this Enumeration in N4JS, provided through a runtime-library definition:

File in source-folder: w3c/dom/XMLHttpRequestResponseType.n4js
@StringBased enum XMLHttpRequestResponseType {
  vacant : "",
  arrayBuffer : "arraybuffer",
  blob : "blob",
  document : "document",
  json : "json",
  text : "text"
 }

Usage of the enumeration in the definition files of the runtime-library. Note the explicit import of the enumeration.

XMLHttpRequestResponse.n4jsd
@@ProvidedByRuntime
import XMLHttpRequestResponseType from "w3c/dom/XMLHttpRequestResponseType";
@Global
export external public class XMLHttpRequestResponse extends XMLHttpRequestEventTarget {
  // ...
  // Setter Throws TypeError Exception
  public responseType: XMLHttpRequestResponseType;
  // ...
}

Client code importing the runtime-library as defined above can now use the Enumeration in a type-safe way:

String-Based Enumeration Usage
import XMLHttpRequestResponseType from "w3c/dom/XMLHttpRequestResponseType";

public function process(req: XMLHttpRequest) : void {
  if( req.responseType == XMLHttpRequestResponseType.text ) {
    // do stuff ...
  } else {
       // signal unrecognized type.
       var errMessage: req.responseType + " is not supported"; // concatination of two strings.
       show( errMessage );
  }
}

4.14. Short-Hand Syntax

Short-hand syntax is available for a number of built-in types.

4.14.1. Array Short-Hand Syntax

For the built-in type Array a convenience short form is available. Thus, writing

let arr: string[];

is equivalent to

let arr: Array<string>;

Multi-dimensional arrays can be declared as such:

let arr: string[][][];

which is equivalent to

let arr: Array<Array<Array<string>>>;

4.14.2. IterableN Short-Hand Syntax

The built-in IterableN types (i.e. Iterable2, Iterable3, …​ Iterable9, see IterableN) are also provided with a short-hand syntax. For example, writing

let i3: [string,number,string[]];

would be equivalent to

let i3: Iterable3<string,number,Array<string>>;

Note the following special cases:

let i0: [];
let i1: [string];
let union: string|number[];

which is equivalent to

let i0: Iterable<?>;
let i1: Iterable<string>;
let union: union{string,Array<number>}; // not: Array<union{string,number}>

Further note: while this syntax is very similar to TypeScript’s tuple syntax, the semantics of tuples and IterableN are very different.

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, or project; public can be combined with @Internal; if export is true the default is else the default is private.

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
xownedMembers,μx=Field 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
xmembers,μx=Field 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 superClassifiers*

5.1.2. Common Semantics of Classifiers

Req. IDE-42: Subtyping of Classifiers (ver. 1)

For a given type C, and supertypes superClassifiers=S1...Sn directly subtyped C, the following constraints must be true:

  1. The supertypes must be accessible to the subtype:
    S1,...,Sn must be accessible to C.

  2. 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.

    0<ik:PSi:
    AC.typeArgs:bindAPA.upperBound<:P.upperBound

  3. Wildcards may not be used as type argument when binding a supertype’s type parameters.

  4. A classifier cannot be directly subtyped directly multiple times:
    Si,Sjij1..n:Si=Sji=j

In order to simplify the following constraints, we use the pseudo property members to refer to all members of a classifier. This includes all members directly declared by the classifier itself, i.e. the ownedMember, 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
Syntax N4 Class Declaration and Expression
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 superType is called direct superclass of a class, and vice versa the class is a direct subclass of superType. Instead of superType, we sometimes simply write sup. The derived set sup+ 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 write interfaces.

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 methods), fields (property fields), static members (property staticOwnedMembers), etc. can easily be added by filtering properties members or ownedMembers.

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}:

¬C.abstractΓC:constructorCC.abstractΓC:typeC

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.

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]:

m1,m2members:m1.name=m2.namem1=m2accessorPairm1m2

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).

Example 30. Simple Class

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 { }
}
Example 31. Extend and implement

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:

left=rightΓTClass left<:TClass rightshortcut
Γleft.superType.declaredType<:rightΓTClass left<:TClass right

Req. IDE-44: Implicit Supertype of Classes (ver. 1)

  1. The implicit supertype of all classes is N4Object. All classes with no explicit supertype are inherited from N4Object.

  2. If the supertype is explicitly set to Object, then the class is not derived from N4Object. Meta-information is created similar to an N4Object-derived class. Usually, there is no reason to explicitly derive a class from Object.

  3. 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:

Table 6. Extensibility of Types
Type C Settings Subclassed in

Project

Vendor

World

C.final

no

no

no

C.ctor.accessModifier=\lenum{project}

yes

no

no

C.ctor.accessModifier=\lenum{public@Internal}

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 abstract property set to true. Other classes are called concrete classes.

Req. IDE-45: Abstract Class (ver. 1)

  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.

  2. An abstract class may not be instantiated.

  3. An abstract class cannot be set to final (with annotation @Final).

Req. IDE-46: Abstract Member (ver. 1)

  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).

  2. Only methods, getters and setters can be declared as abstract (fields cannot be abstract).

  3. It is not possible to inherit from an abstract class which contains abstract members which are not visible in the subclass.

  4. An abstract member must not be set to final (with annotation @Final).

  5. 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 S=C.sup, the following constraints must hold;

  • C.sup must reference a class declaration S

  • S must be be extendable in the project of C

  • CC.sup+

  • All abstract members in S must be accessible from C:

    MS.members:M.abstract
    M is accessible from C.
    Note that M need not be an owned member of S and that this constraint applies even if C 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
Syntax N4 Interface Declaration
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 write interfaces.

5.1.4.1.3. Type Inference

The type of an interface declaration I is of type type{I}:

ΓI:typeI
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:

  1. Interfaces may not be instantiated.

  2. Interfaces cannot be set to final (with annotation @Final): ¬I.final.

  3. 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 is private.

  4. Members of an interface, except methods, must not be declared @Final:

    mI.member:m.finalmI.methods
    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
  5. 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.
mI.ownedMethods:m.name'constructor'.

Example 32. Simple Interfaces

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 I1...In directly extended by I, the following constraints must be true:

  1. Only interfaces can extend interfaces: I,I1,...,In must be interfaces.

  2. An interface may not directly extend the same interface more than once:
    Ii=Iji=j for any i,j1...n.

  3. An interface may (indirectly) extend the same interface J more than once only if

    1. J is not parameterized, or

    2. in all cases J is extended with the same type arguments for all invariant type parameters.
      Note that for type parameters of J that are declared covariant or contravariant on definition site, different type arguments may be used.

  4. All abstract members in Ii, i1...n, must be accessible from I:
    i1...n:MIi.membersM.abstract M is accessible from I.
    Note that M need not be an owned member of Ii.

Req. IDE-50: Implementing Interfaces (ver. 1)

For a given type C, and I1...In directly implemented by C, the following constraints must be true:

  1. Only classes can implement interfaces: C must be a Class.

  2. A class can only implement interfaces: I1,...,In must be interfaces.

  3. A class may not directly implement the same interface more than once:
    Ii=Iji=j for any i,j1...n.

  4. A class may (indirectly) implement the same interface J more than once only if

    1. J is not parameterized, or

    2. in all cases J is implemented with the same type arguments for all invariant type parameters.
      Note that for type parameters of J that are declared covariant or contravariant on definition site, different type arguments may be used.

  5. All abstract members in Ii, i1...n, must be accessible from C:
    i1...n:MIi.membersM.abstract M is accessible from C.
    Note that M need not be an owned member of Ii.

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 |C.typePars|1.

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.

Example 33. Generic Type Definition and Usage as Type of Variable

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.

Example 34. Binding of type variables with multiple types

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:

S

Number

Bound by first type argument, no bound constraints defined for S.

T

C

Bound by second type argument, C must be a subtype of in order to fulfill the type constraint.

U

B

Bound by third type argument, extends is reflexive, that is B fulfills the type constraint.

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

SuperClass.S

Number

Type variable s of supertype SuperClass is bound to Number.

SuperClass.T

SubClass.X=C

Type variable T of supertype SuperClass is bound to type variable X of SubClass. It gets then indirectly bound to C as specified by the type argument of the variable definition.

SuperClass.U

B

Type variable U of supertype SuperClass is auto-bound to C as no explicit binding for the third type variable is specified.

SubClass.X

C

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 parameter T, then

    B<:AG<B><:G<A>

  • if G is contravariant w.r.t. its parameter T, then

    B<:AG<A><:G<B>

  • if G is invariant w.r.t. its parameter T, then

    B<:AG<A><:G<B>
    B<:AG<A><:G<B>

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:

  1. T may only appear in variance-compatible positions:

    1. 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.

    2. 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.

    3. 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.

  2. definition-site variance may not be combined with incompatible use-site variance:

    1. if T is declared on definition site to be covariant, then no wildcard with a lower bound may be provided as type argument for T.

    2. if T is declared on definition site to be contravariant, then no wildcard with an upper bound may be provided as type argument for T.

    3. 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.

Example 35. Use-site declaration of variance

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 Lists 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

Syntax N4JS member access modifier
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, or public; the latter two can be combined with @Internal; default is project 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 accessModifier and the export 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.

assignability is related but not equal to writable modifiers used for fields. We define a partial order on this enumeration as follows:
<lr::=setanygetany
abstract

All members have a flag abstract, 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 configurable, 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)

  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.

  2. No two members may have the same name, except one is static and the other is non-static:

    m1,m2C.ownedMembers,m1m2:m1.namem2.namem1.staticm2.static
  3. 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
Syntax Method Declaration
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 Void. 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 enumerable, 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)

  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.

  2. 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:

    1. The method’s body can be determined at compile time (i.e., it has been declared @Final) and it lacks usages of this or super. This is true for instance and static methods.

    2. 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.

Example 36. Final Methods in Interfaces

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;

  1. a class implementing that interface must not define a method with the same name and

  2. 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 abstract 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).

For a given generic method M of a class C, the following constraint must hold:
 tpmm.typePars,tpCC.typePars:tpm.nametpC.name

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:

ownedCtor

the explicitly defined constructor (if any).

ctor

the explicit or implicit constructor.

If C is provided with an explicit constructor, we have C.ctor=C.ownedCtor and C.ownedCtorC.ownedMembers. Note that C.ctorC.ownedMethods in all cases.

The return type of the constructor of a class C is C. If C has type parameters T1,...,Tn, then the return type is C<T1,...,Tn>. 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 ctor of a class C, the following conditions must hold:

  1. ctor must neither be abstract nor static nor final and it must not be annotated with @Override.

  2. If a class does not explicitly define a constructor then the constructor’s signature of the superclass constructor is assumed.

  3. If a class defines a constructor with formal parameters then this constructor has to be called explicitly in constructors defined in subclasses.

  4. 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.

  5. Constructors may appear in interfaces, but some restrictions apply:

    1. constructors in interfaces must not have a body.

    2. constructors in interfaces or their containing interface or one of its direct or indirect super interfaces must be annotated with @CovariantConstructor.

  6. A constructor must not have an explicit return type declaration.

  7. The implicit return type of a constructor is this?.

  8. 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.

  1. Required attributes must be initialized:
    aC.attr:a.requireder.elements:a.name=e.name

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 a super() call the this type of structural formal parameters is replaced with the this 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.

If the structural this type is used in a constructor of a class C, and if this structural reference contains an additional structural member SM, the following constraints must hold true:

  1. For any subclass S of C, with S.ctor=C.ctor (the subclass does not define its own constructor), S must not contain a public member with same name as SM:

    S<:C,S.ctor=C.ctor MS.members: M.acc=publicM.name=SM.name

  2. C itself must not contain a public member with same name as SM:

    MC.members:M.acc=publicM.name=SM.name
Example 37. Field name conflicts with structural member name

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)

  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.

  2. Only a single formal parameter of a constructor may be annotated with @Spec.

  3. 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 of this, see Structural Read-only, Write-only and Initializer Field Typing).

  4. Using the data provided in the spec object, i.e. in the argument to the spec parameter, a spec constructor will automatically initialize

    1. all owned data fields and owned setters of the containing class, and

    2. 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.

  5. 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:

    sctor.fpar.structuralMembers,ctor.fpar.spec:
    fctor.owner.ownedFieldsΓs<:f

  6. Even if the @Spec annotation is used, the super constructor must be invoked explicitly (as usual).

It follows from no. 4 above that

  1. non-public data fields and setters are never initialized (because they will never be part of the spec parameter’s structural initializer field type),

  2. 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,

  3. 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.

Example 38. Anonymous Interface in Constructor

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
    }
}
Example 39. Spec Object and Subclasses
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.

Example 40. Superfluous Properties in @Spec Constructors

Each non-public field has to be set in the constructor via the with to the parameter otherwise properties are not used to set non-public 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 in n4jsd files can initialized via @Spec constructors or not. In particular, fields of an interface declared in a n4jsd file cannot be initialized via @Spec constructor if the interface

  1. is a built-in or

  2. does not have an @N4JS annotation

The following example illustrates this restriction.

Example 41. Interface fields that cannot be initialized via @Spec constructors
Inf.n4jsd
export external interface I  {
    public m: string;
}

@N4JS
export external interface J  {
    public n: string;
}
Test.n4js
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:

  1. C has a direct super class C' and C' is annotated with @CovariantConstructor or C' has a constructor annotated with @CovariantConstructor.

  2. C has a directly implemented interface I and `I is annotated with @CovariantConstructor or I has a constructor annotated with @CovariantConstructor.

  3. C has a direct super class or directly implemented interface that has 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)

  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.

  2. Given a class C with an owned constructor ctor and a super class Sup that has a covariant constructor (owned or inherited, see Covariant Constructors), then Sup.constructor must be accessible from C,

    1. ctor must be override compatible with S.constructor:

      overrideCompatiblectorS.constructor

      This constraint corresponds to [Req-IDE-72] except for the Override annotation which is not required here.

  3. Given a classifier C implementing interface I and I has a covariant constructor (owned or inherited, see Covariant Constructors), we require

    1. I.constructor must be accessible from C,

    2. an implementation-compatible constructor ctor must be defined in C with

      overrideCompatiblectorI.constructor

      This constraint corresponds to [Req-IDE-74] except for the @Override annotation, which is not required, here.

    3. Given a classifier C without an owned constructor and an extended class or interface Sup that has a covariant constructor (owned or inherited, see Covariant Constructors), we require the inherited constructor ctor of C within the context of C to be override compatible to itself in the context of Sup. Using notation mT to denote that a member M is to be treated as defined in container type T, which means the this-binding is set to T, we can write:

      overrideCompatiblectorCctorSup

      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.

Example 42. Covariant Constructors
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.

Example 43. Covariant Constructors in Interfaces
@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 Any.

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.

const is not the (reversed) value of the property descriptor writable 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 enumerable, set via annotation @Enumerable(true|false). The default value is true.[32]

declaredWriteable

Boolean flag reflecting the property descriptor writeable, set via annotation @Writeable(true|false). The default value is true.[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 a if a class C, the following constraints must hold:

  1. A required data field must not define an initializer:
    a.requireda.init=null

  2. 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:
     mf.owner.members:mfm.namef.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:

Γf:Γd

The type of a field declaration is either the declared type or the inferred type of the initializer expression:

d.declaredTypenullT=d.declaredTypeΓd:T
d.declaredType=nulld.expressionnullΓd:T
E=Γd.expressionEnull, undefinedT=E}
elseΓd:any

If the type contains type variables they are substituted according to type parameters which are provided by the reference:

Γtfield.typeRef:TΓTField tfield:T
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:

  1. An initializer expression must be provided in the declaration (except in n4jsd files):
    f.exprnull

  2. 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:
    subf.expr*:sub="this"

  3. A constant data field must not be annotated with @Final:
    f.const¬f.final

  4. Constant data fields are not writeable (cf. [Req-IDE-68]):
    f.const¬f.writeable

Req. IDE-63: Final Data Fields (ver. 1)

For a data field f marked as @Final, the following constraints must hold:

  1. A final data field must not be modified with const or static:
    f.final¬f.const¬f.declaredStatic

  2. A final data field is not writeable:
    f.final¬f.writeable
    A final field may, however, be set in the constructor. See [Req-IDE-68] for details.

  3. 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.

    f.exprnull(assignExp:assignExpr.containingFunction=f.owner.constructorassignExpr.left.target="this"bindassignExpr.left.propertyf)(f.publicfparf.owner.constructor.fpars:fpar.specsmstructuralMembers:sm.name=f.name)
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)]).

Example 44. Getter and Setter

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.

Getter Setter
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 g is defined or consumed (from an interface) or merged-in (via static polyfill) in a class C and a setter S with s.name=g.names.static=g.static is inherited by C from one of its super classes, then C must define a setter s' with s'.name=g.names'.static=g.static [34].

  • A setter must have exactly one formal parameter, i.e. variadic or default modifiers are not allowed.

The same applies to setters, accordingly.

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:

Syntax of optional fields
class C {
    public field?: string;

    public get getter?(): number {
        return 42;
    }
    public set setter?(value: number) {}
}

The detailed grammar is given in the sections for data fields, cf. Syntax, and field accessors, cf. Syntax.

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 L<:R in which the left-hand side is one of the following:

  1. an object literal.

  2. a new expression.

  3. 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.

  4. a reference to a const variable if its initializer expression is one of the following:

    1. an object literal.

    2. a new expression.

    3. an instance of a final class (as explained above).

    4. 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 R needs not be present in L.

  • in cases 2, 3, 4b, and 4c, only getters are optional, setters are not optional. That means, an optional getter of R needs not be present in L and an optional field of R requires only a setter in L. Note that these cases are more restricted than the cases 1 and 4a.

Moreover, optionality has an effect in case of ternary expression L<:R 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 R need not be present in L but a member with the same name and access is actually present in L, that member in L 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.

Table 7. Optional Fields

Δ

Case

Comment

~

~~

~w~

~r~

~i~

may have setter

never has setter

let x: ΔC = {};

1

nothing mandatory

let x: ΔC = new D0();

2

setters mandatory

let x: ΔC = new DG();

2

setters mandatory

let x: ΔC = new DS();

2

setters mandatory

let x: ΔC = fooD0();

none

D0 not final

let x: ΔC = fooSF0();

none

fooSF0() not nominal

let x: ΔC = fooF0();

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:

Classes and functions used in 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:

Transitivity of the use cases of optional fields
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.

Optional fields 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.).

Problem 1 of optional fields
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).

Problem 2 of optional fields
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:

  • s.static¬s.abstract

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:

  1. The class declaration instance, i.e. the classifier or constructor type, constructor{C}, i.e. C.m

  2. The class declaration instance of a subtype, i.e. the classifier or constructor type, i.e. D.m, if D is a subclass of C.

  3. v.m, if v is a variable of type C (i.e. classifier type as defined in Constructor and Classifier Type) or a subtype thereof.

  4. this.m inside the body of any static method declared in C or any sub-class of C.

  5. Via a type variable T which upper bound is a subclassof C 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.

For static data fields and static setter f the following constraint must hold:

  • For every assign expression assignExpr with f.staticassignExpr.left=T.fT=f.owner.

  • For every writing unary expression u with u.op++--f.staticu.expression=T.fT=f.owner.

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:

Example 45. Static members of classes, inheritance and polymorphism
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):

Example 46. Static members in Java
// !!! 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)

  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).

  2. 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].

  3. 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:

Example 47. Static members of interfaces
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:

  1. The name and static modifiers are equal:
    M.name=S.nameM.static=S.static

  2. The metatypes are compatible:

    μS=MethodμM=Method
    μS=FieldμMField, Getter, Setter
    μS=GetterμMField, Getter
    μS=SetterμMField, Setter

  3. The overridden member must not be declared final:
    ¬S.final

  4. Overridden member declared const can only be overridden (redefined) by const members:
    S.constM.const

  5. It is not possible to override a non-final / non-const field or a setter with a final / const field:
    μS=Field¬S.finalS.constμS=Setter¬μM=FieldM.finalM.const

  6. It is not possible to override a non-abstract member with an abstract one:
    ¬M.abstractS.abstract

  7. The types are compatible:

    μMMethod, Getter, FieldμSSetterΓM<:S
    μMSetter, FieldμSGetter¬S.constΓS<:M4

  8. The access modifier is compatible:
    M.accS.acc

We define a relation overrideCompatibleMS 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 Ii) does not override or implement another member, then it must not be declared as override. That is the following constraint must hold:

¬M.override

M'C.super.membersi=1nIi.members:
M'.name=M.nameM'.static=M.static
M'.acc>private

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 C.ctorC.ownedMethods.

Req. IDE-72: Overriding Members (ver. 1)

Given a class C and a superclass Sup. 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:

  1. S must be accessible from C

  2. M must be override compatible with S:
    overrideCompatibleMS

  3. If S is a field and M is an accessor, then an additional accessor M' must exists so that M,M' are an accessor pair for S:

    μS=FieldμM=Accessor
    M'C.member:
    overrideCompatibleM'SμMμM'=Getter,Setter

  4. 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

For the following constraints, we define two helper sets MC and MI as follows:

Given a C, and interface I1,...,In, implemented by C, with

MC=C.ownedMembers{mC.superType.members|m.acc>private}
MI=i=1nIi.members

Note that these sets already contain only non-private data fields.

5.2.8.2.1. Member Consumption

A member M defined in an interface I is consumed by an implementor C, if it becomes a member of the class, that is, MC.members.

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 I1,...,In implemented (or extended) by C, and sets MC and MI as defined in [interface_and_class_member_sets]. A non-static member M defined in any interface Ii is merged into the consumer (C), if for all other (possible) members M' of C

M'MCMIM:M.name=M'.name¬M'.static

the following constraints hold:

  1. The other member’s meta type matches the meta type of the merge candiate:

    μM=MethodμM'=Method
    μMMethodμM'Field, FieldAccessor

  2. The other member is abstract and not owned by the consumer:

    μM=μM'μM=Field
    M'.abstractM'C.ownedMembers

  3. The merge candidate’s access modifier is not less than the modifier of the other member:

    μM=μM'μM=Field
    M.accM'.acc

  4. The merge candidate’s type compatible with the other member:

    μMMethod, Getter, FieldμM'SetterΓM<:M'
    μMSetter, FieldμM'GetterΓM'<:M

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.

  1. M must be accessible from C.

  2. An implementation-compatible member M' must exist in C:

    1. if M is not a field:

      μMField
      M'C.members:
      overrideCompatibleM'M
      M'C.ownedMembersM'.override

    2. if M is a field, then either an implementation-compatible field F' or accessor pair G',S' must exist:

      μM=Field
      F'C.fields:
      overrideCompatibleF'M
      F'C.ownedMembersF'.override

      G'C.getters,S'C.setters:
      overrideCompatibleG'M
      overrideCompatibleS'M
      G'C.ownedMembersG'.override
      S'C.ownedMembersS'.override

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:

  1. 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.

  2. Methods in an implementor cannot decrease the accessibility of methods from implemented interfaces, that is

    MC.methods,M'Ii.methodsi=1...n:
    M.name=M'.nameM.accprivateM.accM'.acc

  3. Methods in the implementor must be a supertype [39] of methods from implemented interfaces. That is to say the implemented methods are override-compatible.

  4. There may be several methods M1,...,Mn defined in different implemented interfaces and a single owned method M' in MC. In this case, the above constraints must hold for all methods. In particular, M'’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 M' is said to resolve the conflict between the implemented (and also inherited) methods.

  5. 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.

  6. 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.

Example 48. Method Consumption

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.

Table 8. Consumption of methods

Interface I1

MI1

MI1

MI1

MI1

MI1

MI1

Interface I2

MI2

MI2

MI2

class S

MS

MS

MS

class C

MC

MC

MC

C.members

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.

Example 49. Field and Field Initializer Consumption
/* 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 from C, so it is not consumed from I1 (or I2). Consequently, the initializer of s in C is used.

  • d.t = "D": t is defined in D, solving a conflict stemming from the definition of t in I1 and I2. Thus, the initializer of t in D is used.

  • d.u = "I1" : u is only defined in I1, thus the initializer defined in I1 is used.

  • d.v = "I2" : v is overridden in I2, so is the field initializer. This is why d.v must be assigned to I2 and not I0.

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 T, 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 T 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, T 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.

Structural Type Operator and References
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 T is declared as structural at its definition, T.defStructural is true.

Req. IDE-75: Definition Site Structural Typing (ver. 1)

  1. The structurally defined type cannot be used on the right hand side of the instanceof operator: x instanceof T¬T.defStructural

  2. A type X is a subtype of a structurally defined type T either

    1. if it is not a subtype of N4Object [41] but it contains all public, non-static members of that type

      X:N4ObjectT.defStructural}
      mT.members,m.acc=public,¬m.static,mT.ctor:}
      m'X.members:}
      m'.acc=public¬m'.staticm'.name=m.name}
      Γm'<:m}
      μm=FieldΓm<:m'ΓX<:T
      or

    2. if it is a subtype of N4Object which explicitly extends or implements the structurally defined type.

      X<:N4ObjectT.defStructuralTX.superTypes*ΓX<:T
    3. A structurally defined type T is implicitly derived from Object if no other type is specified. In particular, a structurally defined type must not be inherited from

      T.defStructuralΓT<:Object

      T.defStructuralΓT:N4ObjectN4ObjectT.superTypes*

Example 50. Declaration Site Structural Typing

The following snippet demonstrates the effect of definition-site structural types by comparing them to nominal declared types:

Declaration Site Structural Typing
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:C! 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. ~w~X), only the writeable fields, can be accessed for writing. In the table, the term field to both, public datafields and accessors of type X. Non-public properties are never accessable in use-site structural types. In any field-structural type, methods of the referenced nominal type X are not available. The initializer structural typing provides readable fields for every writeable field of the references type.

Table 9. Available Fields of Structural Types
Property of X ~X ~~X ~r~X ~w~X ~i~X

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.

Example 51. Nested Structural Typing Strategies
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:

Example 52. Effect of structural type modifiers on use-site

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

var c:C

class C {
    private x;
    public y;
    public f()
    private g()
    public get z():Z
    public set z(z:Z)
}
interface I {
    a;
    func();
}
var cstructural:~C

class cstructural {

    public y;
    public f()

    public get z():Z
    public set z(z:Z)
}
interface ~I {
    a;
    func();
}
var cfields:~~C

class cfields {

    public y;


    public get z():Z
    public set z(z:Z)
}
interface ~~I {
    a;

}

Type

Structural Type

Structural Field Type

var crofields:~r~C

class crofields {

    public get y():Y


    public get z():Z

}
interface ~r~I {
    public get a():A

}
var cwofields:~w~C

class cwofields {

    public set y(y:Y)



    public set z(z:Z)
}
interface ~w~I {
    public set a(a:A)

}
var cinitfields:~i~C

class cinitfields {

    public get y():Y


    public get z():Z

}
interface ~i~I {
    public get a():A

}

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:

class C {..}
interface I {..}

foo(c: C, i: I) {
    c instanceof C; // ok
    c instanceof I; // ok
}
class C {..}
interface I {..}

foo(c: ~C, i: ~I) {
    c instanceof C; // error
    c instanceof I; // error
}
class C {..}
interface I {..}

foo(c: ~~C, i: ~~I) {
    c instanceof C; // error
    C instanceof I; // error
}

Type

Structural Type

Structural Field Type

Within this spec, we define the following attributes of a type reference T:

  • If a type is referenced with the structural type modifier ~ , the property T.refStructural is true.

  • If a type is referenced with the structural field type modifier ~~, the property T.refStructuralField is true.

  • If a type is referenced with the structural read-only field type modifier ~r~, the property T.refStructuralReadOnlyField is true.

  • If a type is referenced with the structural write-only field type modifier ~w~, then the property T.refStructuralWriteOnlyField is true.

  • If a type is referenced with the structural initializer field type modifier ~i~, then the property T.refStructuralInitField is true.

  • We use T.isStructural to simply refer any structural typing, i.e.+ T.isStructural=T.refStructuralT.refStructuralFieldT.refStructuralReadOnlyField \lor T.refStructuralWriteOnlyField || T.refStructuralInitField || T.defStructural$

  • We use T.isNominal as the opposite of T.isStructural, i.e.
    T.isNominal=¬T.isStructural

We call the following:

  • T the (nominal) type T,

  • ~T the structural version of T,

  • ~~T the structural field version of T,

  • ~r~T the structural read-only field,

  • ~w~T the structural write-only field and

  • ~i~T the structural initializer field version of T.

Req. IDE-76: Use-Site Structural Typing (ver. 1)

  1. The structural version of a type is a supertype of the nominal type:
    T<::~T

  2. The structural field version of a type is a supertype of the structural type:
    ~T<::~~T

  3. The structural read-only field version of a type is a supertype of the structural field type:
    ~~T<::~r~T

  4. The structural write-only field version of a type is a supertype of the structural field type:
    ~~T<::~w~T

  5. The structural (field) version of a type cannot be used on the right hand side of the instanceof operator:

    x instanceof EΓE:T
    ¬(T.refStructural
    T.refStructuralField
    T.refStructuralReadOnlyField
    T.refStructuralWriteOnlyField
    T.refStructuralInitField)

    That is, the following code will always issue an error: x instanceof ~T [42].

  6. A type X is a subtype of a structural version of a type ~T, if it contains all public, non-static members of the type T: [43]

    mT.members,m.ownerN4Object,m.acc=public,¬m.static,mT.ctor:
    m'X.members:
    m'.acc=public¬m'.staticm'.name=m.name
    Γm'<:ΓmΓX<:Γ~T

  7. A type X is a subtype of a structural field version of a type ~~T, if it contains all public, non-static fields of the type T. Special cases regarding optional fields are described in Optional Fields.

    mT.fields,m.ownerN4Object,m.acc=public,¬m.static
     m'X.fields:
    m'.acc=public¬m'.staticm'.name=m.name
    Γm':TmΓm:Tm'Tm=Tm'}
    m'.assignabilitym.assignabilityΓX<:~~T

  8. A type X is a subtype of a structural read-only field version of a type ~r~T, if it contains all public and non-static readable fields of the type T. Special cases regarding optional fields are described in Optional Fields.

    mT.fieldsT.getters,m.ownerN4Object,m.acc=public,¬m.static
     m'X.fieldsX.getters:
    m'.acc=public¬m'.staticm'.name=m.name
    Γm':TmΓm:Tm'Tm=Tm'
    m'.assignabilitym.assignabilityΓX<:~r~T

  9. A type X is a subtype of a structural write-only field version of a type ~w~T, if it contains all public and non-static writable fields of the type T. Special cases regarding optional fields are described in Optional Fields.

    mT.fieldsT.setters,m.ownerN4Object,m.acc=public,¬m.static,¬m.final
     m'X.fieldsX.setters:
    m'.acc=public¬m'.staticm'.name=m.name
    Γm':TmΓm:Tm'Tm=Tm'
    m'.assignabilitym.assignabilityΓX<:~w~T

  10. A type X is a subtype of a structural field version of a type ~~this, 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.

    Γthis:T
    mT.fieldsT.setters,m.ownerN4Object,m.acc=public,¬m.static
    m.exprnull
     m'X.fieldsX.getters:
    m'.acc=public¬m'.staticm'.name=m.name
    Γm'<:mm'.assignabilitym.assignabilityΓX<:~~this

  11. A structural field type ~~T is a subtype of a structural type ~X, if ~X only contains fields (except methods inherited from Object) and if ~~T<:~~X.

    X.methodsObject.methods=Γ~~T<:~~XΓ~~T<:~X
  12. 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 or with 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 X is a (nominal) subtype of T, it is, of course, also a subtype of ~T:

ΓX<:ΓTΓX<:Γ~T

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.

Example 53. Use-Site Structural Typing

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
Example 54. Structural types variable and instanceof operator

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.

Table 10. Member Availability in various Typing Scenarios
Member of type X ~~X ~r~X ~w~X ~i~X

private m0;

 — 

 — 

 — 

 — 

public set m1(m) { }

write

 — 

write

read

public get m2() {…​}

read

read

 — 

public m3;

read/write

read

write

read

public m4 = 'init.m4';

read/write

read

write

read ?

public m5: any?;

read?/write

read?

write

read?

@Final public m6 = 'init.m6';

read

read

@Final public m7;

read

read

read

public get m8() {…​}

read/write

read

write

read

public set m8(m) { }

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 X (structural initializer field type) is a supertype of Y (structural initializer field type) if for each public, non-static, non-optional writable field (mutable data field of setter) of X, there is a corresponding, public, non-static readable field of Y. 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.

Example 55. Subtype relationship between structural field typing
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.

Example 56. Setters with @ProvidesInitializer treated as optional
class 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.

Example 57. Access modifier in structural typing
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:

  1. For all additional members defined in a structural type reference, the constraints for member overriding ([Req-IDE-72]) apply as well.

  2. All additional members have the access modifier set to public.

  3. 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:

Example 58. Additional optional members in structural typing
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.

Example 59. Forbidden additional structural members on type variables
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.

Example 60. Use explicitly defined Interfaces
interface ~J {
    prop : int;
}
interface II<T extends J> {
}

6. Functions

Functions, be they function declarations, expressions or even methods, are internally modeled by means of a function type. In this chapter, the general function type is described along with its semantics and type constraints. Function definitions and expressions are then introduced in terms of statements and expressions. Method definitions and special usages are described in Methods.

6.1. Function Type

A function type is modeled as Object (see [ECMA11a(p.S13, p.p.98)] in ECMAScript.

Function types can be defined by means of;

6.1.1. Properties

In any case, a function type declares the signature of a function and allows validation of calls to that function. A function type has the following properties:

typePars

(0-indexed) list of type parameters (i.e. type variables) for generic functions.

fpars

(0-indexed) list of formal parameters.

returnType

(possibly inferred) return type (expression) of the function or method.

name

Name of function or method, may be empty or automatically generated (for messages).

body

The body of the function, it contains statements stmts. The body is null if a function type is defined in a type expression, and it is the last argument in case of a function object constructor, or the content of the function definition body.

Additionally, the following pseudo properties for functions are defined:

thisTypeRef

The this type ref is the type to which the this-keyword would be evaluated if used inside the function or member. The inference rules are described in This Keyword.

fpars

List of formal parameters and the this type ref. This is only used for sub typing rules. If this is not used inside the function, then any is set instead of the inferred thisTypeRef to allow for more usages. The property is computed as follows:

tfpars=if...this is used or explicitly declaredthen...thisTypeRef+fparselse...any+fpars

Parameters (in pars) have the following properties:

name

Name of the parameter.

type

Type (expression) of the parameter. Note that only parameter types can be variadic or optional.

The function definition can be annotated similar to Methods except that the final and abstract modifiers aren’t supported for function declarations. A function declaration is always final and never abstract. Also, a function has no property advice set.

Semantics

Req. IDE-79: Function Type (ver. 1)

Type Given a function type F, the following constraints must be true:

  1. Optional parameters must be defined at the end of the (formal) parameter list. In particular, an optional parameter must not be followed by a non-optional parameter:

    F.fparsi.optionalk>i:¬F.fparsk.optvar
  2. Only the last parameter of a method may be defined as variadic parameter:

    F.fparsi.variadici=|F.fpars|-1
  3. If a function explicitly defines a return type, the last statement of the transitive closure of statements of the body must be a return statement:

    F.typeRefUndefined
    |f.body.stmts|>0
    f.body.stmts|f.body.stmts*|-1*isa ReturnStatement

  4. If a function explicitly defines a return type, all return statements must return a type conform to that type:

    F.typeRefUndefined

    rF.body.stmts,r isa ReturnStatement:
    r.exprnull[[r.expr.typeRef]]<:[[F.typeRef]]

6.1.2. Type Inference

For the given non-parameterized function types Fleft with Fleft.tfpars=L0,L1,...Lk and |Fleft.typesPars|=0
Fright with Fright.tfpars=R0,R1,...Rn and |Fright.typesPars|=0,
we say Fleft conforms to Fright, written as Fleft<:Fright, if and only if:

  • Fright.returnType=void
    Fleft.returnType=voidFright.opt
    (Fleft.returnType<:Fright.returnType¬Fleft.opt¬Fright.opt)

  • if kn:

    •  i,1ik:Ri.optLi.optLi.varRi.varLi.var

    •  i,1ik:Li:>Ri

    • Lk.var=true i,k<in:LK:>Ri

      else (k>n):

    •  i,1in:Ri.optLi.optLi.varRi.varLi.var

    •  i,1in:Li:>Ri

    •  n<ik:Li.optLi.var

    • Rn.var=true i,n<ik:Li:>Rn

Function Variance Chart shows a simple example with the function type conformance relations.

cdVarianceFunctionChart
Figure 6. Function Variance Chart

{function()} <: {function(A)} <: {function(A, A)} might be surprising for Java programmers. However, in JavaScript it is possible to call a function with any number of arguments independently from how many formal parameters the function defines.

If a function does not define a return type, any is assumed if at least one of the (indirectly) contained return statements contains an expression. Otherwise void is assumed. This is also true if there is an error due to other constraint violations.

bindsfFF.returnType=nullrreturnsF:r.expressionnullΓf’(’arglist ’)’:anybindsfFF.returnType=nullrreturnsF:r.expressionnullΓf’(’arglist ’)’:void

with

rF.body.statements|μr=ReturnStatementsF.body.statementsreturnssreturnsF:RETSsubs.statements|μsub=ReturnStatementsubs.statementsreturnssubreturnss:RETS
Example 61. Function type conformance

The following incomplete snippet demonstrates the usage of two function variables f1 and f2, in which [[f2]]<:[[f1]] must hold true according to the aforementioned constraints. A function bar declares a parameter f1, which is actually a function itself. f2 is a variable, to which a function expression is a assigned. Function bar is then called with f2 as an argument. Thus, the type of f2 must be a subtype of the f1’s type.

function bar(f1: {function(A,B):C}) { ... }

var f2: {function(A,B):C} = function(p1,p2){...};
bar(f1);

The type of this can be explicitly set via the @This annotation.

Example 62. Function Subtyping
function f(): A {..}
function p(): void {..}

fAny(log: {function():any}) {...}
fVoid(f: {function():void}) {..}
fA(g: {function():A}) {...}

fAny(f);    // --> ok       A <: any
fVoid(f);   // -->error     A !<: void
fA(f);      // --> ok (easy)    A <: A

fAny(p);    // --> ok       void <: any
fVoid(p);   // --> ok       void <: void
fA(p);      // --> error    void !<: A
Example 63. Subtyping with function types

If classes A, B, and C are defined as previously mentioned, i.e. C<:B<:A, then the following subtyping relations with function types are to be evaluated as follows:

       {function(B):B} <: {function(B):B}           -> true
        {function():A} <: {function():B}            -> false
        {function():C} <: {function():B}            -> true
         {function(A)} <: {function(B)}             -> true
         {function(C)} <: {function(B)}             -> false

     {function():void} <: {function():void}         -> true
{function():undefined} <: {function():void}         -> true
     {function():void} <: {function():undefined}    -> true (!)

        {function():B} <: {function():void}         -> true (!)
        {function():B} <: {function():undefined}    -> false (!)
     {function():void} <: {function():B}            -> false
{function():undefined} <: {function():B}            -> true

The following examples demonstrate the effect of optional and variadic parameters:

{function(A)} <: {function(B)}                      -> true
{function(A...)} <: {function(A)}                   -> true
{function(A, A)} <: {function(A)}                   -> false
{function(A)} <: {function(A,A)}                    -> true (!)
{function(A, A...)} <: {function(A)}                -> true
{function(A)} <: {function(A,A...)}                 -> true (!)
{function(A, A...)} <: {function(B)}                -> true
{function(A?)} <: {function(A?)}                    -> true
{function(A...)} <: {function(A...)}                -> true
{function(A?)} <: {function(A)}                     -> true
{function(A)} <: {function(A?)}                     -> false
{function(A...)} <: {function(A?)}                  -> true
{function(A?)} <: {function(A...)}                  -> true (!)
{function(A,A...)} <: {function(A...)}              -> false
{function(A,A?)} <: {function(A...)}                -> false
{function(A?,A...)} <: {function(A...)}             -> true
{function(A...)} <: {function(A?,A...)}             -> true
{function(A...)} <: {function(A?)}                  -> true
{function(A?,A?)} <: {function(A...)}               -> true (!)
{function(A?,A?,A?)} <: {function(A...)}            -> true (!)
{function(A?)} <: {function()}                      -> true (!)
{function(A...)} <: {function()}                    -> true (!)

The following examples demonstrate the effect of optional return types:

{function():void} <: {function():void}              -> true
{function():X}    <: {function():void}              -> true
{function():X?}   <: {function():void}              -> true
{function():void} <: {function():Y}                 -> false
{function():X}    <: {function():Y}                 -> X <: Y
{function():X?}   <: {function():Y}                 -> false (!)
{function():void} <: {function():Y?}                -> true (!)
{function():X}    <: {function():Y?}                -> X <: Y
{function():X?}   <: {function():Y?}                -> X <: Y
       {function():B?} <: {function():undefined}    -> false (!)
{function():undefined} <: {function():B?}           -> true

The following examples show the effect of the @This annotation:

{@This(A) function():void} <: {@This(X) function():void}    -> false
{@This(B) function():void} <: {@This(A) function():void}    -> false
{@This(A) function():void} <: {@This(B) function():void}    -> true
{@This(any) function():void} <: {@This(X) function():void}  -> true
{function():void} <: {@This(X) function():void}             -> true
{@This(A) function():void} <: {@This(any) function():void}  -> false
{@This(A) function():void} <: {function():void}             -> false

For the given function types
Fleft with Fleft.tfpars=L0,L1,...Lk
Fright with Fright.tfpars=R0,R1,...Rn,
we say Fleft conforms to Fright, written as Fleft<:Fright, if and only if:

  • if |Fleft.typePars|=|Fright.typePars|=0:

  • else if
    |Fleft.typePars|>0|Fright.typePars|=0:

  • else if |Fleft.typePars|=|Fright.typePars|:

    • ΓVirVil|0inFleft<:Fright ( accordingly)

    • -

      0in:intersectionVil.upperBounds:>intersectionVir.upperBounds

      with Fleft.typePars=V0l,V1l,...Vnl and Fright.typePars=V0r,V1r,...Vnr
      (i.e. we replace each type variable in Fright by the corresponding type variable at the same index in Fleft and check the constraints from Function Type Conformance Non-Parameterized as if Fleft and Fright were non-parameterized functions and, in addition, the upper bounds on the left side need to be supertypes of the upper bounds on the right side).

Note that the upper bounds on the left must be supertypes of the right-side upper bounds (for similar reasons why types of formal parameters on the left are required to be supertypes of the formal parameters’ types in ). Where a particular type variable is used, on co- or contra-variant position, is not relevant:

Example 64. Bounded type variable at co-variant position in function type
class A {}
class B extends A {}

class X {
    <T extends B> m(): T { return null; }
}
class Y extends X {
    @Override
    <T extends A> m(): T { return null; }
}

Method m in Y may return an A, thus breaking the contract of m in X, but only if it is parameterized to do so, which is not allowed for clients of X, only those of Y. Therefore, the override in the above example is valid.

The subtype relation for function types is also applied for method overriding to ensure that an overriding method’s signature conforms to that of the overridden method, see [Req-IDE-72] (applies to method comnsumption and implementation accordingly, see [Req-IDE-73] and [Req-IDE-74]). Note that this is very different from Java which is far more restrictive when checking overriding methods. As Java also supports method overloading: given two types A,B with B<:A and a super class method void m(B param), it is valid to override m as void m(A param) in N4JS but not in Java. In Java this would be handled as method overloading and therefore an @Override annotation on m would produce an error.

The upper bound of a function type F is a function type with the lower bound types of the parameters and the upper bound of the return type:
upperfunctionP1...Pn:R:=functionlowerP1...lowerPn:upperR

The lower bound of a function type F is a function type with the upper bound types of the parameters and the lower bound of the return type:
lowerfunctionP1...Pn:R:=functionupperP1...upperPn:lowerR

6.1.3. Autoboxing of Function Type

Function types, compared to other types like String, come only in on flavour: the Function object representation. There is no primitive function type. Nevertheless, for function type expressions and function declarations, it is possible to call the properties of Function object directly. This is similar to autoboxing for strings.

Access of Function properties on functions
// function declaration
var param: number = function(a,b){}.length // 2

function a(x: number) : number { return x*x; }
// function reference
a.length; // 1

// function variable
var f = function(m,l,b){/*...*/};
f.length; // 3

class A {
    s: string;
    sayS(): string{ return this.s; }
}

var objA: A = new A();
objA.s = "A";

var objB = {s:"B"}

// function variable
var m = objA.sayS; // method as function, detached from objA
var mA: {function(any)} = m.bind(objA); // bind to objA
var mB: {function(any)} = m.bind(objB); // bind to objB

m()  // returns: undefined
mA() // returns: A
mB() // returns: B

m.call(objA,1,2,3);  // returns: A
m.apply(objB,[1,2,3]); // returns: B
m.toString(); // returns: function sayS(){ return this.s; }

6.1.4. Arguments Object

A special arguments object is defined within the body of a function. It is accessible through the implicitly-defined local variable named , unless it is shadowed by a local variable, a formal parameter or a function named arguments or in the rare case that the function itself is called ’arguments’ [ECMA11a(p.S10.5, p.p.59)]. The argument object has array-like behavior even though it is not of type array:

  • All actual passed-in parameters of the current execution context can be retrieved by 0-based index access.

  • The length property of the arguments object stores the actual number of passed-in arguments which may differ from the number of formally defined number of parameters fpars of the containing function.

  • It is possible to store custom values in the arguments object, even outside the original index boundaries.

  • All obtained values from the arguments object are of type any.

In non-strict ES mode the callee property holds a reference to the function executed [ECMA11a(p.S10.6, p.p.61)].

Req. IDE-81: Arguments.callee (ver. 1)

In N4JS and in ES strict mode the use of arguments.callee is prohibited.

Req. IDE-82: Arguments as formal parameter name (ver. 1)

In N4JS, the formal parameters of the function cannot be named arguments. This applies to all variable execution environments like field accessors (getter/setter, Field Accessors (Getter/Setter)), methods (Methods) and constructors (Constructor and Classifier Type), where FormalParameter type is used.

// regular function
function a1(s1: string, n2: number) {
    var l: number = arguments.length;
    var s: string = arguments[0] as string;
}

class A {
    // property access
    get s(): string { return ""+arguments.length; } // 0
    set s(n: number) { console.log( arguments.length ); }  // 1
    // method
    m(arg: string) {
        var l: number = arguments.length;
        var s: string = arguments[0]  as string;
    }
}

// property access in object literals
var x = {
    a:5,
    get b(): string {
        return ""+arguments.length
    }
}

// invalid:
function z(){
    arguments.length // illegal, see next lines
    // define arguments to be a plain variable of type number:
    var arguments: number = 4;
}

6.2. ECMAScript 5 Function Definition

6.2.1. Function Declaration

6.2.1.1. Syntax

A function can be defined as described in [ECMA11a(p.S13, p.p.98)] and additional annotations can be specified. Since N4JS is based on [ECMA15a], the syntax contains constructs not available in [ECMA11a]. The newer constructs defined only in [ECMA15a] and proposals already implemented in N4JS are described in ECMAScript 2015 Function Definition and ECMAScript Proposals Function Definition.

In contrast to plain JavaScript, function declarations can be used in blocks in N4JS. This is only true, however, for N4JS files, not for plain JS files.
Syntax Function Declaration and Expression
FunctionDeclaration <Yield>:
    => ({FunctionDeclaration}
        annotations+=Annotation*
        (declaredModifiers+=N4Modifier)*
        -> FunctionImpl <Yield,Yield,Expression=false>
    ) => Semi?
;


fragment AsyncNoTrailingLineBreak *: (declaredAsync?='async' NoLineTerminator)?;

fragment FunctionImpl<Yield, YieldIfGenerator, Expression>*:
    'function'
    (
        generator?='*' FunctionHeader<YieldIfGenerator,Generator=true> FunctionBody<Yield=true,Expression>
    |   FunctionHeader<Yield,Generator=false> FunctionBody<Yield=false,Expression>
    )
;

fragment FunctionHeader<Yield, Generator>*:
    TypeVariables?
    name=BindingIdentifier<Yield>?
    StrictFormalParameters<Yield=Generator>
    (-> ':' returnTypeRef=TypeRef)?
;

fragment FunctionBody <Yield, Expression>*:
        <Expression> body=Block<Yield>
    |   <!Expression> body=Block<Yield>?
;

Properties of the function declaration and expression are described in Function Type.

For this specification, we introduce a supertype FunctionDefinition for both, FunctionDeclaration and FunctionExpression. This supertype contains all common properties of these two subtypes, that is, all properties of FunctionExpression.

Example 65. Function Declaration with Type Annotation
// plain JS
function f(p) { return p.length }
// N4JS
function f(p: string): number { return p.length }
6.2.1.2. Semantics

A function defined in a class’s method (or method modifier) builder is a method, see Methods for details and additional constraints. The metatype of a function definition is function type (Function Type), as a function declaration is only a different syntax for creating a Function object. Constraints for function type are described in Function Type. Another consequence is that the inferred type of a function definition fdecl is simply its function type F.

[[fdecl]][[F]]

Note that the type of a function definition is different from its return type f.decl!

  1. In plain JavaScript, function declarations must only be located on top-level, that is they must not be nested in blocks. Since this is supported by most JavaScript engines, only a warning is issued.

6.2.2. Function Expression

A function expression [ECMA11a(p.S11.2.5)] is quite similar to a function declaration. Thus, most details are explained in ECMAScript 5 Function Definition.

6.2.2.1. Syntax
FunctionExpression:
         ({FunctionExpression}
            FunctionImpl<Yield=false,YieldIfGenerator=true,Expression=true>
         )
;
6.2.2.2. Semantics and Type Inference

In general, the inferred type of a function expression simply is the function type as described in Function Type. Often, the signature of a function expression is not explicitly specified but it can be inferred from the context. The following context information is used to infer the full signature:

  • If the function expression is used on the right hand side of an assignment, the expected return type can be inferred from the left hand side.

  • If the function expression is used as an argument in a call to another function, the full signature can be inferred from the corresponding type of the formal parameter declaration.

Although the signature of the function expression may be inferred from the formal parameter if the function expression is used as argument, this inference has some conceptual limitations. This is demonstrated in the next example.

Example 66. Inference Of Function Expression’s Signature

In general, {function():any} is a subtype of {function():void} (cf. Function Type). When the return type of a function expression is inferred, this relation is taken into account which may lead to unexpected results as shown in the following code snippet:

function f(cb: {function():void}) { cb() }
f(function() { return 1; });

No error is issued: The type of the function expression actually is inferred to {function():any}, because there is a return statement with an expression. It is not inferred to {function():void}, even if the formal parameter of f suggests that. Due to the previously-stated relation {function():any} <: {function():void} this is correct – the client (in this case function f) works perfectly well even if cb returns something. The contract of arguments states that the type of the argument is a subtype of the type of the formal parameter. This is what the inferencer takes into account!

6.3. ECMAScript 2015 Function Definition

6.3.1. Formal Parameters

Parameter handling has been significantly upgraded in ECMAScript 6. It now supports parameter default values, rest parameters (variadics) and destructuring. Formal parameters can be modified to be either default or variadic. In case a formal parameter has no modifier, it is called normal. Modified parameters also become optional.

Modifiers of formal parameters such as default or rest are neither evaluated nor rewritten in the transpiler.

6.3.1.1. Optional Parameters

An optional formal parameter can be omitted when calling a function/method. An omitted parameter has the value undefined. In case the omitted parameter is variadic, the value is an empty array.

Parameters can not be declared as optional explicitly. Instead, being optional is true when a parameter is declared as default or variadic. Note that any formal parameter that follows a default parameter is itself also a default thus an optional parameter.

6.3.1.2. Default Parameters

A default parameter value is specified for a parameter via an equals sign (=). If a caller doesn’t provide a value for the parameter, the default value is used.

Default initializers of parameters are specified at a formal parameter of a function or method after the equal sign using an arbitrary initializer expression, such as var = "s". However, this default initializer can be omitted. When a formal parameter has a declared type, the default initializer is specified at the end, such as: var : string = "s". The initializer expression is only evaluated in case no actual argument is given for the formal parameter. Also, the initializer expression is evaluated when the actual argument value is undefined.

Formal parameters become default parameters implicitly when they are preceded by an explicit default parameter. In such cases, the default initializer is undefined.

Req. IDE-14501: Default parameters (ver. 1)

Any normal parameter which is preceded by a default parameter also becomes a default parameter. Its initializer is undefined.

When a method is overwritten, its default parameters are not part of the overwriting method. Consequently, initializers of default parameters in abstract methods are obsolete.

6.3.1.3. Variadic

Variadic parameters are also called rest parameters. Marking a parameter as variadic indicates that method accepts a variable number of parameters. A variadic parameter implies that the parameter is also optional as the cardinality is defined as 0..*. No further parameter can be defined after a variadic parameter. When no argument is given for a variadic parameter, an empty array is provided when using the parameter in the body of the function or method.

Req. IDE-16: Variadic and optional parameters (ver. 1)

For a parameter p, the following condition must hold: p.varp.opt.

A parameter can not be declared both variadic and with a default value. That is to say that one can either write varName= (default) or ...varName, but not ...varName=.

Declaring a variadic parameter of type T causes the type of the method parameter to become Array<T>. That is, declaring function(…​tags : string) causes tags to be an Array<string> and not just a scalar string value.

To make this work at runtime, the compiler will generate code that constructs the parameter from the arguments parameter explicitly passed to the function.

Req. IDE-17: Variadic at Runtime (ver. 1)

At runtime, a variadic parameter is never set to undefined. Instead, the array may be empty. This must be true even if preceding parameters are optional and no arguments are passed at runtime.

For more constraints on using the variadic modifier, see Function-Object-Type.

6.3.2. Generator Functions

Generators come together with the yield expression and can play three roles: the role of an iterator (data producer), of an observer (data consumer), and a combined role which is called coroutines. When calling a generator function or method, the returned generator object of type Generator<TYield,TReturn,TNext> can be controlled by its methods (cf. [ECMA15a(p.S14.4)], also see [Kuizinas14a]).

6.3.2.1. Syntax

Generator functions and methods differ from ordinary functions and methods only in the additional * symbol before the function or method name. The following syntax rules are extracted from the real syntax rules. They only display parts relevant to declaring a function or method as a generator.

GeneratorFunctionDeclaration <Yield>:
        (declaredModifiers+=N4Modifier)*
        'function' generator?='*'
        FunctionHeader<YieldIfGenerator,Generator=true>
        FunctionBody<Yield=true,Expression=false>
;

GeneratorFunctionExpression:
        'function' generator?='*'
        FunctionHeader<YieldIfGenerator,Generator=true>
        FunctionBody<Yield=true,Expression=true>
;

GeneratorMethodDeclaration:
    annotations+=Annotation+ (declaredModifiers+=N4Modifier)* TypeVariables?
    generator?='*' NoLineTerminator LiteralOrComputedPropertyName<Yield>
    MethodParamsReturnAndBody<Generator=true>
6.3.2.2. Semantics

The basic idea is to make code dealing with Generators easier to write and more readable without changing their functionality. Take this example:

Example 67. Two simple generator functions
// explicit form of the return type
function * countTo(iMax:int) : Generator<int,string,undefined> {
	for (int i=0; i<=iMax; i++)
		yield i;
	return "finished";
}
val genObj1 = countTo(3);
val values1 = [...genObj1]; // is [0,1,2,3]
val lastObj1 = genObj1.next(); // is {value="finished",done=true}

// shorthand form of the return type
function * countFrom(start:int) : int {
	for (int i=start; i>=0; i--)
		yield i;
	return finished;
}
val genObj2 = countFrom(3);
val values2 = [...genObj2]; // is [3,2,1,0]
val lastObj2 = genObj2.next(); // is {value="finished",done=true}

In the example above, two generator functions are declared. The first declares its return type explicitly whereas the second uses a shorthand form.

Generator functions and methods return objects of the type Generator<TYield,TReturn,TNext> which is a subtype of the Iterable<TYield> and Iterator<TYield> interfaces. Moreover, it provides the methods throw(exception:any) and return(value:TNext?) for advanced control of the generator object. The complete interface of the generator class is given below.

The generator class
public providedByRuntime interface Generator<out TYield, out TReturn, in TNext>
	extends Iterable<TYield>, Iterator<TYield> {
	public abstract next(value: TNext?): IteratorEntry<TYield>
	public abstract [Symbol.iterator](): Generator<TYield, TReturn, TNext>
	public abstract throw(exception: any): IteratorEntry<TYield>;
	public abstract return(value: TNext?): IteratorEntry<TReturn>;
}

Req. IDE-14370: Modifier * (ver. 1)

  1. * may be used on declared functions and methods, and for function expressions.

  2. A function or method f with a declared return type R that is declared * has an actual return type of Generator<TYield,TReturn,TNext>.

  3. A generator function or method can have no declared return type, a shorthand form of a return type or an explicitly declared return type.

    1. The explicitly declared return type is of the form Generator<TYield,TReturn,TNext> with the type variables:

      1. TYield as the expected type of the yield expression argument,

      2. TReturn as the expected type of the return expression, and

      3. TNext as both the return type of the yield expression.

    2. The shorthand form only declares the type of TYield which implicitly translates to Generator<TYield,TReturn,any> as the return type.

      1. The type TReturn is inferred to either undefined or any from the body.

      2. In case the declared type is void, actual return type evaluates to Generator<undefined,undefined,any>.

    3. If no return type is declared, both TYield and TReturn are inferred from the body to either any or undefined. TNext is any.

  4. Given a generator function or method f with an actual return type Generator<TYield,TReturn,TNext>:

    1. all yield statements in f must have an expression of type TYield.

    2. all return statements in f must have an expression of type TReturn.

  5. Return statements in generator functions or methods are always optional.

Req. IDE-14371: Modifier yield and yield* (ver. 1)

  1. yield and yield* may only be in body of generator functions or methods.

  2. yield expr takes only expressions expr of type TYield in a generator function or methods with the actual type Generator<TYield,TReturn,TNext>.

  3. The return type of the yield expression is TNext.

  4. yield* fg() takes only iterators of type Iterator<TYield>, and generator functions or methods fg with the actual return type Generator<? extends TYield,? extends TReturn,? super TNext>.

  5. The return type of the yield* expression is any, since a custom iterator could return an entry {done=true,value} and any value for the variable value.

Similar to async functions, shorthand and explicit form * function():int{}; and * function():Generator<int,TResult,any> are equal, given that the inferred TResult of the former functions equals to TResult in the latter function). In other words, the return type of generator functions or methods is wrapped when it is not explicitly defined as Generator already. Thus, whenever a nested generator type is desired, it has to be defined explicitly. Consider the example below.

Type variables with async methods.
class C<T> {
	genFoo(): T{} // equals to genFoo(): Generator<T, undefined, any>;
				// note that TResult depends on the body of genFoo()
}
function fn(C<int> c1, C<Generator<int,any,any>> c2) {
	c1.genFoo();  // returns Generator<int, undefined, any>
	c2.genFoo();  // returns Generator<Generator<int,any,any>, undefined, any>
}
6.3.2.3. Generator Arrow Functions

As of now, generator arrow functions are not supported by EcmaScript 6 and also, the support is not planned. However, introducing generator arrow function in EcmaScript is still under discussion. For more information, please refer to ESDiscuss.org and StackOverflow.com.

6.3.3. Arrow Function Expression

This is an ECMAScript 6 expression (see [ECMA15a(p.S14.2)]) for simplifying the definition of anonymous function expressions, a.k.a. lambdas or closures. The ECMAScript Specification calls this a function definition even though they may only appear in the context of expressions.

Along with Assignments, Arrow function expressions have the least precedence, e.g. they serve as the entry point for the expression tree.

Arrow function expressions can be considered syntactic window-dressing for old-school function expressions and therefore do not support the benefits regarding parameter annotations although parameter types may be given explicitly. The return type can be given as type hint if desired, but this is not mandatory (if left out, the return type is inferred). The notation @=> stands for an async arrow function (Asynchronous Arrow Functions).

6.3.3.1. Syntax

The simplified syntax reads like this:

ArrowExpression returns ArrowFunction:
    =>(
        {ArrowFunction}
        (
            '('
                ( fpars+=FormalParameterNoAnnotations ( ',' fpars+=FormalParameterNoAnnotations )* )?
            ')'
            (':' returnTypeRef=TypeRef)?
        |   fpars+=FormalParameterNoType
        )
        '=>'
    ) (
        (=> hasBracesAroundBody?='{' body=BlockMinusBraces '}') | body=ExpressionDisguisedAsBlock
    )
;

FormalParameterNoAnnotations returns FormalParameter:
    (declaredTypeRef=TypeRef variadic?='...'?)? name=JSIdentifier
;
FormalParameterNoType returns FormalParameter: name=JSIdentifier;

BlockMinusBraces returns Block: {Block} statements+=Statement*;

ExpressionDisguisedAsBlock returns Block:
    {Block} statements+=AssignmentExpressionStatement
;

AssignmentExpressionStatement returns ExpressionStatement: expression=AssignmentExpression;
6.3.3.2. Semantics and Type Inference

Generally speaking, the semantics are very similar to the function expressions but the devil’s in the details:

  • arguments: Unlike normal function expressions, an arrow function does not introduce an implicit arguments variable (Arguments Object), therefore any occurrence of it in the arrow function’s body has always the same binding as an occurrence of arguments in the lexical context enclosing the arrow function.

  • this: An arrow function does not introduce a binding of its own for the this keyword. That explains why uses in the body of arrow function have the same meaning as occurrences in the enclosing lexical scope. As a consequence, an arrow function at the top level has both usages of arguments and this flagged as error (the outer lexical context doesn’t provide definitionsfor them).

  • super: As with function expressions in general, whether of the arrow variety or not, the usage of super isn’t allowed in the body of arrow functions.

In N4JS, a top-level arrow function can’t refer to this as there’s no outer lexical context that provides a binding for it.

In N4JS, a top-level arrow function can’t include usages of arguments in its body, again because of the missing binding for it.

6.4. ECMAScript Proposals Function Definition

6.4.1. Asynchronous Functions

To improve language-level support for asynchronous code, there exists an ECMAScript proposal [45] based on Promises which are provided by ES6 as built-in types. N4JS implements this proposal. This concept is supported for declared functions and methods (Asynchronous Methods) as well as for function expressions and arrow functions (Asynchronous Arrow Functions).

6.4.1.1. Syntax

The following syntax rules are extracted from the real syntax rules. They only display parts relevant to declaring a function or method as asynchronous.

AsyncFunctionDeclaration <Yield>:
        (declaredModifiers+=N4Modifier)*
        declaredAsync?='async' NoLineTerminator 'function'
        FunctionHeader<Yield,Generator=false>
        FunctionBody<Yield=false,Expression=false>
;

AsyncFunctionExpression:
        declaredAsync?='async' NoLineTerminator 'function'
        FunctionHeader<Yield=false,Generator=false>
        FunctionBody<Yield=false,Expression=true>
;

AsyncArrowExpression <In, Yield>:
        declaredAsync?='async' NoLineTerminator '('
            (fpars+=FormalParameter<Yield>
                (',' fpars+=FormalParameter<Yield>)*)?
        ')' (':' returnTypeRef=TypeRef)? '=>'
        (   '{' body=BlockMinusBraces<Yield> '}'
            | body=ExpressionDisguisedAsBlock<In>
        )
;

AsyncMethodDeclaration:
    annotations+=Annotation+ (declaredModifiers+=N4Modifier)* TypeVariables?
    declaredAsync?='async' NoLineTerminator LiteralOrComputedPropertyName<Yield>
    MethodParamsReturnAndBody

’async’ is not a reserved word in ECMAScript and it can therefore be used either as an identifier or as a keyword, depending on the context. When used as a modifier to declare a function as asynchronous, then there must be no line terminator after the async modifier. This enables the parser to distinguish between using async as an identifier reference and a keyword, as shown in the next example.

Example 68. Async as keyword and identifier
async (1)
function foo() {}
// vs
async function bar(); (2)
1 In this snippet, the async on line 1 is an identifier reference (referencing a variable or parameter) and the function defined on line 2 is a non-asynchronous function. The automatic semicolon insertion adds a semicolon after the reference on line 1.
2 In contrast, async on line 4 is recognized as a modifier declaring the function as asynchronous.
6.4.1.2. Semantics

The basic idea is to make code dealing with Promises easier to write and more readable without changing the functionality of Promises. Take this example:

A simple asynchronous function using async/await.
// some asynchronous legacy API using promises
interface DB {}
interface DBAccess {
    getDataBase(): Promise<DB,?>
    loadEntry(db: DB, id: string): Promise<string,?>
}

var access: DBAccess;

// our own function using async/await
async function loadAddress(id: string) : string {
    try {
        var db: DB = await access.getDataBase();
        var entry: string = await access.loadEntry(db, id);
        return entry.address;
    }
    catch(err) {
        // either getDataBase() or loadEntry() failed
        throw err;
    }
}

The modifier async changes the return type of loadAddress() from string (the declared return type) to Promise<string,?> (the actual return type). For code inside the function, the return type is still string: the value in the return statement of the last line will be wrapped in a Promise. For client code outside the function and in case of recursive invocations, the return type is Promise<string,?>. To raise an error, simply throw an exception, its value will become the error value of the returned Promise.

If the expression after an await evaluates to a Promise, execution of the enclosing asynchronous function will be suspended until either a success value is available (which will then make the entire await-expression evaluate to this success value and continue execution) or until the Promise is rejected (which will then cause an exception to be thrown at the location of the await-expression). If, on the other hand, the expression after an await evaluates to a non-promise, the value will be simply passed through. In addition, a warning is shown to indicate the unnecessary await expression.

Note how method loadAddress() above can be implemented without any explicit references to the built-in type Promise. In the above example we handle the errors of the nested asynchronous calls to getDataBase() and loadEntry() for demonstration purposes only; if we are not interested in the errors we could simply remove the try/catch block and any errors would be forwarded to the caller of loadAddress().

Invoking an async function commonly adopts one of two forms:

  • var p: Promise<successType,?> = asyncFn()

  • await asyncFn()

These patterns are so common that a warning is available whenever both

  1. Promise is omitted as expected type; and

  2. await is also omitted.

The warning aims at hinting about forgetting to wait for the result, while remaining non-noisy.

Req. IDE-86: Modifier async and await (ver. 1)

  1. async may be used on declared functions and methods as well as for function expressions and arrow functions.

  2. A function or method that is declared async can have no declared return type, a shorthand form of a return type or an explicitly declared return type.

    1. The explicitly declared return type is of the form Promise<R,E> where R is the type of all return statements in the body, and E is the type of exceptions that are thrown in the body.

    2. The shorthand form only declares the type of R which implicitly translates to Promise<R,?> as the actual return type.

    3. In case no return type is declared, the type R of Promise<R,?> is inferred from the body.

  3. A function or method f with a declared return type R that is declared async has an actual return type of

    1. R if R is a subtype of Promise<?,?>,

    2. Promise<undefined,?> if R is type void.

    3. Promise<R,?> in all other cases (i.e. the declared return type R is being wrapped in a Promise).

  4. Return type inference is only performed when no return type is declared.

    1. The return type R of Promise<R,?> is inferred either as void or as any.

  5. Given a function or method f that is declared async with a declared return type R, or with a declared return type Promise<R,?>, all return statements in f must have an expression of type R (and not of type Promise<R,?>).

  6. await can be used in expressions directly enclosed in an async function, and behaves like a unary operator with the same precedence as yield in ES6.

  7. Given an expression expr of type T, the type of (await expr) is inferred to T if T is not a Promise, or it is inferred to S if T is a Promise with a success value of type S, i.e. T <: Promise<S,?> .

In other words, the return type R of async functions and methods will always be wrapped to Promise<R,?> unless R is a Promise already. As a consequence, nested Promises as a return type of a async function or method have to be stated explicitly like Promise<Promise<R,?>,?>.

When a type variable T is used to define the the return type of an async function or method, it will always be wrapped. Consider the example below.

Example 69. Type variables with async methods.
interface I<T> {
	async foo(): T;  // amounts to foo(): Promise<T,?>
}
function snafu(i1: I<int>, i2: I<Promise<int,?>>) {
	i1.foo();  // returns Promise<int,?>
	i2.foo();  // returns Promise<Promise<int,?>,?>
}
6.4.1.3. Asynchronous Arrow Functions

An await expression is allowed in the body of an async arrow function but not in the body of a non-async arrow function. The semantics here are intentional and are in line with similar constraint for function expressions.

6.5. N4JS Extended Function Definition

6.5.1. Generic Functions

A generic function is a function with a list of generic type parameters. These type parameters can be used in the function signature to declare the types of formal parameters and the return type. In addition, the type parameters can be used in the function body, for example when declaring the type of a local variable.

In the following listing, a generic function foo is defined that has two type parameters S and T. Thereby S is used as to declare the parameter type Array<S> and T is used as the return type and to construct the returned value in the function body.

Generic Function Definition
function <S,T> foo(s: Array<S>): T { return new T(s); }

If a generic type parameter is not used as a formal parameter type or the return type, a warning is generated.

6.5.2. Promisifiable Functions

In many existing libraries, which have been developed in pre-ES6-promise-API times, callback methods are used for asynchronous behavior. An asynchronous function follows the following conventions:

'function' name '(' arbitraryParameters ',' callbackFunction ')'

Usually the function returns nothing (void). The callback function usually takes two arguments,in which the first is an error object and the other is the result value of the asynchronous operation. The callback function is called from the asynchronous function, leading to nested function calls (aka ’callback hell’).

In order to simplify usage of this pattern, it is possible to mark such a function or method as @Promisifiable. It is then possible to ’promisify’ an invocation of this function or method, which means no callback function argument has to be provided and a will be returned. The function or method can then be used as if it were declared with async. This is particularly useful in N4JS definition files (.n4jsd) to allow using an existing callback-based API from N4JS code with the more convenient await.

Example 70. Promisifiable

Given a function with an N4JS signature

f(x: int, cb: {function(Error, string)}): void

This method can be annotated with Promisifiable as follows:

@Promisifiable f(x: int, cb: {function(Error, string)}): void

With this annotation, the function can be invoked in four different ways:

f(42, function(err, result1) { /* ... */ });            // traditional
var promise: Promise<string,Error> = @Promisify f(42);  // promise
var result3: string = await @Promisify f(42);           // long
var result4: string = await f(42);                      // short

The first line is only provided for completeness and shows that a promisifiable function can still be used in the ordinary way by providing a callback - no special handling will occur in this case. The second line shows how f can be promisified using the @Promisify annotation - no callback needs to be provided and instead, a Promise will be returned. We can either use this promise directly or immediately await on it, as shown in line 3. The syntax shown in line 4 is merely shorthand for await @Promisify, i.e. the annotation is optional after await.

Req. IDE-87: Promisifiable (ver. 1)

A function or method f can be annotated with @Promisifiable if and only if the following constraints hold:

  1. Last parameter of f is a function (the callback).

  2. The callback has a signature of

    • {function(E, T0, T1, …​, Tn): V}, or

    • {function(T0, T1, …​, Tn): V}

      in which E is type Error or a subtype thereof, T0,...,Tn are arbitrary types except or its subtypes. E, if given, is then the type of the error value, and T0,...,Tn are the types of the success values of the asynchronous operation.
      Since the return value of the synchronous function call is not available when using @Promisify, V is recommended to be void, but it can be any type.

  3. The callback parameter may be optional.[46]

According to [Req-IDE-87], a promisifiable function or method may or may not have a non-void return type, and that only the first parameter of the callback is allowed to be of type Error, all other parameters must be of other types.

A promisifiable function f with one of the two valid signatures given in [Req-IDE-87] can be promisified with Promisify or used with await, if and only if the following constraints hold:

  1. Function f must be annotated with @Promisifiable.

  2. Using @Promisify f() without await returns a promise of type Promise<S,F> where

    • S is IterableN<T0,…​,Tn> if n2, T if n=1, and undefined if n=0.

    • F is E if given, undefined otherwise.

  3. Using await @Promisify f() returns a value of type IterableN<T0,…​,Tn> if n2, of type T if n=1, and of type undefined if n=0.

  4. In case of using an await, the annotation can be omitted.
    I.e., await @Promisify f() is equivalent to await f().

  5. Only call expressions using f as target can be promisified, in other words this is illegal:

    var pf = @Promisify f; // illegal code!

7. Conversions and Reflection

7.1. Autoboxing and Coercing

Coercing is the ability to implicitly cast one (primitive) type to another. Autoboxing is a special kind of coercing in that is the ability to automatically convert a primitive value type, such as string, number, or boolean, to its corresponding Object type version String, Number, Boolean. The capital letters in the latter are an essential distinction.

Conversion between primitives and object-representations of a datatype are not automatic in N4JS. Only in the cases of object-method invocations on a primitive type (for string to call "abc".length, for example) automatic conversion is applied.

Note that N4JS specific primitive types pathselector and i18nkey are handled similarly to string.

7.1.1. Coercing

In [ECMA11a], coercing is defined by means of the abstract specification method ToPrimitive [ECMA11a(p.S9.1)], also see [ECMA11a(p.S9.10)]). Other conversions, such as ToNumber, are not directly supported but reflected in the typing rules of expressions.

We express absence of automatic coercion here by means of subtype relations:

ΓBoolean:booleanΓboolean:BooleanΓNumber:numberΓnumber:NumberΓString:stringΓstring:String

and for the N4JS specific types:

ΓpathSelector<T><:string
Γi18nKey<:string

If a conversion between primitive and object type is desired, we require the user of N4JS to actively convert the values. The reason for that is the notably different behavior of object- and primitive-variants of a type in expression evaluation:

var bool: boolean = false;
var Bool: Boolean = new Boolean( false );

console.log( bool ? "true" : "false"); // prints "false"
console.log( Bool ? "true" : "false"); // prints "true"!

Conversion between a primitive type to its object-variant is achieved by the new operator. The valueOf() method converts the object-variant back to a primitive value.

// objects from literals:
var bo: Boolean = new Boolean( true ); // typeof bo: object
var no: Number = new Number( 42 ); // typeof no: object
var so: String = new String( "foo" ); // typeof so: object

// to primitive
var b: boolean = bo.valueOf(); // typeof b: boolean -- true
var n: number = no.valueOf(); // typeof n: number -- 42
var s: string = so.valueOf(); // typeof s: string -- "foo"

// to object-type
bo = new Boolean( b );
no = new Number( n );
so = new String( s );

Conversion of variables of type Object or from one primitive type to another is expressed in terms of typing rules for expressions. That is, it is not possible to convert any Object to a primitive in general, but it is possible to do so in the context of certain expressions such as additive operator. The applied conversions are described in Auto-Conversion of Class Instances

7.1.2. Autoboxing of Primitives

In [ECMA11a], autoboxing is defined by ToObject [ECMA11a(p.S9.9)].

Autoboxing is not directly supported in N4JS. Instead, primitive types virtually have the same members as their corresponding object types. It is then possible to use the Autoboxing feature when calling a member. In general, Autoboxing is only supported for accessing built-in read-only (immutable) properties. For example, "some string value".split(" "); is supported but "some string value".foo=1; will be rejected as String does not allow properties to be added (cf. String vs. String+, see [_dynamic]).

Autoboxing often leads to problems, in particular in combination with dynamic types – this is why it is not directly supported in N4JS.

var s: String+ = "Hello"; (1)

s.prop = 1;
console.log(s.prop); (2)
1 will produce an error to prevent the following scenario:
2 prints "undefined"

7.1.3. Autoboxing of Function Expressions and Declarations

Function expressions and declarations always define an object of type Function, thus coercing or Autoboxing is not required in case of functions:

It is always possible to use a function expression where a Function is required, and to use an object of type Function where a function expression is expected. This is only possible if the function signatures are subtype-compatible, see Function Type for details.

Still, it is always possible to call members of Function, e.g., function(){}.length().

7.2. Auto-Conversion of Objects

7.2.1. Auto-Conversion of Class Instances

All classes defined in N4JS modules implicitly subclass N4Object, which is a plain JavaScript Object type. The same auto-conversion rules defined for JavaScript Object therefore apply to N4Object instances as well.

The basic conversion uses the abstract JavaScript function ToPrimitive [ECMA11a(p.S9.1)], which relays on the specification method Object [ECMA11a(p.S8.12.8)]. DefaultValue calls valueOf or toString methods if they are defined by the class (in the methods-builder).

Note that according to the [ECMA11a], in most cases, objects are first converted into primitives. That is, in most cases, no extra hint is passed to DefaultValue. Thus valueOf usually takes precedence over toString as demonstrated in the following example:

Example 71. Auto-Conversion

Assume some classes and corresponding instances defined as follows:

class A {}
class B{
    @Override public toString(): string { return "MyB"}
}
class C{
    @Override public valueOf(): any { return 10}
}
class D{
    @Override public toString(): string { return "MyD"}
    @Override public valueOf(): any { return 20}
}
var a = new A(), b = new B(), c = new C(), d = new D();

Instances of these classes will be converted as demonstrated as follows:

console.log(a+"");                      // [object Object]
console.log(a+1);                       // [object Object]1

console.log(""+b+"");                   // MyB
console.log(1+b+1);                     // 1MyB1

console.log(c+"");                      // 10
console.log(c+1);                       // 11

console.log(d+"");                      // 20
console.log(d+1);                       // 21
7.2.1.1. Auto-Conversion of Interface Instances

Instances of interfaces actually are instances of classes at runtime. The auto-conversion rules described in Auto-Conversion of Class Instances are applied to instances declared as instances of interfaces as well.

7.2.2. Auto-Conversion of Enum Literals

Enumeration values are objects and thus follow the behavior for ECMAScript Object and Function. They have a custom toString method which returns the name of the enumeration value.

7.3. Type Cast and Type Check

7.3.1. Type Cast

(IDEBUG-56): Casting to TypeVars

Type casts are expressed with the cast expression (as), see Cast (As) Expression for details.

We first define helper rules for the type cast constraints as follows:

μTTEnumClassPrimitiveObjectTypeisCPOETμTClassifierTypeTypeTypeμT.typeRefTypeVariableisCPOETμTInterfaceisInterfaceTμT=TypeVariableT.upperBoundsisBoundTypeVarTμT TEnumPrimitiveμT=ClassT.extensibility=final)isFinalByTypeT

Req. IDE-89: Cast Validation At Compile Time (ver. 1)

Given a type cast expression e in which Γe.expr:S and and target type T, the following constraints must hold:

  1. T must be a classifier, enum, primitive, function type expression, classifier type, type variable, union or intersection type:

    μT{any,Class,Interface,Enum,Primitive,ObjectType,
    FunctionTypeExpression,ClassifierType,TypeVariable,Union,Intersection}

  2. if S is a subtype of T, the cast is unnecessary and a warning will be generated.

  3. if S and T are classes, enums or primitive types, then T must be a subtype of S. This is also true if T is an interface and the type of S cannot have subtypes, or vice versa.

    (isCPOET
    (isCPOES
    μS=IntersectionS'S:isCPOES')
    isInterfaceTisFinalByTypeS
    isFinalByTypeTisInterfaceS)ΓT<:S

  4. if S is a class, enum or primitive type and T is a type-variable, then for each given boundary Tiup of T of type class, enum or primitive S must be a member of the type hierarchy: [47]

    isBoundTypeVarTisCPOES
    TiupT.upperBounds(isCPOETiupΓ(Tiup<:STiup:>S))

  5. if S is a union or intersection type, then the type cast is valid if it is valid for at least one element of S.

  6. if S and T are generics, and if S0=T0, a cast is possible if type arguments are sub- or supertypes of each other: [48]

    μS=ClassifierμT=ClassifierS0=T0
    ( S.typeArgi<:T.typeArgi)( T.typeArgi<:S.typeArgi)

  7. If T is a union type, then the type cast is valid if it is valid for at least one element of T.

  8. If T is an intersection type, then the type cast is valid if it is valid for all elements of T.

any is a supertype of all other types, thus it is always possible to cast a variable of type any to other (non-composed) types.

7.3.2. Type Check

There are basically two ways of testing the type of a variable: typeof and instanceof. N4JS supports type comparison via the ECMAScript instanceof operator. The operator instanceof retains its standard ECMAScript behavior (e.g. checking whether a value is an instance of a constructor function), but has additional functionality when used with N4JS types.

When used with an N4JS class, instanceof also supports checking against an interface. For N4JS enumeration values, it can be used to check whether the value is part of a specific enumeration.

typeof only returns a string with the name of the ECMAScript type, which is Object for all class instances.

N4JS specific string types, that is pathSelector and i18nkey cannot be tested during runtime. These types, therefore, must not be used in instanceof expressions. The same is true for string-based enums and arrays which cannot be tested during runtime, thus string-based enum and array types are not permitted on the right-hand side of instancesof constructs. For all types for which the evaluation result of instanceof could be computed at compile time, the check is unnecessary and thus it is refused by the compiler. Using structural types on the right-hand side of instancesof constructs is also not permitted.

In order to avoid errors at runtime, the instanceof operator defines appropriate constraints, see Relational Expression for details.

Example 72. Type Check Example

Given the following classes and variable:

interface I{}
class S{}
class Sub extends S implements I{}

var x = new Sub();

typeof x will simply return object. The following table shows the difference between plain JavaScript instanceof and N4JS’s instanceof:

Check JavaScript N4JS

x instanceof Sub

true

true

x instanceof S

true

true

x instanceof I

false

true

7.4. Reflection meta-information

All N4JS classes, interfaces and enumerations provide meta-information that is used by the runtime and standard library. All classifiers (including enums) provide meta-information by means of a static getter n4type. Since it is static getter, it is actually an instance getter of the constructor (or classifier) of a type, which is the only way to retrieve that information in case of interfaces. For enums, this can be retrieved from instances as well.

This getter is of type N4Class which is a built-in type just like N4Object. It contains the following members:

fqn

The FQN of the type.

n4superType

The N4Class of the supertype, may be null if supertype is a not an N4Class.

allImplementedInterfaces

List of The FQN of implemented interfaces (transitively but without interfaces implemented by supertype)

get isClass

True if the type is an N4Class.

get isInterface

True if the type is an N4Interface.

7.4.1. Reflection for Classes

The meta-information for classes is available by means of N4Object’s static getter n4type. Since it is static getter, it is actually an instance getter of the constructor of a type.

In addition, the static method of in N4Type is available to retrieve the meta-information for a given instance or constructor. This also allows to retrieve meta-information directly for an instance of some class C without having the constructor of C available, for example because the constructor is not accessible.

Example 73. Reflection with N4class

This example demonstrates how these reflective features are accessed:

class A {}
class B extends A {}
var b = new B();
console.log(B.n4type.fqn);
console.log(b.constructor.n4type.fqn);
console.log(b.constructor.n4type.n4superType.fqn);
console.log(N4Type.of(b));
console.log(N4Type.of(B.n4type).fqn);

Assuming this code is defined in file A, this will output

A.B
A.B
A.A
A.B
N4BuiltInClasses.N4Class

The built-in types N4Object and N4Class are also accessible. They are not defined in a module, thus their FQN returns only their simple name.

Example 74. Reflection with Built-In Types
console.log('N4Object.n4class.fqn:      ' + N4Object.n4class.fqn)
console.log('N4Class.n4class.fqn:       ' + N4Class.n4class.fqn)

class A {}
console.log('A.n4class.fqn:             ' + A.n4class.fqn)
console.log('A.n4class.n4superType.fqn: ' + A.n4class.n4superType.fqn)

Assuming this code is defined in file A, this will output

N4Object.n4class.fqn:      N4Object
N4Class.n4class.fqn:       N4Class
A.n4class.fqn:             A.A
A.n4class.n4superType.fqn: N4Object

Note that classes extending Object do not provide the static n4class getter, hat is

class B extends Object {}
console.log('B.n4class.fqn:             ' + B.n4class.fqn)

would issue an error as cannot be resolved.

Example 75. N4Class.of

The type has a method to retrieve the meta-information from instances (i.e. or enumeration literals using ) without using the constructor.

class C { }
interface I {}  class IImpl implements I {}
enum E { L }

var c: C = new C();
var i: I = new IImpl();
var e: E = E.L;

console.log(C.n4type.fqn);
console.log(N4Class.of(c).fqn);

console.log(I.n4type.fqn);
console.log(N4Class.of(i).fqn);

console.log(E.n4type.fqn);
console.log(N4EnumType.of(e).fqn);

7.4.2. Reflection for Interfaces

The meta-information of an interface X is available via getter n4class defined in the type{X}. This field is of type N4Class as well. Since an interface cannot have a super classs, the property n4superTypes will always be empty. Calling isInterface respectively on the returned N4Class instance will return true.

7.4.3. Reflection for Enumerations

var n: number; var b: boolean; var s: string;

The meta-information for enumerations is available by means of the getter n4class, either statically by using the enumeration type or (in terms of an instance getter) via a literal. Calling isEnum on the returned N4Class instance will return true.

7.5. Conversion of primitive types

Conversion between primitives is given as follows:

var n: number; var b: boolean; var s: string;
From To Conversion Example

string

number

Number…​

n = Number("42");//42

string

boolean

N4Primitives.parseBoolean(…​)

 b=N4Primitives.parseBoolean("false");

number

boolean

Boolean(…​)

b=Boolean(17.5); //true

number

string

Number.toString()

s=42.toString(); //"42"

boolean

number

N4Primitives.toNumber(…​)

n=N4Primitives.toNumber(true);

boolean

string

Boolean.toString()

 s=true.toString();//"true" }

Remarks:

  1. ECMAScript doesn’t define explicit conversion from string content. Implicit handling states all strings with >0==true. N4Primitives.parseBoolean(x) yields true for x.trim().toLowerCase().equals("true")

  2. The call to Boolean(..) for the arguments 0, -0, null, false, NaN, undefined and "" evaluate to false. All other values evaluate to true.

  3. Number has several methods for converting a value to string [ECMA11a(p.S15.7.4)]: toExponential(), to Fixed(), toPrecision().

  4. ECMAScript doesn’t define explicit conversion from boolean to number. Implicit handling states true 1 and false 0, which N4Primitives.toNumber() yields.

8. Expressions

For all expressions, we define the following pseudo properties:

containingExpression

The parent expression, in which an expression is contained, may be null.

containingStatement

The statement in which the expression is (indirectly) contained.

containingFunctionOrAccessor

The function, method, getter or setter in which the expression is (indirectly) contained, may be null.

containingClass

The class in which the expression is (indirectly) contained, may be null.

probableThisTarget

The potential target of a this keyword binding, this is not necessarily the containing class or object literal. In case of instance methods of a class T, this usually is the classifier T; in case of static methods, it is the classifier type type{type}.

container

The direct owner of the expression. z

The expressions and statements are ordered, describing first the constructs available in the 5th edition of ECMA-262, referred to as [ECMA11a] in the following. It is worth noting that the grammar snippets already use newer constructs in some cases.

8.1. ECMAScript 5 Expressions

N4JS supports the same expressions as ECMAScript. The semantics are described in [ECMA11a(p.S11)]. In N4JS, some expressions are extended for supporting the declaration of types, annotations, or parameterized usages. These extensions and type-related aspects as well as specific N4JS constraints are described in this section.

Some operators come in different ’flavors’, that is as binary operator, unary pre- or postfix operators, or assignment operators. For these operators, type constraints are only defined for the binary operator version and the other variants are deduced to that binary version. E.g., ++ and += are deduced to + (and simple assignment).

8.1.1. The this Literal

This section describes the this literal and the semantics of the @This annotation, the type this is described in This Type.

Semantics

Semantics are similar to the original ECMAScript this keyword, see [ECMA11a(p.11.1.1, p.p.63)]) Also see [West06a] and MozillaJSRef

Regarding the location where this may be used, the following restrictions apply:

Req. IDE-173: Valid location for this literal (ver. 1)

The literal may not be used in

  1. the initializer expression of static data fields in classes.

  2. the initializer expression of data fields in interfaces (applies to both static and non-static).

  3. static methods of interfaces and static field accessors of interfaces.

See also [Req-IDE-69].

Note: [Req-IDE-173] also applies for this literals inside arrow expressions in initializers.

The use of this is illustrated with some examples as it can often be confusing. Type inference heuristics and explanations are provided in the next section.

Example 76. This in Unrestricted Mode

In unrestricted mode, this is bound to the receiver. If there is no receiver it is bound to the global object, however, we often do not know exactly what the global object would be.

var name = "global a"; // assume the top level is similar to the global object
this.name; // <-- "global a"
function f() {
    return this.name; // <-- depends on call, usually "global a"
}
var ol1 = {
    name: "John",
    greeting: "Hello " + this.name, // "Hello global a" -- we do not greet John!
}
var ol2 = {
    name: "John",
    f: function() {
        this.name; // usually "John", as we assume f is called like ol2.f()
        var g = function() {
           return this.name; // "global a"
        }
        return g(); // no receiver, this in nested function g will be global scope
    }
}
Example 77. This in strict mode

In strict mode, this is bound to the receiver. If there is no receiver, it is bound to undefined. Thus, we will probably get a lot of errors:

"use strict"
var name = "global a"; // assume the top level is similar to the global object
this.name; // <-- error, this is undefined, there is no receiver
function f() {
    return this.name; // <-- depends on call, usually this produces an error as this is undefined
}
var ol1 = {
    name: "John",
    greeting: "Hello " + this.name, // will produce an error, as this is undefined
}
var ol2 = {
    name: "John",
    f: function() {
        this.name; // usually "John", as we assume f is called like ol2.f()
        var g = function() {
           this.name; // an error, see call below:
        }
        return g(); // no receiver, this in nested function g is undefined
    }
}
Example 78. This in N4JS mode

As in strict mode, this is bound to the receiver and if there is no receiver, it is bound to undefined. So the example above is also true for N4JS mode. Classes behave slightly differently:

class A {
    name = "John";
    greeting  = "Hello " + this.name; // works, in N4JS classes, greeting is "Hello John"

    f() {
        return this.name; // this usually is instance object, similar to object literals.
    }

    g() {
        var h = function() {
            return this.name; // as in object literals: no receiver, no this.
        }
        return h();
    }
}
In N4JS classes, this is always bound to the instance when used in field initialization.
Type Inference

The type is inferred from the this type is bound to. The inference, therefore, has to consider the original semantics as described in [ECMA11a(p.10.4., p.10.4.3, p.p.58)]. In ECMAScript the type of this is unfortunately determined by the function call and not by the function definition:

  • By default, this is bound to the global object [ECMA11a(p.10.4.1.1)]. Unfortunately it is often unknown what the global object will be at run time (e.g., node.js differs from browsers).

  • If a function is called without a receiver, this is bound to

    • the global object or

    • to undefined in strict mode.

  • If a function is called with a receiver,this is bound to the receiver object.

Actually, this is bound to the newly created object if a function is called with the new operator. If a function is known to be invoked with an explicit thisArg (apply() etc.), the @This annotation can be used to explicitly set the this type. This annotation has precedence over otherwise inferred bindings.

In general, the actual this target can not be inferred from the context of the this keyword. A heuristic is defined, however, to compute the probable this type:

  1. If the this keyword is used in some function annotated with an annotation @This, the type specified in the annotation is used. The inferred type is always nominal.

    f="this".containingFunctionOrAccessor
    f.hasAnnotation"@This"T=f.annotation["@This"]Γ"this":T

  2. If the this keyword is used in some instance method of a classifier or in an instance field initializer,this is bound to the T itself. If the this keyword is used in some static method of a classifier T or in a static field initializer, the prototype type (or constructor) of the classifier is used, that is type[T]. In both cases, the target is determined by using the expressions’s pseudo property probableThisTarget. If the this keyword is used in a function expression assigned to an property of an object literal, the type of the object literal is used. Note that usually this is the this type in instance methods, and the this type in static methods.

    T="this".probableThisTargetTnullΓ"this":T
  3. In all other cases: Non-strict mode:

    mode=unrestrictedΓ"this":global

Strict mode and N4JS mode:

modeunrestrictedΓ"this":globalundefined

If the actual this type is defined as a structural type, the structural type information is moved to the this type itself. This is transparent to the user in general but maybe visible in case of error messages. That is to say that the actual this type is always a nominal type. This is indicated by the nominal modifier (cf. [Req-IDE-90] constraints 1 and 2.).

  1. The @This annotation is only allowed on declared functions, function expressions (including arrow functions), methods, and field accessors, i.e. getters and setters, except static members of interfaces.

  2. The type declared by way of @This(..) an annotation of a method or field accessor must be a subtype of the member’s containing classifier.

Req. IDE-92: Single @This Annotation (ver. 1)

It is not allowed to use more then one @This(..) annotation on an element.

Example 79. Effect of Nominal This Type

Given the following declaration

@This(~Object with {a: string;}) f() {}

Since the this type is always nominal, ~ Object becomes Object. In case of method call, however, the returned value becomes structural again. In case of error messages the type of the return type is then

~this[Object] with {a: string;}

For the sake of simplicity, additional structural members are usually omitted in error messages, leading to

~this[Object]

instead of

this[~Object]
Example 80. This and Function Declaration

This example demonstrates the usage of functions annotated with @This. By using the argument union{A,B} it is possible to have two completely unrelated classes as the receiver type of the function logger. To pass an actual object the apply() method of the function is used.

class A {
    log: string() { return "A was logged"; }
}

class B {
    log: string() { return "B was logged"; }
}

@This(union{A,B})
function logger() { console.log("~ "+this.log()+" ~"); }


var a: A = new A();
logger.apply(a,[]); // prints "~ A was logged ~"
logger.apply( new B(),[]) // prints "~ B was logged ~"
Example 81. This and Function Expressions

In this example a function is created via a function expression. The function is then assigned to member field of class B. Via annotating the expression with @This(B), access to the receiver of type B is enabled.

class B {
    log(): string { return "B was logged"; }     // method
    logMe : {@This(B) function():void}; // reference to a function
}

var b: B = new B();
b.logMe = @This(B) function() { console.log("*>"+this.log()+"<*"); }
b.logMe(); // prints "*>B was logged<*"

Note that if a function is called as a constructor function with new, the type of this can be declared via annotation @This(..), as shown in the following snippet:

@This(
    ~Object with {
        w: number; h: number;
        area: {function():number};
    })
function Box(w: number w, h: number) {
    this.w = w;
    this.h = h;
    this.area = @This(
        ~Object with {
            w: number; h: number;
            area: {function():number};
        }) function() { return this.w * this.h }
}
var bError = Box(1,2)
var bOK = new Box(1,2)

Inside the constructor function Box, this is bound to the structural type definition due to the annotation.

Inside the nested function area, this is bound to the receiver object (if the function is called like bOk.area()). Again, this depends on the way the nested function is called, which can usually not be determined at the declaration location. The nested function must then be annotated accordingly.

When calling this function, the type of this is checked against the declared this type, which would cause an error in the first case.

The use of the @This annotation is not allowed on methods.

Using constructor functions is not recommended and an error or warning will be created. This is only useful for adapting third-party library code. Even in the latter case, it would probably make more sense to declare a (library) class Rectangle rather then defining the constructor function.

8.1.2. Identifier

Syntax

Identifiers as expressions are identifier references. They are defined as follows:

IdentifierRef <Yield>:
    id=[types::IdentifiableElement|BindingIdentifier<Yield>]
;

BindingIdentifier <Yield>:
    IDENTIFIER
    | <!Yield> 'yield'
    | N4Keyword
;
Semantics

The type of an identifier i is resolved depending on its binding and scope respectively (cf. [ECMA11a(p.10.2.2.1GetIdentifierReference, p.p.56)]. The following scopes (aka Lexical Environments) are defined:

  • function local; local variables, parameters

  • zero or more function closure in case of nested functions

  • module

  • global

These scope are nested as illustrated in Scopes.

Note that classes definitions and object literal do not define a scope: members of a class or properties of an object literal are to be accessed via this. Identifier references always reference declared elements, that is to say either variable, function, or class declarations. Properties of object literals or members of a class are referenced via PropertyAccess-Expression.property (see Property Accessors).

scopes
Figure 7. Scopes

An identifier may be bound to a variable (global or local variable, parameter, variable defined in a function’s closure), or to a property of an object. The latter case is known as property access as further described in Property Accessors.

Req. IDE-93: Read Access to Identifier (ver. 1)

If an identifier i is accessed, the bound declared element D must be readable if it is not used on the left-hand side of an assignment expression.

bindiD
 AssignmentExpression aei.container*:
ae.left=i
μae.left=PropertyAccessExpressionae.left.property=i:
D.readable

Type Inference

An identifier reference i is bound to an identifiable element i.id, which is expressed with the function bindii.id. The type of the reference is then inferred as follows:

Γidref.id:TΓIdentifierRef idref:T

8.1.3. Literals

Type Inference

The type of a literal can directly be derived from the grammar. The following axioms are defined for literals:

NullLiteral:null
BooleanLiteral:boolean
NumericLiteral:intornumber
StringLiteral:string
RegularExpressionLiteral:RegExpr

Note that there are no literals specific for pathSelector or i18nkey.

8.1.3.1. Integer Literals

Numeric literals representing integers in the range of JavaScript’s int32 are inferred to the built-in primitive type int instead of number. The following rules apply:

Req. IDE-94: Numeric literals (ver. 1)

  • Numeric literals with a fraction or using scientific notation, e.g. 2.0 and 2e0, respectively, are always inferred to number, even if they represent integers in the range of int32.

  • Numeric literals that represent integers in the range of JavaScript’s int32, i.e. from -231 to 231-1, are inferred to int.

  • Hexadecimal and octal literals are always interpreted as positive numbers, so all values above 0x7fffffff and 017777777777 lie outside the range of int32 and will thus be inferred to number; this is an important difference to Java. See below for further elaboration.

There are differences to numeric literals in Java:

Java JavaScript N4JS

Literal

Value

Type

Value

Type

2147483648

-2147483648

int

-2147483648

int

2147483647

2147483647

int

2147483647

int

0x7fffffff

2147483647

int

2147483647

int

0x80000000

-2147483648

int

+2147483648

number

0xffffffff

-1

int

4294967295

number

0x100000000

n/a

4294967296

number

017777777777

2147483647

int

2147483647

int

020000000000

-2147483648

int

+2147483648

number

037777777777

-1

int

4294967295

number

040000000000

0

int

4294967296

number

0100000000000

n/a

8589934592

number

The literals 0x100000000 and 0100000000000 produce a syntax error in Java.

Until IDE-1881 is complete, all built-in operations always return a number even if all operands are of type int. For the time being, we therefore interpret -1 as a negative integer literal (inferred to int), but -(1) as the negation of a positive integer literal (inferred to number).

8.1.4. Array Literal

Syntax
ArrayLiteral <Yield> returns ArrayLiteral:
    {ArrayLiteral} '['
        elements+=ArrayPadding* (
            elements+=ArrayElement<Yield>
            (',' elements+=ArrayPadding* elements+=ArrayElement<Yield>)*
            (trailingComma?=',' elements+=ArrayPadding*)?
        )?
    ']'
;

/**
 * This array element is used to pad the remaining elements, e.g. to get the
 * length and index right
 */
ArrayPadding returns ArrayElement: {ArrayPadding} ',';

ArrayElement <Yield> returns ArrayElement: {ArrayElement} spread?='...'? expression=AssignmentExpression<In=true,Yield>;
Type Inference

In general, an array literal is inferred as Array<T> (similar to the type of new Array()). The interesting question is the binding of the type variable T.

The type of an array padding p is inferred as follows:

Γp:undefined

The element type of an array literal is simply inferred as the (simplified) union of the type elements of the array. Thus, the type of an array literal a is inferred as follows:

Γa.elements¯:Te¯T=Te¯Γa:Array<T>

In other languages not supporting union types, the element type is often inferred as the join (LCST) of the element types. Using a union type here preserves more information (as the actual types are still known). For many use cases the behavior is similar though, as the members of a union type are the members of the join of the elements of the union.

Note that typeof [1,2,3] does not return Array<number> (as ECMAScript is not aware of the generic array type), but Object.

Example 82. Array Type Inference

The type for all variables declared in this example is inferred to Array<string>:

var names1          = ["Walter", "Werner"];
var names2          = new Array("Wim", "Wendelin");
var names3          = new Array<string>(3); // length is 3
var names4: Array<string>;

Empty array literals are inferred to any, by default. We are not using Array<?> here because then a typical JavaScript pattern would no longer be supported:

var a = [];
a.push('hello'); (1)
1 This would fail if a and thus [] were inferred to Array<?>
An important exception; if a type expectation exists for the empty array literal and the expected type is Array<T>, this will be used as the type of the array literal.

Req. IDE-95: Empty array literal (ver. 1)

An empty array literal will be inferred as follows:

  • If there is a type expectation for the empty array literal and the expected type is Array<T>, for any type T, then the type of the empty array literal will be inferred to Array<T>.

  • Otherwise, the type of the empty array literal will be inferred to Array<any>.

8.1.5. Object Literal

In addition to ordinary Javascript object literals, N4JS supports the spread operator within object literals as introduced in [ECMA18a].

Syntax

Cf. [ECMA11a(p.S11.1.5, p.p.65ff)] The syntax of an object literal is given by:

ObjectLiteral <Yield>: {ObjectLiteral}
    '{'
        ( propertyAssignments+=PropertyAssignment<Yield>
          (',' propertyAssignments+=PropertyAssignment<Yield>)* ','?
        )?
    '}'
;

PropertyAssignment <Yield>:
      PropertyNameValuePair<Yield>
    | PropertyGetterDeclaration<Yield>
    | PropertySetterDeclaration<Yield>
    | PropertyMethodDeclaration<Yield>
    | PropertyNameValuePairSingleName<Yield>
    | PropertySpread<Yield>
;


PropertyMethodDeclaration <Yield>:
    => ({PropertyMethodDeclaration}
        annotations+=Annotation*
        TypeVariables? returnTypeRef=TypeRef?
            (
                generator?='*'  LiteralOrComputedPropertyName<Yield> ->MethodParamsAndBody<Generator=true>
                | LiteralOrComputedPropertyName<Yield> ->MethodParamsAndBody <Generator=false>
            )
        )
    ';'?
;

PropertyNameValuePair <Yield>:
    => (
        {PropertyNameValuePair}
        annotations+=Annotation*
        declaredTypeRef=TypeRef? LiteralOrComputedPropertyName<Yield> ':'
    )
    expression=AssignmentExpression<In=true,Yield>
;

/*
 * Support for single name syntax in ObjectLiteral (but disallowed in actual object literals by ASTStructureValidator
 * except in assignment destructuring patterns)
 */
PropertyNameValuePairSingleName <Yield>:
    declaredTypeRef=TypeRef?
    identifierRef=IdentifierRef<Yield>
    ('=' expression=AssignmentExpression<In=true,Yield>)?
;

PropertyGetterDeclaration <Yield>:
    =>(
        {PropertyGetterDeclaration}
        annotations+=Annotation*
        GetterHeader<Yield>
    )
    body=Block<Yield=false>
;

PropertySetterDeclaration <Yield>:
    =>(
        {PropertySetterDeclaration}
        annotations+=Annotation*
        'set'
        ->LiteralOrComputedPropertyName <Yield>
    )
    '(' fpar=FormalParameter<Yield> ')' body=Block<Yield=false>
;

PropertySpread <Yield>:
	'...' expression=AssignmentExpression<In=true,Yield>
;
import Address from "my/Address";
var simple = {name: "Walter", age: 72, address: new Address()};
8.1.5.1. Properties

PropertyAssignments have common properties of PropertyNameValuePair, PropertyGetterDeclaration, and PropertySetterDeclaration:

annotations

The annotations of the property assignment.

name

The name of the property. This may be an identifier, a string or a numeric literal. When comparing names, we implicitly assume the name to be converted to an identifier, even if this identifier is not a valid ECMAScript identifier.

declaredType

The declared type of the property which may be null. This property is a pseudo property for PropertySetterDeclaration, in this case it is derived from the declared type of the setter’s formal parameter.

Additionally, we introduce the following pseudo properties to simplify constraints:

isAccessor

The read-only boolean property. This is true if the property assignment is a setter or getter declaration. This is comparable to ECMAScript’s spec function IsAccessoprDescriptor. For a given property assignment p this is semantically equivalent to μp=PropertyGetterDeclarationμp=PropertySetterDeclaration.

isData

The read-only boolean property. This is true if the property assignment is a name value pair. For a given property assignment p this is semantically equivalent to μp=PropertyNameValuePair. It is comparable to ECMAScript’s spec function isDataDescriptor. The equation isAccessor=¬isData is always true.

Semantics

Req. IDE-96: Object literal (ver. 1)

For a given object literal ol the following constraints must hold (cf. [ECMA11a(p.p.66)]:

  • the name of each property 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.

  • Object literal may not have two PropertyNameValuePairs with the same name in strict mode (cf. 4.a [ECMA11a(p.p.66)]):

    mode=strictpaol.propertyAssignments,pa.isData:
    pa'ol.propertyAssignments:
    pa'.isAccessorpa'.name=pa.name

  • Object literal may not have PropertyNameValuePair and PropertyGetterDeclaration/PropertySetterDeclaration with the same name (cf. 4.b/c [ECMA11a(p.p.66)]):

    paol.propertyAssignments,pa.isData:
    pgsdol.propertyAssignments:
    μpgsdPropertyNameValuePairpgsd.name=pa.name

  • Object literal may not have multiple PropertyGetterDeclaration or PropertySetterDeclaration with the same name (cf. 4.d [ECMA11a(p.p.66)]):

    pgol.propertyAssignments,pg.isAccessor:
    pg'ol.propertyAssignmentspg:
    μpg'=μpgpg'.name=pg.name

It is a SyntaxError if the Identifier eval or the Identifier arguments occurs as the Identifier in a PropertySetParameterList of a PropertyAssignment that is contained in strict code or if its FunctionBody is strict code. [ECMA11a(p.p.66)]
  • If two or more property assignments have the same name (and the previous conditions hold), then the types of these assignments must conform. That is to say that the inferred (but not declared) type of all assignments must be type of probably declared types and if the types are explicitly declared, they must be equal.

  • In N4JS mode, the name of a property must be a valid N4JSIdentifier:

    mode=n4jspaol.propertyAssignments:
    μpa.name=N4JSIdentifier

Let E be the expected type of an object literal O as defined by the context in which O is used. If E is not type Object and not dynamic, then the compiler creates a warning O contains properties not found in E.

This is true in particular for object literals passed in as arguments of a spec-constructor.

8.1.5.2. Scoping and linking
Example 83. Scoping and linking
var p = {
    f: function() {
        console.log("p´s f");
    },
    b: function() {
        this.f();
    },
    o: {
        nested: "Hello"
    }
};
p.b();
p.o.nested;
  • Other properties within an object literal property can be accessed using this. In the expression of property name value pairs, however, this is not be bound to the containing object literal, but usually to undefined or global.

  • The properties of an object literal are accessible from outside.

  • Nested properties of an object literal are also accessible from outside.

Type Inference

An object literal implicitly extends ~Object, therefore, object literal types use structural typing. For details see Structural Typing. From a type systems point of view, the two variables ol and st below have the same type.

var ol = {
    s: "hello",
    n: 42
}
var st: ~Object with { s: string; n: number;};

8.1.6. Parenthesized Expression and Grouping Operator

The grouping operator is defined here as a parenthesized expression.

Syntax
ParenExpression <Yield>: '(' expression=Expression<In=true,Yield> ')';
Type Inference

The type of the grouping operator simply is the type of its nested expression. The type if a parenthesized expression pe is inferred as follows:

Γe:TΓ’(’e’)’:T
Example 84. Parenthesized Expression Type Examples

In the following listing, the type of the plain expressions is equivalent to the parenthesized versions:

class A{} class B extends A{}
var f: boolean; var a: A a; var b: B;

/* simple       <->     parenthesized */
10;                     (10);
"hello";                ("hello");
true;                   (true);
a;                      (a);
10-5;                   (10-5);
f?a:b                   (f?a:b);

8.1.7. Property Accessors

Syntax

Property accessors in N4JS are based on [ECMA11a(p.S11.2.1, p.p.67ff)]. They cannot only be used for accessing properties of an object, but also for accessing members of a class instance. In order to support parameterized calls, the syntax is extended to optionally allow type arguments.

ParameterizedPropertyAccessExpression:
    target=PrimaryExpression<Yield> ParameterizedPropertyAccessExpressionTail<Yield>
;

IndexedAccessExpression:
    target=PrimaryExpression<Yield> IndexedAccessExpressionTail<Yield>
;

fragment IndexedAccessExpressionTail <Yield>*:
    '[' index=Expression<In=true,Yield> ']'
;

fragment ParameterizedPropertyAccessExpressionTail <Yield>*:
    '.' TypeArguments? property=[types::IdentifiableElement|IdentifierName]
;

Note that in [ECMA11a], the index access is called bracket notation.

Direct Property Access

We define a special case of property access as follows:

A property access expression is called direct, iff

  • its target is an identifier reference to a class, interface, enum, or the built-in object Symbol, and

  • its property name denotes an owned member of the target classifier (not an inherited, consumed, or polyfilled member) or a literal if the target is an enum.

As a consequence, a direct property access can only refer to static members.

The first requirement of the above definition rules out property access expressions that do not directly point to their target classifier or enum, as shown in the following example:

class C {
  const field = 'hello';
}
C.field;  // direct property access to 'field'
let ctor = C;
ctor.field;  // *not* a direct property access to 'field'

Direct property access is the only form of property access allowed in compile-time expressions, cf. Compile-Time Expressions.

8.1.7.1. Properties

We define the following properties:

target

The receiver of the property access.

index

The index expression in case of an IndexedAccessExpression (returns null otherwise).

property

The name of the property in case of non-indexed-access expressions (returns null otherwise, although the index may be interpreted as property name).

We define the following pseudo properties:

isDotAccess

Read-only boolean property, returns true for non-index access expression (similar to μpIndexedAccessExpression).

isIndexAccess

Read-only boolean property, returns true for index access expression (similar to μp=IndexedAccessExpression.
The equation p.isDotAccess=¬p.isIndexAccess is always true.

name

Returns the name of the property. This is either the property converted to a simple name or the index converted to a name (where possible) if it is an indexed-accessed expression.

Semantics

The parameterization is part of the property access in case of generic methods. For generic functions, a parameterized function call is introduced (cf. Function Calls). The constraints are basically similar.

Req. IDE-97: Property Access and Dot Notation (ver. 1)

  1. If dot notation is used in N4JS mode, the referenced property must exist unless receiver is a dynamic type:

    pae.isDotAccess¬R.dyn mpae.target.type.properties:m.name=pae.name
  2. If dot notation is used and the referenced property exists, then the property must be accessible:

    pae.isDotAccess¬R.dyn mpae.target.type.properties:m.name=pae.nameαpaem
  3. If dot notation is used and the referenced property exists and this property is a member with a declared @This type (only possible for methods or field accessors), then the receiver must be a subtype of the declared @This type.

Req. IDE-98: Index Access (ver. 1)

An index access expression is valid iff one of the following cases applies:

  1. the receiver is of a dynamic type. In this case, the index may be any expression (need not be a compile-time expression).

  2. the receiver is an immediate instance of Object, i.e. it is a subtype of Object and its super types but not of any other type including ~Object and ~~Object.

  3. the receiver is of type Array, ArgumentType, string, or String (including their subtypes) and the index is an expression of type number.

  4. the index expression is a compile-time expression

    • and the receiver type defines a member with a name equal to the string representation of the index expression’s compile-time value

    • and the receiver is not an enum.

Although index access is very limited, it is still possible to use immediate instances of Object in terms of a map (but this applies only to index access, not the dot notation):

Example 85. Object as Map
var map: Object = new Object();
map["Kant"] = "Imperative";
map["Hegel"] = "Dialectic";
map.spinoza = "Am I?";  // error: Couldn't resolve reference to IdentifiableElement 'spinoza'.

Req. IDE-99: Parameterized Property Access (ver. 1)

For a parameterized property access expression pae, the following constraints must hold:

  1. The receiver or target must be a function or method:
    pae.target.type<:Function

  2. The number of type arguments must match the number of type parameters of the generic function or method:
    |pae.typeArgs|=|pae.target.typeVars|

  3. The type arguments of a parameterized property access expression must be subtypes of the boundaries of the parameters of the called generic method.

Also see constraints on read ([Req-IDE-93]) and write ([Req-IDE-121]) access.

Type Inference

We define the following type inferencing rules for property accessors:

  • The type of an indexed-access expression p is inferred as follows:

    ¬p.target.dynp.index.type<:numberΓp.target:Array<T>Γp:TelseΓp:any
  • The type of a property access expression is inferred as follows:

    ΓθRexpr.target:RΓexpr.property:TPropertyAccessExpression expr:T
  • The type of a parameterized access expression p is inferred as follows:

    mp.target:m.name=p.nameΓm:TΓp:TΓp:any

8.1.8. New Expression

Syntax
NewExpression: 'new' callee=MemberExpression<Yield> (-> TypeArguments)?
        (=> withArgs?='(' Arguments<Yield>? ')' )?
import Address from "my/Address";

var a = new Address();
// a.type := my/Address

class C<T> {
    constructor(param: T) {}
}
var c = new C<string>("hello");
Semantics

Req. IDE-100: New expression (ver. 1)

Let ne be a new expression, with Γne.callee:C. The following constraints must hold:

  1. The callee must be a constructor type: C<:constructor? or a constructable type.

  2. Let O be the type argument of C, that is C=constructorO. In that case,

    1. O must not be an interface or enum: μCInterfaceEnum

    2. O must not contain any wildcards.

    3. O must not be a type variable.

  3. If C is not a constructor type, it must be a constructable type, that is one of the following:

    Object, Function, String, Boolean,Number, Array, Date, RegExp, Error

    In particular, it must not refer to a primitive type or a defined functions (i.e., subtypes of Function) cannot be used in new-expressions in N4JS.

Remarks:

to 1 The type of an abstract class A is typeA. Or in other words: Only instantiable classes have an inferred type of constructor....

to 2 Even though it is possible to use the constructor type of an abstract class – concrete subclasses with override compatible constructor signature will be subclasses of this constructor.

to 3 It is not possible to refer to union or intersection at that location. So this is not explicitly denied here since it is not possible anyway.

Example 86. Abstract classes and construction

The following examples demonstrates the usage of abstract classes and constructor types, to make the first two constraints more clearer:

/* XPECT_SETUP org.eclipse.n4js.spec.tests.N4JSSpecTest END_SETUP */

abstract class A {}
class B extends A {}

// XPECT errors --> "Cannot instantiate abstract class A." at "A"
var x = new A();
// XPECT noerrors -->
var y = new B();

function foo(ctor : constructor{A}) {
    // XPECT noerrors -->
    return new ctor();
}

// XPECT errors --> "type{A} is not a subtype of constructor{A}." at "A"
foo(A);
// XPECT noerrors -->
foo(B);
Type Inference

The type of a new expression ne is inferred as follows:

Γne.callee:constructorCΓne:C

For classes, constructors are described in Constructor and Classifier Type.

In N4JS it is not allowed to call new on a plain function. For example:

function foo() {}
var x = new foo();

will issue an error.

8.1.10. Function Calls

In N4JS, a function call [ECMA11a(p.S11.2.3)] is similar to a method call. Additionally to the ECMAScript’s CallExpression, a ParameterizedCallExpression is introduced to allow type arguments passed to plain functions.

Syntax
[[function-calls-syntax]]

Similar to [ECMA11a(p.S11.2.3, p.p.68ff)], a function call is defined as follows:

CallExpression <Yield>:
    target=IdentifierRef<Yield>
    ArgumentsWithParentheses<Yield>
;

ParameterizedCallExpression <Yield>:
    TypeArguments
    target=IdentifierRef<Yield>
    ArgumentsWithParentheses<Yield>
;

fragment ArgumentsWithParentheses <Yield>*:
    '(' Arguments<Yield>? ')'
;

fragment Arguments <Yield>*:
    arguments+=AssignmentExpression<In=true,Yield> (',' arguments+=AssignmentExpression<In=true,Yield>)* (',' spread?='...' arguments+=AssignmentExpression<In=true,Yield>)?
    | spread?='...' arguments+=AssignmentExpression<In=true,Yield>
;
Semantics

Req. IDE-101: Function Call Constraints (ver. 1)

For a given call expression f bound to a method or function declaration F, the following constraints must hold:

  • If less arguments are provided than formal parameters were declared, the missing formal parameters must have been declared optional:
    |f.args|<|F.pars||f.args|<i|F.pars|:Fparsi.optional

  • If more arguments are provided than formal parameters were declared, the last formal parameter must have been declared variadic:
    |f.args|>|F.pars|F.pars|F.pars|-1.variadic

  • Types of provided arguments must be subtypes of the formal parameter types:
    0<i<min|f.args||F.pars|:f.argsi<:F.parsi

  • If more arguments are provided than formal parameters were declared, the type of the exceeding arguments must be a subtype of the last (variadic) formal parameter type:
    |F.pars|<i|f.args|:f.argsi<:F.pars|F.pars|-1

  • The number of type arguments in a parameterized call expression must be equal to the number of type parameters of the generic function / method and the type arguments must be subtypes of the corresponding declared upper boundaries of the type parameters of the called generic function.

Note that (for a limited time), constraints [Req-IDE-101] and [Req-IDE-102] are not applied if the the type of F is Function. See Function-Object-Type.

Type Inference

A call expression expr is bound to a method (Methods) or function declaration (which may be part of a function definition (Function Declaration or specified via a function type Function Type) F (via evaluation of MemberExpression. The type of the call is inferred from the function declaration or type F as follows:

bindexpr.targetFF.returnType:TΓexpr:T

A generic method invocation may be parameterized as well. This is rarely required as the function argument types are usually inferred from the given arguments. In some cases, for instance with pathSelectors, this is useful. In that case, the type variable defined in the generic method declaration is explicitly bound to types by using type arguments. See Property Accessors for semantics and type inference.

Example 87. Generic Method Invocation

This examples demonstrate how to explicitly define the type argument in a method call in case it cannot be inferred automatically.

class C {
    static <T> foo(p: pathSelector<T>): void {..}
};
C.<my.Address>foo("street.number");

Note that in many cases, the type inferencer should be able to infer the type automatically. For example, for a method

function <T> bar(c: T, p: pathSelector<T>): void {..};

and a function call

bar(context, "some.path.selector");
[source,n4js]

the type variable T can be automatically bound to the type of variable context.

8.1.11. Postfix Expression

Syntax
PostfixExpression returns Expression: LeftHandSideExpression
         (=>({PostfixExpression.expression=current} /* no line terminator here */ op=PostfixOperator))?
    ;
enum PostfixOperator: inc='++' | dec='--';
Semantics and Type Inference

The type inference and constraints for postfix operators ++ and --, cf. [ECMA11a(p.S11.3.1, p.p.70)], [ECMA11a(p.S11.3.1, p.p.70)], are defined similarly to their prefix variants (unary expressions), see Unary Expression.

Req. IDE-103: Postfix Expression Constraints (ver. 1)

For a given postfix expression u u with u.op++-- and u.expression.type:T, the following constraints must hold:

  • In N4JS mode, the type T of the expression must be a number.

  • If u.expression=PropertyAccesspappa.isDotAccess both get p and set p must be defined.

8.1.12. Unary Expression

Syntax

We define the following unary operators and expression, similar to [ECMA11a(p.p.70ff)]:

UnaryExpression returns Expression:
      PostfixExpression
    | ({UnaryExpression} op=UnaryOperator expression=UnaryExpression);
enum UnaryOperator: delete | void | typeof | inc='++' | dec='--' | pos='+' | neg='-' | inv='$\sim$' | not='!';
Semantics

For semantics of the delete operator, see also [MozillaJSRef]

Req. IDE-104: Delete Operator Constraints (ver. 1)

For a given unary expression u with u.op=delete, the following constraints must hold:

  • In strict mode, u.expression must be a reference to a property of an object literal, a member of a class type, or to a property of the global type (i.e., the reference must be bound, and the bound target must not be a variable).

  • In N4JS mode, the referenced property or member must not be declared in the containing type and the containing type reference must be declared dynamic.

Req. IDE-105: Void Operator Constraints (ver. 1)

There are no specific constraints defined for with u.op=void

Req. IDE-106: Typeof Operator Constraints (ver. 1)

There are no specific constraints defined for unary expression u with u.op=typeof.

Req. IDE-107: Increment/Decrement Constraints (ver. 1)

For a given unary expression u u with u.op++-- and u.expression.type:T, the following constraints must hold:

  • If mode is N4JS, the type T of the expression must be a number

    ΓUnaryExpressionExpression:number
  • If u.expression=PropertyAccesspappa.isDotAccess both get p and set p must be defined.

For a given unary expression u u with u.op+- and u.expression.type:T, the following constraints must hold:

  • In N4JS mode, the type T of the expression must be a number:

ΓUnaryExpressionExpression:number

Req. IDE-109: Logical Not Operator Constraints (ver. 1)

There are no specific constraints defined for with u.op=!.

Type Inference

The following operators have fixed types independent of their operand types:

Γ’delete’ expression:boolean
Γ’void’ expression:undefined
Γ’typeof’ expression:string
Γ(’++’—’–’—’+’—’-’—’ ’) expression:number
Γ’!’ expression:boolean

8.1.13. Multiplicative Expression

Syntax
MultiplicativeExpression returns Expression: UnaryExpression
      (=>({MultiplicativeExpression.lhs=current} op=MultiplicativeOperator) rhs=UnaryExpression)*;
enum MultiplicativeOperator: times='*' | div='/' | mod='%';
Semantics

Req. IDE-110: Multiplicative Expression Constraints (ver. 1)

For a given multiplicative expression the following constraints must hold in N4JS mode :

  • The types of the operands may be any type:

    ΓMultiplicativeExpressionExpression:any

If a non-numeric operand is used, the result may be NaN which actually is a number as well.

Type Inference
[[type-inference-9]]

The inferred type of a multiplicative expression always is number:

ΓMultiplicativeExpression:number

8.1.14. Additive Expression

Syntax
AdditiveExpression returns Expression: MultiplicativeExpression
    (=>({AdditiveExpression.lhs=current} op=AdditiveOperator) rhs=MultiplicativeExpression)*;
enum AdditiveOperator: add='+' | sub='-';
Semantics

Req. IDE-111: Additive Expression Constraints (ver. 1)

For a given additive expression the following constraints must hold in N4JS mode:

  • The type of the operand can be any type:

ΓAdditiveExpression eExpression:any

In JavaScript it is possible to subtract two non-numerics, leading to NaN. Also undefined or null may be used. The real difference is what type is to be returned (string or number, see below).

8.1.14.1. Type Inference

The type of an additive expression is usually inferred to number, except for addition which may lead to string as well. The result for the addition operator is only be a number if both operands are numbers, booleans, null, or undefined. Using undefined in an additive expression leads to NaN which actually is a number from the type system’s point of view. Additional analysis may create errors in the latter case though.

We first define two helper rules to simplify the addition operator condition:

Ninnumber, int, boolean, null, undefined:T<:=NnbTnbnbT (μT=Union   ET.typeRefs:mnbEmnbTmnbΓe.lhs:LΓe.rhs:RnbLnbRtoNumexprtoNumΓe.lhs:LΓe.rhs:RmnbLmnbRmayNumexprmayNum

The type of an additive expression e is inferred as follows:

e.op='+'¬toNume¬mayNumeΓe:stringe.op='+'¬toNumemayNumeΓe:unionnumber,stringe.op='+'toNumeΓe:numbere.op'+'Γe:number

That is, if both operands are number, int, boolean, null, or even undefined, then the 'plus' is interpreted as mathematical addition and the result is a number. In other cases the 'plus' is interpreted as string concatenation and the result is a string. In case of union types, the result may be a union of number and string.

Adding two integers (int) leads to a number, since the result may not be represented as an (JavaScript) int anymore.

Example 88. Type of addition expression
1+2;            // number 3
"1"+"2";        // string "12"
"1"+2;          // string "12"
1+true;         // number 2
false+1;        // number 1
"1"+true;       // string "1true"
"1"+null;       // string "1null"
1+null;         // number 1
1+undefined;    // number NaN
"1"+undefined;  // string "1undefined"

Support new Symbol.toPrimitive.

8.1.15. Bitwise Shift Expression

Syntax
Cf. +[+<<ECMA11a,ECMA11a(p.p.76f)>>+]+
ShiftExpression returns Expression: AdditiveExpression
    (=>({ShiftExpression.lhs=current} op=ShiftOperator rhs=AdditiveExpression))*
;

ShiftOperator returns ShiftOperator:
      '>' '>' '>'? // SHR, USHR
    | '<' '<'  // SHL
    ;
Semantics

Req. IDE-112: Bitwise Shift Expression Constraints (ver. 1)

For a given bitwise shift expression e the following constraints must hold in N4JS mode: * The types of the operands can be any.

ΓBitwiseShiftExpression  Expression:any
Type Inference

The type returned by a bitwise shift expression is always number:

Γ (Expression (’<<’—’>>’—’>>>’) Expression):number

A non-numeric operand is interpreted as 0, except for true which is interpreted as 1; or objects implementing the symbol toPrimitive.

8.1.16. Relational Expression

Syntax
RelationalExpression returns Expression: ShiftExpression
    (=>({RelationalExpression.lhs=current} op=RelationalOperator) rhs=ShiftExpression)*;

RelationalExpressionNoIn returns Expression: ShiftExpression
    (=>({RelationalExpression.lhs=current} op=RelationalOperatorNoIn) rhs=ShiftExpression)*;

enum RelationalOperator:
    lt='<' | gt='>' | lte='<=' | gte='>=' | instanceof | in;
RelationalOperatorNoIn returns RelationalOperator:
    '<' | '>' | '<=' | '>=' | 'instanceof';
Semantics

For a given relational expression e with e.op{<,>,<=,>=} in N4JS mode, the following constraints must hold:

  • The operands must have the same type and the type must be either a number, string, or boolean:

    Γrhs:TTnumber,string,booleanΓlhs ('<'|'<='|'>'|'>=')rhs  lhs:T
    Γrhs:OOnumber,string,booleanT=unionnumber,string,booleanΓlhs ('<'|'<='|'>'|'>=')rhs  lhs:T
    Γlhs:TTnumber,string,booleanΓlhs ('<'|'<='|'>'|'>=')rhs  rhs:T
    Γlhs:OOnumber,string,booleanT=unionnumber,string,booleanΓlhs ('<'|'<='|'>'|'>=')rhs  rhs:T

Req. IDE-114: Instanceof Operator Constraints (ver. 1)

For a given relational expression e with e.op=instanceof, the following constraints must hold:

  • The right operand of the instanceof operator must be a Function [49]

In other words,

Γlhs ’instanceof’ rhs  rhs:typeClass

is contained in the the first type rule, an object type reference [50] or an enum type reference.

Γlhs ’instanceof’ rhs  rhs:FunctionΓlhs ’instanceof’ rhs  rhs:typeObjectΓlhs ’instanceof’ rhs  rhs:typeN4Enum

The type of a definition site structural classifier C is not of type C. Thus, the instanceof operator cannot be used for structural types. Use-site structural typing is also not possible since ~ would be interpreted (by the parser) as a binary operator.

Req. IDE-115: Operator Constraints (ver. 1)

For a given relational expression e with e.op=in, the following constraints must hold:

  1. The right operand of the in operator must be an Object:

    Γlhs ’in’ rhs  rhs:Object
  2. In N4JS mode, the left operand is restricted to be of type string or number:

    Γlhs ’in’ rhs  lhs:unionstring,number

A special feature of N4JS is support for interface type references in combination with the instance of operator. The compiler rewrites the code to make this work.

Example 89. instanceof with Interface

The following example demonstrates the use of the operator with an interface. This is, of course, not working in pure ECMAScript.

interface I {}

class A implements I {}
class B extends A {}
class C {}

function f(name: string, p: any) {
    if (p instanceof I) {
        console.log(name + " is instance of I");
    }
}

f("A", new A())
f("B", new B())
f("C", new C())

This will print out

A is instance of I
B is instance of I
Type Inference

The type of a relational expression always is boolean;

Γlhs ’¡’|’¡=’|’¿’|’¿=’|’instanceof’|’in’ rhs :boolean

8.1.17. Equality Expression

Syntax
EqualityExpression returns Expression: RelationalExpression
    (=>({EqualityExpression.lhs=current} op=EqualityOperator) rhs=RelationalExpression)*;

EqualityExpressionNoIn returns Expression: RelationalExpressionNoIn
    (=>({EqualityExpression.lhs=current} op=EqualityOperator) rhs=RelationalExpressionNoIn)*;


enum EqualityOperator: same='===' | nsame='!==' | eq='==' | neq='!=';
Semantics

There are no hard constraints defined for equality expressions.

In N4JSmode, a warning is created if for a given expression lhs(’===’—’!==’)rhs, neither Γlhs.upper<:rhs.upper nor Γrhs.upper<:lhs.upper and no interface or composed type is involved as the result is constant in these cases.

Note that a warning is only created if the upper bounds do not match the described constraints. This is necessary for wildcards. For example in

// with
class A{} class B extends A{}
function isFirst(ar: Array<? extends A>, b: B): boolean {
    return b === ar[0]
}

the type of array elements is ? extends A.
Neither ? extends A<:B nor B<:? extends A is true. This is why the upper bounds are to be used.

Type Inference

The inferred type of an equality expression always is boolean.

Γlhs ’==’|’!=’|’===’|’!==’ rhs :boolean

8.1.18. Binary Bitwise Expression

Syntax
BitwiseANDExpression returns Expression: EqualityExpression
    (=> ({BitwiseANDExpression.lhs=current} '&') rhs=EqualityExpression)*;

BitwiseANDExpressionNoIn returns Expression: EqualityExpressionNoIn
    (=> ({BitwiseANDExpression.lhs=current} '&') rhs=EqualityExpressionNoIn)*;

BitwiseXORExpression returns Expression: BitwiseANDExpression
    (=> ({BitwiseXORExpression.lhs=current} '^') rhs=BitwiseANDExpression)*;

BitwiseXORExpressionNoIn returns Expression: BitwiseANDExpressionNoIn
    (=> ({BitwiseXORExpression.lhs=current} '^') rhs=BitwiseANDExpressionNoIn)*;

BitwiseORExpression returns Expression: BitwiseXORExpression
    (=> ({BitwiseORExpression.lhs=current} '|') rhs=BitwiseXORExpression)*;

BitwiseORExpressionNoIn returns Expression: BitwiseXORExpressionNoIn
    (=> ({BitwiseORExpression.lhs=current} '|') rhs=BitwiseXORExpressionNoIn)*;
Semantics

For a given bitwise bitwise expression e the following constraints must hold in N4JS mode:

  • The types of the operands must be both number.

ΓBitwiseBitwiseExpression  Expression:number
Type Inference

The type returned by a binary bitwise expression is always number:

8.1.19. Binary Logical Expression

Syntax
LogicalANDExpression returns Expression: BitwiseORExpression
    (=> ({LogicalANDExpression.lhs=current} '&&') rhs=BitwiseORExpression)*;
LogicalANDExpressionNoIn returns Expression: BitwiseORExpressionNoIn
    (=> ({LogicalANDExpression.lhs=current} '&&') rhs=BitwiseORExpressionNoIn)*;

LogicalORExpression returns Expression: LogicalANDExpression
    (=> ({LogicalORExpression.lhs=current} '||') rhs=LogicalANDExpression)*;
LogicalORExpressionNoIn returns Expression: LogicalANDExpressionNoIn
    (=> ({LogicalORExpression.lhs=current} '||') rhs=LogicalANDExpressionNoIn)*;
Semantics

Req. IDE-117: Binary Logical Expression Constraints (ver. 1)

For a given binary logical expression e with e.lhs.type:L and e.rhs.type:R the following constraints must hold:

  • In N4JS mode L must not be undefined or null.

Type Inference

The evaluation relies on ECMAScript’s abstract operation ToBoolean [ECMA11a(p.p.43)]. A short-circuit evaluation strategy is used so that depending on the types of the operands, different result types may be inferred. In particular, the inferred type usually is not boolean ((cf. [ECMA11a(p.S11.11., p.p.83ff)] ). The type inference does not take this short-circuit evaluation strategy into account, as it will affect the result in case one of the types is null either or undefined, which is not allowed in N4JS mode.

Γlhs&&’—’——’rhs:unionΓlhsΓrhs

8.1.20. Conditional Expression

Syntax
ConditionalExpression returns Expression: LogicalORExpression
    (=> ({ConditionalExpression.expression=current} '?') trueExpression=AssignmentExpression  ':' falseExpression=AssignmentExpression)?;

ConditionalExpressionNoIn returns Expression: LogicalORExpressionNoIn
    (=> ({ConditionalExpression.expression=current} '?') trueExpression=AssignmentExpression  ':' falseExpression=AssignmentExpressionNoIn)?;
Semantics

Req. IDE-118: Conditional Expression Constraints (ver. 1)

For a given conditional expression e with

e.expression.type:C,
e.trueExpression.type:T,
e.false-Expression.type:F

the following constraints must hold:

  • A warning will be issued in N4JSmode if e.expression evaluates to a constant value. That is to say
    e.expressionfalsetruenullundefined or Cvoidundefined.

There are no specific constraints defined for the condition. The ECMAScript operation ToBoolean [ECMA11a(p.S9.2, p.p.43)] is used to convert any type to boolean.

Type Inference

The inferred type of a conditional expression is the union of the true and false expression (cf. [ECMA11a(p.S11.12, p.p.84)] ():

T=unionΓet,ΓefΓcond ’?’et ’:’ef:T
Example 90. Type of Conditional Expressions

Given the following declarations:

class A{}       class B extends A{}
class C{}       class D extends A{}
class G<T> { field: T; }

var ga: G<A>, gb: G<B>;
    a: A, b: B, c: C, d: D;
var boolean cond;

Then the type of the following conditional expression is inferred as noted in the comments:

cond ? a : a;                           // A
cond ? a : b;                           // union{A,B}
cond ? a : c;                           // union{A,C}
cond ? b : d;                           // union{B,D}
cond ? (cond ? a : b) : (cond ? c : d); // union{A,B,C,D}
cond ? (cond ? a : b) : (cond ? b : d); // union{A,B,D}
cond ? ga : gb;                         // union{G<A>,G<B>}

8.1.21. Assignment Expression

Syntax
AssignmentExpression <In, Yield>:
    lhs=Expression op=AssignmentOperator rhs=AssignmentExpression<In,Yield>
;
AssignmentOperator:
      '='
    | '*=' | '/=' | '%=' | '+=' | '-='
    | '<<=' | '>>=' | '>>>='
    | '&=' | '^=' | '|='
;
Semantics

Req. IDE-119: Simple Assignment (ver. 1)

For a given assignment assignment with

assignment.op=’=’

the following constraints must hold:

  1. [[assignment.lhs]]<:[[assignment.rhs]]

    In the following inference rule and the constraint, ’@’ is to be replaced with the right part of one of the assignment operators listed above, that is,

Req. IDE-120: Compound Assignment (ver. 1)

For a given assignment left op right, with op=’@=’ but not +=, both, left and right must be subtypes of number.
For operator +=,

  • if the left-hand side is a number, then left ’+’right must return a number as well. The right-hand side must, in fact, be a number (and not a boolean) here in order to avoid unexpected results.

  • if the left-hand side is a string, then left’+’right must return a string as well. That means that the right-hand side can be of any type.

The expected type for the left-hand side is union{number,string}.

The basic idea behind these constraints is that the type of the left-hand side is not to be changed by the compound assignment.

Req. IDE-121: Write Acccess (ver. 1)

For a given assignment expression assignExpr, the left-hand side must be writeable or a final data field and the assignment must be in the constructor. Let v be the bound variable (or field) with bindassignExpr.leftv

v.writeablev.finalv.expr=nullassignExpr.containingFunction=v.owner.constructorμassignExpr.left=PropertyAccessassignExpr.left.target="this"

The value of writeable is true for setters and usually for variables and data fields. Assignability of variables and data fields can be restricted via const or the @Final annotation. See Assignment Modifiers(data fields) and Const (const variables) for details.

Also see [Req-IDE-93] for read access constraint.

The left-hand side of an assignment expression may be an array or object literal and the assignment expression is then treated as a destructuring assignment. See Array and Object Destructuring for details.

Type Inference

Similarly to [ECMA11a(p.S11.1, p.p.84ff)], we define type inference for simple assignment (=) and compound assignment (op=) individually.

The type of the assignment is simply the type of the right-hand side:

Γright:TΓleft ’=’right:T

Compound assignments are reduced to the former by splitting an operator @=, in which @ is a simple operator, into a simple operator expression with operator @ and a simple assignment =. Since the type of the latter is the right-hand side, we can define:

Γleft ’@’right:TΓleft ’@=’right:T

8.1.22. Comma Expression

Syntax
CommaExpression <In, Yield>:
    exprs+=AssignmentExpression<In,Yield> ',' exprs+=AssignmentExpression<In,Yield>
    (','    exprs+=AssignmentExpression<In,Yield>)*
;
Semantics

All expressions will be evaluated even though only the value of the last expression will be the result.

Example 91. Comma Expression

Assignment expressions preceed comma expressions:

var b: boolean;
b = (12, 34, true); // ok, b=true
b =  12, 34, true ; // error, b=12 is invalid
Type Inference

The type of a comma expression cexpr is inferred to the last expression:

n=|cexpr.exprs|,Γcexpr.exprsn:TΓcexpr:T

8.2. ECMAScript 6 Expressions

8.2.1. The super Keyword

SuperLiteral: {SuperLiteral} 'super';

Apart from the use of keyword super in wildcards of type expressions (cf. Type Expressions), there are two use cases for keyword super: super member access and super constructor calls.

Example 92. Super Keyword

Two use cases for keyword super:

class B extends A {
    constructor() {
        // super call
        super();
    }
    @Override
    m();: void {
        // super member access
        super.m();
    }
}
Semantics

super can be used to access the supertype’s constructor, methods, getters and setters. The supertype is defined lexically, which is different from how this works.[51]

Note that in [ECMA15a] Chapter 12.3.5 The Super Keyword, super is defined as a keyword but the syntax and semantics are defined in conjunction of member access.

Req. IDE-122: Type of Super is Always Nominal (ver. 1)

The type referenced with the super literal is always nominal. This is a consequence of references to types in extend clauses to be nominal.

Γsuper:TT.typingStrategy=nominal

If the super literal s is used to access the super constructor of a class, all of the following constraints must hold:

  1. The super constructor access must be a call expression:

    μcexpr=CallExpressionc.target=cexpr
  2. The super constructor call must be the expression of an expression statement exprStmt:

    exprStmt=cexpr.containerμcexpr.container=ExpressionStatement
  3. The containing statement stmtExpr must be directly contained in a constructor body:

    μexprStmt.containingFunction=Constructor) exprStmt.container=exprStmt.containingFunction.body

  4. There must be no access to and not return statement before the containing statement exprStm