17. Experimental Features

This Chapter introduces the ArkTS features that are considered a part of the language, but have no counterpart in TypeScript, and are therefore not recommended to those in need of a single source code for TypeScript and ArkTS.

Some features introduced in this Chapter are still under discussion. They can be removed from the final version of the ArkTS specification. Once a feature introduced in this Chapter is approved and/or implemented, the corresponding section is moved to the body of the specification as appropriate.

The array creation feature introduced in Array Creation Expressions enables users to dynamically create objects of array type by using runtime expressions that provide the array size. This is a useful addition to other array-related features of the language, such as array literals.

The construct can also be used to create multi-dimensional arrays.

The feature function and method overloading is supported in many (if not all) modern programming languages. Overloading functions/methods is a practical and convenient way to write program actions that are similar in logic but different in implementation.

The ArkTS language supports (as an experimental feature at the moment) two semantically and syntactically different implementations of overloading: the TypeScript-like implementation, and that of other languages. See Function and Method Overloading for more details.

Section Native Functions and Methods introduces practically important and useful mechanisms for the inclusion of components written in other languages into a program written in ArkTS.

Section Final Classes and Methods discusses the well-known feature that in many OOP languages provides a way to restrict class inheritance and method overriding. Making a class final prohibits defining classes derived from it, whereas making a method final prevents it from overriding in derived classes.

Section Extension Functions defines the ability to extend a class or an interface with new functionality without having to inherit from the class. This feature can be used for GUI programming (Support for GUI Programming).

Section Enumeration Methods adds methods to declarations of the enumeration types. Such methods can help in some kinds of manipulations with enums.

Section Exceptions discusses the powerful, commonly used mechanism for the processing of various kinds of unexpected events and situations that break the ‘ordinary’ program logic. There are constructs to raise (”throw”) exceptions, “catch” them along the dynamic sequence of function calls, and handle them. Some support for exceptions is also provided by the classes from the standard library (see Standard Library).

Note: The exceptions mechanism is sometimes deprecated for being too time-consuming and unsafe. Some modern languages do not support the exceptions mechanism as discussed in this section. That is why the expediency of adding this feature to the language is still under discussion.

The ArkTS language supports writing concurrent applications in the form of coroutines (see Coroutines) that allow executing functions concurrently, while the channels through which the coroutines can produce results are asynchronous.

There is a basic set of language constructs that support concurrency. A function to be launched asynchronously is marked by adding the modifier async to its declaration. In addition, any function—or lambda expression—can be launched as a separate thread explicitly by using the launch expression.

The await statement is introduced to synchronize functions launched as threads. The generic class Promise<T> from the standard library (see Standard Library) is used to exchange information between threads. The class can be handled as an implementation of the channel mechanism. The class provides a number of methods to manipulate the values produced by threads.

Section Packages discusses a well-known and proven language feature intended to organize large pieces of software that typically consist of many components. Packages allow developers to construct a software product as a composition of subsystems, and organize the development process in a way that is appropriate for independent teams to work in parallel.

Package is the language construct that combines a number of declarations, and makes them parts of an independent compilation unit.

The export and import features are used to organize communication between packages. An entity exported from one package becomes known to— and accessible in—another package which imports that feature. Various options are provided to simplify export/import, e.g., by defining non-exported, i.e., ‘internal’ declarations that are not accessible from the outside of the package.

In addition, ArkTS supports the package initialization semantics that makes a package even more independent from the environment.

In addition to the notion of generic constructs, the declaration-site variance feature is considered. The idea of the feature is briefly described below, and in greater detail in Declaration-Site Variance.

Normally, two different argument types that specialize a generic class are handled as different and unrelated types (invariance). ArkTS proposes to extend the rule, and to allow such specializations become base classes and derived classes (covariance), or vice versa (contravariance), depending on the relationship of inheritance between argument types.

Special markers are used to specify the declaration-site variance. The markers are to be added to generic parameter declarations.

The practices of some languages (e.g., Scala) have proven the usefulness of this powerful mechanism. However, its practical application can be relatively difficult. Therefore, the addition of this feature to the language is still under consideration.


17.1. Character Type and Literals

17.1.1. Character Literals

A char literal represents the following:

  • A value with a single character; or

  • A single escape sequence preceded by the characters ‘single quote’ (U+0027) and ‘c’ (U+0063), and followed by a ‘single quote’ U+0027).


CharLiteral:
    'c\'' SingleQuoteCharacter '\''
    ;

SingleQuoteCharacter:
    ~['\\\r\n]
    | '\\' EscapeSequence
    ;

The examples are presented below:

1   c'a'
2   c'\n'
3   c'\x7F'
4   c'\u0000'

Character Literals are of type char.


17.1.2. Character Type and Operations

Type

Type’s Set of Values

Corresponding Class Type

char

Symbols with codes from U+0000 to U+FFFF inclusive, that is, from 0 to 65,535

Char

ArkTS provides a number of operators to act on character values as discussed below.

The class Char provides constructors, methods, and constants that are parts of the ArkTS standard library (see Standard Library).


17.2. Array Creation Expressions

An array creation expression creates new objects that are instances of arrays. The array literal expression is used to create an array instance, and to provide some initial values (see Array Literal).

1   newArrayInstance:
2       'new' typeReference dimensionExpression+
3       ;
4
5   dimensionExpression:
6       '[' expression ']'
7       ;
1   let x = new number[2][2] // create 2x2 matrix

An array creation expression creates an object that is a new array with the elements of the type specified by typeReference.

The type of each dimensionExpression must be convertible (see Primitive Types Conversions) to an integer type. Otherwise, a compile-time error occurs.

A numeric conversion (see Primitive Types Conversions) is performed on each dimensionExpression to ensure that the resultant type is int. Otherwise, compile-time error occurs.

A compile-time error occurs if any dimensionExpression is a constant expression that is evaluated at compile time to a negative integer value.

If the type of any dimensionExpression is number or other floating-point type, and its fractional part is different from 0, then errors occur as follows:

  • Runtime error, if the situation is identified during program execution; and

  • Compile-time error, if the situation is detected during compilation.

1   let x = new number[-3] // compile-time error
2
3   let y = new number[3.141592653589]  // compile-time error
4
5   foo (3.141592653589)
6   function foo (size: number) {
7      let y = new number[size]  // runtime error
8   }

A compile-time error occurs if typeReference refers to a class that does not contain an accessible parameterless constructor or constructor with all parameters of the second form of optional parameters (see Optional Parameters), or if typeReference has no a default value:

1   let x = new string[3] // compile-time error: string has no default value
2
3   class A {
4      constructor (p1?: number, p2?: string) {}
5   }
6   let y = new A[2] // OK, as all 3 elements of array will be filled with
7   // new A() objects

17.2.1. Runtime Evaluation of Array Creation Expressions

The evaluation of an array creation expression at runtime is performed as follows:

  1. The dimension expressions are evaluated. The evaluation is performed left-to-right; if any expression evaluation completes abruptly, then the expressions to the right of it are not evaluated.

  2. The values of dimension expressions are checked. If the value of any dimExpr expression is less than zero, then NegativeArraySizeException is thrown.

  3. Space for the new array is allocated. If the available space is not sufficient to allocate the array, then OutOfMemoryError is thrown, and the evaluation of the array creation expression completes abruptly.

  4. When a one-dimensional array is created, each element of that array is initialized to its default value if the type default value is defined (Default Values for Types). If the default value for an element type is not defined, but the element type is a class type, then its parameterless constructor is used to create the value of each element.

  5. When a multi-dimensional array is created, the array creation effectively executes a set of nested loops of depth n-1, and creates an implied array of arrays.


17.3. Indexable Types

If a class or an interface declares one or two functions with names $_get and $_set, and signatures (index: Type1): Type2 and (index: Type1, value: Type2) respectively, then an indexing expression (see Indexing Expression) can be applied to variables of such types:

1 class SomeClass {
2    $_get (index: number): SomeClass { return this }
3    $_set (index: number, value: SomeClass) { }
4 }
5 let x = new SomeClass
6 x = x[1] // This notation implies a call: x = x.$_get (1)
7 x[1] = x // This notation implies a call: x.$_set (1, x)

If only one function is present, then only the appropriate form of the index expression (see Indexing Expression) is available:

 1 class ClassWithGet {
 2    $_get (index: number): ClassWithGet { return this }
 3 }
 4 let getClass = new ClassWithGet
 5 getClass = getClass[0]
 6 getClass[0] = getClass // Error - no $_set function available
 7
 8
 9 class ClassWithSet {
10    $_set (index: number, value: ClassWithSet) { }
11 }
12 let setClass = new ClassWithSet
13 setClass = setClass[0] // Error - no $_get function available
14 setClass[0] = setClass

Type string can be used as a type of the index parameter:

1 class SomeClass {
2    $_get (index: string): SomeClass { return this }
3    $_set (index: string, value: SomeClass) { }
4 }
5 let x = new SomeClass
6 x = x["index string"]
7    // This notation implies a call: x = x.$_get ("index string")
8 x["index string"] = x
9    // This notation implies a call: x.$_set ("index string", x)

Functions $_get and $_set are ordinary functions with compiler-known signatures. The functions can be used like any other function. The functions can be abstract or defined in an interface and implemented later. The functions can be overridden and provide a dynamic dispatch for the indexing expression evaluation (see Indexing Expression). They can be used in generic classes and interfaces for better flexibility.

A compile-time error occurs if these functions are marked as async.

 1 interface ReadonlyIndexable<K, V> {
 2    $_get (index: K): V
 3 }
 4
 5 interface Indexable<K, V> extends ReadonlyIndexable<K, V> {
 6    $_set (index: K, value: V)
 7 }
 8
 9 class IndexableByNumber<V> extends Indexable<number, V> {
10    private data: V[] = []
11    $_get (index: number): V { return this.data [index] }
12    $_set (index: number, value: V) { this.data[index] = value }
13 }
14
15 class IndexableByString<V> extends Indexable<string, V> {
16    private data = new Map<string, V>
17    $_get (index: string): V { return this.data [index] }
18    $_set (index: string, value: V) { this.data[index] = value }
19 }
20
21 class BadClass extends IndexableByNumber<boolean> {
22    override $_set (index: number, value: boolean) { index / 0 }
23 }
24
25 let x: IndexableByNumber<boolean> = new BadClass
26 x[666] = true // This will be dispatched at runtime to the overridden
27    // version of the $_set method
28 x.$_get (15)  // $_get and $_set can be called as ordinary
29    // methods

17.4. Iterable Types

A class or an interface can be made iterable, meaning that their instances can be used in for-of statements (see for-of Statements).

A type is iterable if it declares a parameterless function with name $_iterator and signature (): ITER, where ITER is a type that implements Iterator interface defined in the standard library (see Standard Library).

The example below defines iterable class C:

 1   class C {
 2     data: string[] = ['a', 'b', 'c']
 3     $_iterator() {
 4       return new CIterator(this)
 5     }
 6   }
 7
 8   class CIterator implements Iterator<string> {
 9     index = 0
10     base: C
11     constructor (base: C) {
12       this.base = base
13     }
14     next(): IteratorResult<string> {
15       return {
16         done: this.index >= this.base.data.length,
17         value: this.base.data[this.index++]
18       }
19     }
20   }
21
22   let c = new C()
23   for (let x of c) {
24         console.log(x)
25       }

In the example above, class C function $_iterator returns CIterator<string>, which implements Iterator<string>. If executed, this code prints out the following:

"a"
"b"
"c"

The function $_iterator is an ordinary function with a compiler-known signature. The function can be used like any other function. It can be abstract or defined in an interface to be implemented later.

A compile-time error occurs if this function is marked as async.

Note: To support the code compatible with TypeScript, the name of the function $_iterator can be written as [Symbol.iterator]. In this case, the class iterable looks as follows:

1   class C {
2     data: string[] = ['a', 'b', 'c'];
3     [Symbol.iterator]() {
4       return new CIterator(this)
5     }
6   }

The use of the name [Symbol.iterator] is considered deprecated. It can be removed in the future versions of the language.


17.5. Statements


17.5.1. For-of Type Annotation

An explicit type annotation is allowed for a for variable:

1   // explicit type is used for a new variable,
2   let x: number[] = [1, 2, 3]
3   for (let n: number of x) {
4     console.log(n)
5   }

17.5.2. Multiple Catch Clauses in Try Statements

When an exception or an error is thrown in the try block, or in a throwing or rethrowing function (see Throwing Functions and Rethrowing Functions) called from the try block, the control is transferred to the first ‘catch’ clause if the statement has at least one ‘catch’ clause that can catch that exception or error. If no ‘catch’ clause is found, then exception or error is propagated to the surrounding scope.

Note: An exception handled within a non-throwing function (see Non-Throwing Functions) is never propagated outside that function.

A catch clause has two parts:

  • An exception parameter that provides access to the object associated with the exception or the error occurred; and

  • A block of code that is to handle the situation.

Default catch clause is a ‘catch’ clause with the exception parameter type omitted. Such a ‘catch’ clause handles any exception or error that is not handled by any previous clause. The type of that parameter is of the class Object.

A compile-time error occurs if:

  • The default ‘catch’ clause is not the last ‘catch’ clause in a try statement.

  • The type reference of an exception parameter (if any) is neither the class Exception or Error, nor a class derived from Exception or Error.

 1   class ZeroDivisor extends Exception {}
 2
 3   function divide(a: int, b: int): int throws {
 4     if (b == 0) throw new ZeroDivisor()
 5     return a / b
 6   }
 7
 8   function process(a: int; b: int): int {
 9     try {
10       let res = divide(a, b)
11
12       // further processing ...
13     }
14     catch (d: ZeroDivisor) { return MaxInt }
15     catch (e) { return 0 }
16   }

All exceptions that the try block can throw are caught by the function ‘process’. Special handling is provided for the ZeroDivisor exception, and the handling of other exceptions and errors is different.

‘’Catch’’ clauses do not handle every possible exception or error that can be thrown by the code in the try block. If no ‘catch’ clause can handle the situation, then exception or error is propagated to the surrounding scope.

Note: If a try statement (default catch clause) is placed inside a non-throwing function (see Non-Throwing Functions), then exception is never propagated.

If a ‘catch’ clause contains a block that corresponds to a parameter of the error’, then it can only handle that error.

The type of the ‘catch’ clause parameter in a default catch clause is omitted. The ‘catch’ clause can handle any exceptions or errors unhandled by the previous clauses.

The type of a ‘catch’ clause parameter (if any) must be of the class Error or Exception, or of another class derived from Exception or Error.

1     function process(a: int; b: int): int {
2     try {
3       return a / b
4     }
5     catch (x: DivideByZeroError) { return MaxInt }
6   }

A ‘catch’ clause handles the DivideByZeroError at runtime. Other errors are propagated to the surrounding scope if no ‘catch’ clause is found.


17.5.3. Assert Statements

An assert statement can have one or two expressions. The first expression is of type boolean; the optional second expression is of type string. A compile-time error occurs if the types of the expressions fail to match.

assertStatement:
    'assert' expression (':' expression)?
    ;

Assertions control mechanisms that are not part of ArkTS, yet the language allows having assertions either enabled or disabled.

The execution of the enabled assertion starts from the evaluation of the boolean expression. An error is thrown if the expression evaluates to false. The second expression is then evaluated (if provided). Its value passes as the error argument.

The execution of the disabled assertion has no effect whatsoever.

1   assert p != null
2   assert f.IsOpened() : "file must be opened" + filename
3   assert f.IsOpened() : makeReportMessage()

17.6. Function and Method Overloading

Like the TypeScript language, ArkTS supports overload signatures that allow specifying several headers for a function or method with different signatures. Most other languages support a different form of overloading that specifies a separate body for each overloaded header.

Both approaches have their advantages and disadvantages. The experimental approach of ArkTS allows for improved performance as a specific body is executed at runtime.


17.6.1. Function Overloading

If a declaration scope declares two functions with the same name but different signatures that are not override-equivalent (see Override-Equivalent Signatures), then the function name is overloaded. An overloaded function name causes no compile-time error on its own.

No specific relationship is required between the return types, or between the ‘throws’ clauses of the two functions with the same name but different signatures that are not override-equivalent.

When calling a function, the number of actual arguments (and any explicit type arguments) and compile-time types of arguments is used at compile time to determine the signature of the function being called (see Function Call Expression).


17.6.2. Class Method Overloading

If two methods within a class have the same name, and their signatures are not override-equivalent, then the methods name is considered overloaded.

An overloaded method name cannot cause a compile-time error on its own.

If the signatures of two methods with the same name are not override-equivalent, then the return types of those methods, or the ‘throws’ or ‘rethrows’ clauses of those methods can have any kind of relationship.

A number of actual arguments, explicit type arguments, and compile-time types of the arguments is used at compile time to determine the signature of the method being called (see Method Call Expression, and Step 2: Selection of Method).

In the case of an instance method, the actual method being called is determined at runtime by using the dynamic method lookup (see Method Call Expression) provided by the runtime system.


17.6.3. Interface Method Overloading

If two methods of an interface (declared or inherited in any combination) have the same name but different signatures that are not override-equivalent (see Inheriting Methods with Override-Equivalent Signatures), then such method name is considered overloaded.

However, this causes no compile-time error on its own, because no specific relationship is required between the return types, or between the ‘throws’ clauses of the two methods.


17.6.4. Constructor Overloading

The constructor overloading behaves identically to the method overloading (see Class Method Overloading). Each class instance creation expression (see New Expressions) resolves the overloading at compile time.


17.6.5. Declaration Distinguishable by Signatures

Declarations with the same name are distinguishable by signatures if:

The example below represents the functions distinguishable by signatures:

1   function foo() {}
2   function foo(x: number) {}
3   function foo(x: number[]) {}
4   function foo(x: string) {}

The following example represents the functions undistinguishable by signatures that cause a compile-time error:

1   // Functions have override-equivalent signatures
2   function foo(x: number) {}
3   function foo(y: number) {}
4
5   // Functions have override-equivalent signatures
6   function foo(x: number) {}
7   type MyNumber = number
8   function foo(x: MyNumber) {}

17.7. Native Functions and Methods


17.7.1. Native Functions

A native function implemented in a platform-dependent code is typically written in another programming language (e.g., C). A compile-time error occurs if a native function has a body.


17.7.2. Native Methods

Native methods are the methods implemented in a platform-dependent code written in another programming language (e.g., C).

A compile-time error occurs if:

  • A method declaration contains the keyword abstract along with the keyword native.

  • A native method has a body (see Method Body) that is a block instead of a simple semicolon or empty body.


17.8. Final Classes and Methods


17.8.1. Final Classes

A class may be declared final to prevent its extension. A class declared final cannot have subclasses, and no method of a final class can be overridden.

If a class type F expression is declared final, then only a class F object can be its value.

A compile-time error occurs if the ‘extends’ clause of a class declaration contains another class that is final.


17.8.2. Final Methods

A method can be declared final to prevent it from being overridden (see Overriding by Instance Methods) or hidden in subclasses.

A compile-time error occurs if:

  • A method declaration contains the keyword abstract or static along with the keyword final.

  • A method declared final is overridden.


17.9. Default and Static Interface Methods


17.9.1. Default Method Declarations

interfaceDefaultMethodDeclaration:
    'private'? identifier signature block
    ;

A default method can be explicitly declared private in an interface body.

A block of code that represents the body of a default method in an interface provides a default implementation for any class if such class does not override the method that implements the interface.


17.9.2. Static Method Declarations

interfaceStaticMethodDeclaration:
    'static' 'private'? identifier signature block
    | 'private'? 'static' identifier signature block
    ;

A static method in an interface body can be explicitly declared private.

Static interface method calls refer to no particular object.

In contrast to default methods, static interface methods are not instance methods.

A compile-time error occurs if:

  • The body of a static method attempts to use the keywords this or super.

  • The header or body of a static method of an interface contains the name of any surrounding declaration’s type parameter.


17.10. Extension Functions

The extension function mechanism allows using a special form of top-level functions as extensions of class or interface. Syntactically, extension adds a new functionality.

Extensions can be called in the usual way like methods of the original class. However, extensions do not actually modify the classes they extend. No new member is inserted into a class; only new extension functions are callable with the dot-notation on variables of the class.

Extension functions are dispatched statically; what extension function is being called is already known at compile time based on the receiver type specified in the extension function declaration.

Extension functions specify names, signatures, and bodies:

extensionFunctionDeclaration:
    'static'? 'function' typeParameters? typeReference '.' identifier
    signature block
    ;

The keyword this inside an extension function corresponds to the receiver object (i.e., typeReference before the dot).

Class or interface referred by typeReference, and private or protected members are not accessible within the bodies of their extension functions. Only public members can be accessed:

 1   class A {
 2       foo () { ... this.bar() ... }
 3                    // Extension function bar() is accessible
 4       protected member_1 ...
 5       private member_2 ...
 6   }
 7   function A.bar () { ...
 8      this.foo() // Method foo() is accessible as it is public
 9      this.member_1 // Compile-time error as member_1 is not accessible
10      this.member_2 // Compile-time error as member_2 is not accessible
11      ...
12   }
13   let a = new A()
14   a.foo() // Ordinary class method is called
15   a.bar() // Class extension function is called

Extension functions can be generic as in the example below:

1  function <T> B<T>.foo(p: T) {
2       console.log (p)
3  }
4  function demo (p1: B<SomeClass>, p2: B<BaseClass>) {
5      p1.foo (new SomeClass())
6        // Type inference should determine the instantiating type
7      p2.foo <BaseClass>(new DerivedClass())
8       // Explicit instantiation
9  }

Extension functions are top-level functions that can call one other. The form of such calls depends on whether static was or was not used while declaring. This affects the kind of receiver to be used for the call:

  • A static extension function requires the name of type (class or interface).

  • A non-static extension function requires a variable (as in the examples below).

 1   class A {
 2       foo () { ...
 3          this.bar() // non-static extension function is called with this.
 4          A.goo() // static extension function is called with class name receiver
 5          ...
 6       }
 7   }
 8   function A.bar () { ...
 9      this.foo() // Method foo() is called
10      A.goo() // Other static extension function is called with class name receiver
11      ...
12   }
13   static function A.goo () { ...
14      this.foo() // Compile-time error as instance members are not accessible
15      this.bar() // Compile-time error as instance extension functions are not accessible
16      ...
17   }
18   let a = new A()
19   a.foo() // Ordinary class method is called
20   a.bar() // Class instance extension function is called
21   A.goo() // Static extension function is called

Extension functions are dispatched statically, and remain active for all derived classes until the next definition of the extension function for the derived class is found:

 1   class Base { ... }
 2   class Derived extends Base { ... }
 3   function Base.foo () { console.log ("Base.foo is called") }
 4   function Derived.foo () { console.log ("Derived.foo is called") }
 5
 6   let b: Base = new Base()
 7   b.foo() // `Base.foo is called` to be printed
 8      b = new Derived()
 9   b.foo() // `Base.foo is called` to be printed
10   let d: Derived = new Derived()
11   d.foo() // `Derived.foo is called` to be printed

As illustrated by the examples below, an extension function can be:

  • Put into a compilation unit other than class or interface; and

  • Imported by using a name of the extension function.

 1   // file a.ets
 2   import {bar} from "a.ets" // import name 'bar'
 3   class A {
 4       foo () { ...
 5          this.bar() // non-static extension function is called with this.
 6          A.goo() // static extension function is called with class name receiver
 7          ...
 8       }
 9   }
10
11   // file ext.ets
12   import {A} from "a.ets" // import name 'A'
13   function A.bar () { ...
14      this.foo() // Method foo() is called
15      ...
16   }

If an extension function and a type method have the same name and signature, then calls to that name are routed to the method:

1   class A {
2       foo () { console.log ("Method A.foo is called") }
3   }
4   function A.foo () { console.log ("Extension A.foo is called") }
5   let a = new A()
6   a.foo() // Method is called, `Method A.foo is called` to be printed out

The precedence between methods and extension functions can be expressed by the following formula:

derived type instance method < base type instance method < derived type extension function < base type extension function.

In other words, the priority of standard object-oriented semantics is higher than that of type extension functions:

 1   class Base {
 2      foo () { console.log ("Method Base.foo is called") }
 3   }
 4   class Derived extends Base {
 5      override foo () { console.log ("Method Derived.foo is called") }
 6   }
 7   function Base.foo () { console.log ("Extension Base.foo is called") }
 8   function Derived.foo () { console.log ("Extension Derived.foo is called") }
 9
10   let b: Base = new Base()
11   b.foo() // `Method Base.foo is called` to be printed
12   b = new Derived()
13   b.foo() // `Method Derived.foo is called` to be printed
14   let d: Derived = new Derived()
15   d.foo() // `Method Derived.foo is called` to be printed

If an extension function and another top-level function have the same name and signature, then calls to this name are routed to a proper function in accordance with the form of the call. Extension functions cannot be called without a receiver as they have access to this:

1   class A { ... }
2   function A.foo () { console.log ("Extension A.foo is called") }
3   function foo () { console.log ("Top-level foo is called") }
4   let a = new A()
5   a.foo() // Extension function is called, `Extension A.foo is called` to be printed out
6   foo () // Top-level function is called, `Top-level foo is called` to be printed out

17.11. Trailing Lambda

The trailing lambda mechanism allows using a special form of function or method call when the last parameter of a function or a method is of function type, and the argument is passed as a lambda using the {} notation.

Syntactically, the trailing lambda looks as follows:

1   class A {
2       foo (f: ()=>void) { ... }
3   }
4
5   let a = new A()
6   a.foo() { console.log ("method lambda argument is activated") }
7   // method foo receives last argument as an inline lambda

The formal syntax of the trailing lambda is presented below:

trailingLambdaCall:
    ( objectReference '.' identifier typeArguments?
    | expression ('?.' | typeArguments)?
    )
    arguments block
    ;

Currently, no parameter can be specified for the trailing lambda. Otherwise, a compile-time error occurs.

Note: If a call is followed by a block, and the function or method being called has no last function type parameter, then such block is handled as an ordinary block of statements but not as a lambda function.

In case of other ambiguities (e.g., when a function or method call has the last parameter, which can be optional, of a function type), a syntax production that starts with ‘{’ following the function or method call is handled as the trailing lambda. If other semantics is needed, then the semicolon ‘;’ separator can be used. It means that the function or the method is to be called without the last argument (see Optional Parameters).

 1   class A {
 2       foo (p?: ()=>void) { ... }
 3   }
 4
 5   let a = new A()
 6   a.foo() { console.log ("method lambda argument is activated") }
 7   // method foo receives last argument as an inline lambda
 8
 9   a.foo(); { console.log ("that is the block code") }
10   // method 'foo' is called with 'p' parameter set to 'undefined'
11   // ';' allows to specify explicitly that '{' starts the block
12
13   function bar(f: ()=>void) { ... }
14
15   bar() { console.log ("function lambda argument is activated") }
16   // function 'bar' receives last argument as an inline lambda,
17   bar(); { console.log ("that is the block code") }
18   // function 'bar' is called with 'p' parameter set to 'undefined'
 1  function foo (f: ()=>void) { ... }
 2  function bar (n: number) { ... }
 3
 4  foo() { console.log ("function lambda argument is activated") }
 5  // function foo receives last argument as an inline lambda,
 6
 7  bar(5) { console.log ("after call of 'bar' this block is executed") }
 8
 9  foo(() => { console.log ("function lambda argument is activated") })
10  { console.log ("after call of 'foo' this block is executed") }
11  /* here, function foo receives lambda as an argument and a block after
12   the call is just a block, not a trailing lambda. */

17.12. Enumeration Super Type

Any enum type has class type Object as its supertype. This allows polymorphic assignments into Object type variables. The instanceof check can be used to get enumeration variable back by applying the ‘as’ conversion:

1 enum Commands { Open = "fopen", Close = "fclose" }
2 let c: Commands = Commands.Open
3 let o: Object = c // Autoboxing of enum type to its reference version
4 // Such reference version type has no name, but can be detected by instanceof
5 if (o instanceof Commands) {
6    c = o as Commands // And explicitly converted back by 'as' conversion
7 }

17.12.1. Enumeration Types Conversions

Every enum type is compatible (see Type Compatibility) with type Object (see Enumeration Super Type). Every variable of enum type can thus be assigned into a variable of type Object.


17.13. Enumeration Methods

Several static methods are available to handle each enumeration type as follows:

  • ‘values()’ returns an array of enumeration constants in the order of declaration.

  • ‘getValueOf(name: string)’ returns an enumeration constant with the given name, or throws an error if no constant with such name exists.

1   enum Color { Red, Green, Blue }
2   let colors = Color.values()
3   //colors[0] is the same as Color.Red
4   let red = Color.valueOf("Red")

There is an additional method for instances of any enumeration type:

  • ‘valueOf()’ returns an int or string value of an enumeration constant depending on the type of the enumeration constant.

  • ‘getName()’ returns the name of an enumeration constant.

1   enum Color { Red, Green = 10, Blue }
2   let c: Color = Color.Green
3   console.log(c.valueOf()) // prints 10
4   console.log(c.getName()) // prints Green

Note: c.toString() returns the same value as c.getValue().toString().


17.14. Exceptions

Exception is the base class of all exceptions. Exception is used to define a new exception, or any class derived from the Exception as the base of a class:

1   class MyException extends Exception { ... }

A compile-time error occurs if a generic class is a direct or indirect subclass of Exception.

An exception is thrown explicitly with the throw statement.

When an exception is thrown, the surrounding piece of code is to handle it by correcting the problem, trying an alternative approach, or informing the user.

An exception can be processed in two ways:

  • Propagating the exception from a function to the code that calls that function (see Throwing Functions);

  • Using a try statement to handle the exception (see try Statements).


17.14.1. Throwing Functions

The keyword throws is used at the end of a signature to indicate that a function (this notion here includes methods, constructors, or lambdas) can throw an exception. A function ending with throws is called a throwing function. The function type can also be marked as throws:

1   function canThrow(x: int): int throws { ... }

A throwing function can propagate exceptions to the scope from which it is called. The propagation of an exception occurs if:

  • The call of a throwing function is not enclosed in a try statement; or

  • The enclosed try statement does not contain a clause that can catch the exception.

In the example below, the function call is not enclosed in a try statement; any exception raised by canThrow function is propagated:

1   function propagate1(x: int): int throws {
2     return y = canThrow(x) // exception is propagated
3   }

In the example below, the try statement can catch only this exceptions. Any exception raised by canThrow function—except for MyException itself, and any exception derived from MyException—is propagated:

1   function propagate2(x: int): int throws {
2     try {
3       return y = canThrow(x) //
4     }
5     catch (e: MyException) /*process*/ }
6       return 0
7   }

17.14.2. Non-Throwing Functions

A non-throwing function is a function (this notion here includes methods, constructors, or lambdas) not marked as throws. Any exceptions inside a non-throwing function must be handled inside the function.

A compile-time error occurs if not all of the following requirements are met:

  • The call of a throwing function is enclosed in a try statement;

  • The enclosing try statement has a default ‘catch’ clause.

 1   // non-throwing function
 2   function cannotThrow(x: int): int {
 3     return y = canThrow(x) // compile-time error
 4   }
 5
 6   function cannotThrow(x: int): int {
 7     try {
 8       return y = canThrow(x) //
 9     }
10     catch (e: MyException) { /* process */ }
11     // compile-time error – default catch clause is required
12   }

17.14.3. Rethrowing Functions

A rethrowing function is a function that accepts a throwing function as a parameter, and is marked with the keyword rethrows.

The body of such function must not contain any throw statement that is not handled by a try statement within that body. A function with unhandled throw statements must be marked with the keyword throws but not rethrows.

Both a throwing and a non-throwing function can be an argument of a rethrowing function foo that is being called.

If a throwing function is an argument, then the calling of foo can throw an exception.

This rule is exception-free, i.e., a non-throwing function used as a call argument cannot throw an exception:

 1     function foo (action: () throws) rethrows {
 2     action()
 3   }
 4
 5   function canThrow() {
 6     /* body */
 7   }
 8
 9   function cannotThrow() {
10     /* body */
11   }
12
13   // calling rethrowing function:
14     foo(canThrow) // exception can be thrown
15     foo(cannotThrow) // exception-free

A call is exception-free if:

  • Function foo has several parameters of a function type marked with throws; and

  • All actual arguments of the call to foo are non-throwing.

However, the call can raise an exception, and is handled as any other throwing function call if at least one of the actual function arguments is throwing.

It implies that a call to foo within the body of a non-throwing function must be guaranteed with a try-catch statement:

 1   function mayThrowContext() throws {
 2     // calling rethrowing function:
 3     foo(canThrow) // exception can be thrown
 4     foo(cannotThrow) // exception-free
 5   }
 6
 7   function neverThrowsContext() {
 8     try {
 9       // calling rethrowing function:
10       foo(canThrow) // exception can be thrown
11       foo(cannotThrow) // exception-free
12     }
13     catch (e) {
14       // To handle the situation
15     }
16   }

17.14.4. Exceptions and Initialization Expression

A variable declaration (see Variable Declarations) or a constant declaration (see Constant Declarations) expression used to initialize a variable or constant must not have calls to functions that can throw or rethrow exceptions if the declaration is not within a statement that handles all exceptions.

See Throwing Functions and Rethrowing Functions for details.


17.14.5. Exceptions and Errors Inside Field Initializers

Class field initializers cannot call throwing or rethrowing functions.

See Throwing Functions and Rethrowing Functions for details.


17.15. Coroutines

A function or lambda can be a coroutine. ArkTS supports basic coroutines, structured coroutines, and communication channels. Basic coroutines are used to create and launch a coroutine; the result is then to be awaited.


17.15.1. Create and Launch a Coroutine

The following expression is used to create and launch a coroutine:

1   launchExpression: 'launch' expression;

A compile-time error occurs if that expression is not a function call expression (see Function Call Expression).

1   let res = launch cof(10)
2
3   // where 'cof' can be defined as:
4   function cof(a: int): int {
5     let res: int
6     // Do something
7     return res
8   }

Lambda is used in a launch expression as follows:

1   let res = launch (n: int) => { /* lambda body */(7)

The result of the launch expression is of type Promise<T>, where T is the return type of the function being called:

1   function foo(): int {}
2   function bar() {}
3   let resfoo = launch foo()
4   let resbar = launch bar()

In the example above the type of resfoo is Promise<int>, and the type of resbar is Promise<void>.

Similarly to TypeScript, ArkTS supports the launching of a coroutine by calling the function async (see Async Functions). No restrictions apply as to from what scope to call the function async:

1   async function foo(): Promise<int> {}
2
3   // This will create and launch coroutine
4   let resfoo = foo()

17.15.2. Awaiting a Coroutine

The expressions await and wait are used while a previously launched coroutine finishes and returns a value:

awaitExpresson:
  'await' expression
  ;

A compile-time error occurs if the expression type is not Promise<T>.

1   let promise = launch (): int { return 1 } ()
2   console.log(await promise) // output: 1

If the coroutine result must be ignored, then the expression statement await is used:

1   function foo() { /* do something */ }
2   let promise = launch foo()
3   await promise

17.15.3. The Promise<T> Class

The class Promise<T> represents the values returned by launch expressions. It belongs to the essential kernel packages of the standard library (see Standard Library), and thus it is imported by default and may be used without any qualification.

The following methods are used as follows:

  • then takes two arguments (the first argument is the callback used if the promise is fulfilled, and the second if it is rejected), and returns Promise<U>.

Promise<U> Promise<T>::then<U>(fullfillCallback :
    function
<T>(val: T) : Promise<U>, rejectCallback : (err: Object)
: Promise<U>)
  • catch is the alias for Promise<T>.then<U>((value: T) : U => {}, onRejected).

Promise<U> Promise<T>::catch<U>(rejectCallback : (err:
    Object) : Promise<U>)
  • finally takes one argument (the callback called after promise is either fulfilled or rejected) and returns Promise<T>.

Promise<U> Promise<T>::finally<U>(finallyCallback : (
    Object:
T) : Promise<U>)

17.15.4. Structured Coroutines


17.15.5. Channels Classes

Channels are used to send data between coroutines.

Channels classes are a part of the coroutine-related package of the standard library (see Standard Library).


17.15.6. Async Functions

The function async is an implicit coroutine that can be called as a regular function.

The return type of an async function must be Promise<T> (see The Promise<T> Class). Returning values of types Promise<T> and T from the function async is allowed.

Using return statement without an expression is allowed if the return type is Promise<void>. No-argument return statement can be added implicitly as the last statement of the function body if there is no explicit return statement in a function with the return type Promise<void>.

Note: Using this annotation is not recommended because this type of functions is only supported for the sake of backward TypeScript compatibility.


17.16. Packages

One or more package modules form a package:

packageDeclaration:
    packageModule+
    ;

Packages are stored in a file system or a database (see Compilation Units in Host System).

A package can consist of several package modules if all such modules have the same package header:

packageModule:
    packageHeader packageModuleDeclaration
    ;

packageHeader:
    'package' qualifiedName
    ;

packageModuleDeclaration:
    importDirective* packageTopDeclaration*
    ;

packageTopDeclaration:
    topDeclaration | packageInitializer
    ;

A compile-time error occurs if:

  • A package module contains no package header; or

  • Package headers of two package modules in the same package have different identifiers.

A package module automatically imports all exported entities from the essential kernel packages of the standard library (see Standard Library). All entities from these packages are accessible as simple names.

A package module can automatically access all top-level entities declared in all modules that constitute the package.


17.16.1. Internal Access Modifier

The modifier internal indicates that a class member, a constructor, or an interface member is accessible within its compilation unit only. If the compilation unit is a package (see Packages), then internal members can be used in any package module. If the compilation unit is a separate module (see Separate Modules), then internal members can be used within this module.

 1   class C {
 2     internal count: int
 3     getCount(): int {
 4       return this.count // ok
 5     }
 6   }
 7
 8   function increment(c: C) {
 9     c.count++ // ok
10   }

17.16.2. Package Initializer

Among all package modules there can be one to contain a code that performs initialization of global variables across all package modules. The appropriate syntax is presented below:

packageInitializer:
    'static' block
    ;

A compile-time error occurs if a package contains more than one package initializer.

A package initializer is executed only once right before the first activation of the package (calling an exported function or accessing an exported global variable).


17.16.3. Sub-Entity Binding

The import bindings ‘qualifiedName’ (that consists of at least two identifiers) or ‘qualifiedName as A’ bind a sub-entity to the declaration scope of the current module.

‘L’ is a static entity and the last identifier in the ‘qualifiedName A.B.L’. L’s public access modifier is defined in the class or interface denoted in the previous part of the ‘qualifiedName’. ‘L’ is accessible regardless of the export status of the class or the interface it belongs to.

An entity (or—in the case of overloaded methods—entities) is bound by its original name, or by an alias (if the alias is set). In the latter case, the original name becomes inaccessible.

The following module can be considered an example:

1   class A {
2     class B {
3       public static L: int
4     }
5   }

The table below illustrates the import of this module:

Import

Usage

import {A.B.L} from "..."
if (L == 0) { ... }
import {A.B} from "..."
let x = new B() // OK
let y = new A() // Error: 'A' is
   not accessible
import {A.B.L as X} from ".."
if (X == 0) { ... }
import {A.B as AB} from "..."
let x = new AB()

This form of binding is included in the language specifically to simplify the migration from the languages that support access to sub-entities as simple names. This feature is to be used only for migration.


17.16.4. All Static Sub-Entities Binding

The import binding ‘qualifiedName.* ‘ binds all public static sub-entities of the entity denoted by the qualifiedName to the declaration scope of the current module.

The following module can be considered an example:

1   class A {
2     class Point {
3       public static X: int
4       public static Y: int
5       public isZero(): boolean {}
6     }
7   }

The examples below illustrate the import of this module:

1   // Import:
2   import A.Point.* from "..."
1   // Usage:
2   import A.Point.* from "..."
3
4   if ((X == 0) && (Y == 0)) { // OK
5      // ...
6   }
7
8   let x = isZero() / Error: 'isZero' is not static

This form of binding is included into ArkTS specifically to simplify the migration from the languages that support access to sub-entities as simple names. This feature is to be used only for migration.


17.16.5. Import and Overloading of Function Names

While importing functions, the following situations can occur:

  • Overloading, where different imported functions have the same name but different signatures, or a function (functions) of the current module and an imported function (functions) have the same name but different signatures.

  • Shadowing, where a function (functions) of the current module and an imported function (functions) have the same name and signature.


17.16.6. Overloading of Function Names

Overloading is the situation when a compilation unit has access to several functions with the same names (regardless of where such functions are declared). The code can use all such functions if they have distinguishable signatures (i.e., the functions are not override-equivalent):

 1   package P1
 2   function foo(p: int) {}
 3
 4   package P2
 5   function foo(p: string) {}
 6
 7   // Main module
 8   import {foo} from "path_to_file_with_P1"
 9   import {foo} from "path_to_file_with_P2"
10
11   function foo (p: double) {}
12
13   function main() {
14     foo(5) // Call to P1.foo(int)
15     foo("A string") // Call to P2.foo(string)
16     foo(3.141592653589) // Call to local foo(double)
17   }

17.16.7. Shadowing of Function Names

Shadowing is the compile-time error that occurs if an imported function is identical to the function declared in the current compilation unit (the same names and override-equivalent signatures), i.e., the declarations are duplicated.

Qualified import or alias in import can be used to access the imported entity.

 1   package P1
 2      function foo() {}
 3   package P2
 4      function foo() {}
 5   // Main program
 6   import {foo} from "path_to_file_with_P1"
 7   import {foo} from "path_to_file_with_P2" /* Error: duplicating
 8       declarations imported*/
 9   function foo() {} /* Error: duplicating declaration identified
10       */
11   function main() {
12     foo() // Error: ambiguous function call
13     // But not a call to local foo()
14     // foo() from P1 and foo() from P2 are not accessible
15   }

17.17. Generics Experimental

17.17.1. Declaration-Site Variance

Optionally, a type parameter can have keywords in or out (a variance modifier, which specifies the variance of the type parameter).

Note: This description of variance modifiers is preliminary. The details are to be specified in the future versions of ArkTS.

Type parameters with the keyword out are covariant, and can be used in the out-position only.

Type parameters with the keyword in are contravariant, and can be used in the in-position only.

Type parameters with no variance modifier are implicitly invariant, and can occur in any position.

A compile-time error occurs if a function, method, or constructor type parameters have a variance modifier specified.

Variance is used to describe the subtyping (see Subtyping) operation on parameterized types (see Generic Declarations). The variance of the corresponding type parameter F defines the subtyping between T<A> and T<B> (in the case of declaration-site variance with two different types A <: B) as follows:

  • Covariant (out F): T<A> <: T<B>;

  • Contravariant (in F): T<A> :> T<B>;

  • Invariant (default) (F).

17.17.2. NonNullish Type Parameter

When some generic class has a type parameter with nullish union type constraint then a special syntax for the type annotation can be used to get a non-nullish version of the type parameter variable. The example below illustrates the possibility:

 1   class A<T> {  // in fact it extends Object|null|undefined
 2       foo (p: T): T! { // foo returns non-nullish value of p
 3          return p!
 4       }
 5   }
 6
 7   class B<T extends SomeType | null> {
 8       foo (p: T): T! { // foo returns non-nullish value of p
 9          return p!
10       }
11   }
12
13   class C<T extends SomeType | undefined> {
14       foo (p: T): T! { // foo returns non-nullish value of p
15          return p!
16       }
17   }
18
19   let a = new A<Object>
20   let b = new B<SomeType>
21   let c = new C<SomeType>
22
23   let result: Object = new Object  // Type of result is non-nullish !
24   result = a.foo(result)
25   result = b.foo(new SomeType)
26   result = c.foo(new SomeType)
27
28   // All such assignments are type-valid as well
29   result = a.foo(void)      // void! => never
30   result = b.foo(null)      // null! => never
31   result = c.foo(undefined) // undefined! => never