4. Names, Declarations and Scopes¶
This chapter introduces the following three mutually-related notions:
Names,
Declarations, and
Scopes.
Each entity in an ArkTS program—a variable, a constant, a class, a type, a function, a method, etc.—is introduced via a declaration. An entity declaration assigns a name to the entity declared. The name is used to refer to the entity further in the program text.
Each declaration is valid (i.e., available and known) only within its scope. Scope is the region of the program text where the entity is declared and can be referred to by its simple (unqualified) name (see Scopes for more details).
4.1. Names¶
A name refers to any declared entity.
Simple names consist of a single identifier. Qualified names consist of a sequence of identifiers with the ‘.’ tokens as separators:
qualifiedName:
Identifier ('.' Identifier )*
;
In a qualified name N.x (where N is a simple name, and x
is an
identifier that can follow a sequence of identifiers separated with ‘.’
tokens), N can name the following:
The name of a compilation unit (see Compilation Units, Packages, and Modules) introduced as a result of
import * as N
(see Bind All with Qualified Access) withx
to name the exported entity;A class or interface type (see Classes, Interfaces) with
x
to name its static member;A variable of a class or interface type with
x
to name its member.
4.2. Declarations¶
A declaration introduces a named entity in an appropriate declaration scope (see Scopes).
4.3. Distinguishable Declarations¶
Each declaration in the declaration scope must be distinguishable. A compile-time error occurs otherwise.
Declarations are distinguishable if:
They have different names.
They are distinguishable by signatures (see Declaration Distinguishable by Signatures).
The examples below are declarations distinguishable by names:
1 const PI = 3.14
2 const pi = 3
3 function Pi() {}
4 type IP = number[]
5 class A {
6 static method() {}
7 method() {}
8 field: number = PI
9 static field: number = PI + pi
10 }
If a declaration is indistinguishable by name, then a compile-time error occurs:
1 // The constant and the function have the same name.
2 const PI = 3.14 // compile-time error
3 function PI() { return 3.14 } // compile-time error
4
5 // The type and the variable have the same name.
6 class P type Person = P // compile-time error
7 let Person: Person // compile-time error
8
9 // The field and the method have the same name.
10 class C {
11 counter: number // compile-time error
12 counter(): number { // compile-time error
13 return this.counter
14 }
15 }
4.4. Scopes¶
The scope of a name is the region of program code within which the entity declared by that name can be referred to without having the name qualified. It means that a name is accessible in some context if it can be used in this context by its simple name.
The nature of scope usage depends on the kind of the name. A type name is used to declare variables or constants. A function name is used to call that function.
The scope of a name depends on the context the name is declared in:
A name declared on the package level (package level scope) is accessible throughout the entire package. The name can be accessed in other packages if exported.
Module level scope is applicable for separate modules only. A name declared on the module level is accessible throughout the entire module. The name can be accessed in other packages if exported.
A name declared inside a class (class level scope) is accessible in the class and sometimes, depending on the access modifier, outside the class, or by means of a derived class.
Access to names inside the class is qualified with one of the following:
this;
Class instance expression for the names of instance entities; or
Name of the class for static entities.
Outside access is qualified with one of the following:
Expression the value stores;
Reference to the class instance for the names of instance entities; or
Name of the class for static entities.
A name declared inside an interface (interface level scope) is accessible inside and outside that interface (default public).
Enum level scope is identical to the package or module level scope, as every enumeration defines a type inside a package or module. The scope of all enumeration constants and of the enumeration itself is the same.
The scope of a type parameter name in a class or interface declaration is that entire declaration, excluding static member declarations.
The scope of a type parameter name in a function declaration is that entire declaration (function parameter scope).
The scope of a name declared immediately inside the body of a function (method) declaration is the body of that function declaration from the point of declaration and up to the end of the body (method or function scope).
The scope of a name declared inside a statement block is the body of the statement block from the point of declaration and up to the end of the block (block scope).
1 function foo() {
2 let x = y // compile-time error – y is not accessible
3 let y = 1
4 }
Scopes of two names can overlap (e.g., when statements are nested). If scopes of two names overlap, then:
The innermost declaration takes precedence; and
Access to the outer name is not possible.
Class, interface, and enum members can only be accessed by applying the dot operator ‘.’ to an instance. Accessing them otherwise is not possible.
4.5. Type Declarations¶
An interface declaration (see Interfaces), a class declaration (see Classes), or an enum declaration (see Enumerations) are type declarations.
typeDeclaration:
classDeclaration
| interfaceDeclaration
| enumDeclaration
;
4.6. Type Alias Declaration¶
Type aliases enable using meaningful and concise notations by providing the following:
Names for anonymous types (array, function, and union types); or
Alternative names for existing types.
Scopes of type aliases are package or module level scopes. Names of all type aliases must be unique across all types in the current context.
typeAlias:
'type' identifier typeParameters? '=' type
;
Meaningful names can be provided for anonymous types as follows:
1 type Matrix = number[][]
2 type Handler = (s: string, no: number) => string
3 type Predicate<T> = (x: T) => Boolean
4 type NullableNumber = Number | null
If the existing type name is too long, then a shorter new name can be introduced by using type alias (particularly for a generic type).
1 type Dictionary = Map<string, string>
2 type MapOfString<T> = Map<T, string>
A type alias acts as a new name only. It neither changes the meaning of the original type nor introduces a new type.
1 type Vector = number[]
2 function max(x: Vector): number {
3 let m = x[0]
4 for (let v of x)
5 if (v > m) v = m
6 return m
7 }
8
9 function main() {
10 let x: Vector = [3, 2, 1]
11 console.log(max(x)) // ok
12 }
Type aliases can be recursively referenced inside the right-hand side of a type alias declaration (see Recursive Type Aliases).
4.6.1. Recursive Type Aliases¶
In a type alias defined as type A = something, A can be used recursively if it is one of the following:
Array element type:
type A = A[]
; orType argument of a generic type: type A = C<A>.
1 type A = A[] // ok, used as element type
2
3 class C<T> { /*body*/}
4 type B = C<B> // ok, used as a type argument
5
6 type D = string | Array<D> // ok
Any other use causes a compile-time error, because the compiler does not have enough information about the defined alias:
1 type E = E // compile-time error
2 type F = string | E // compile-time error
The same rules apply to a generic type alias defined as type A<T> = something:
1 type A<T> = Array<A<T>> // ok, A<T> is used as a type argument
2 type A<T> = string | Array<A<T>> // ok
3
4 type A<T> = A<T> // compile-time error
A compile-time error occurs if a generic type alias is used without a type argument:
1 type A<T> = Array<A> // compile-time error
Note: There is no restriction on using a type parameter T in the right side of a type alias declaration. The following code is valid:
1 type NodeValue<T> = T | Array<T> | Array<NodeValue<T>>;
4.7. Variable and Constant Declarations¶
4.7.1. Variable Declarations¶
A variable declaration introduces a new named variable that can be assigned an initial value:
variableDeclarations:
'let' varDeclarationList
;
variableDeclarationList:
variableDeclaration (',' variableDeclaration)*
;
variableDeclaration:
identifier ('?')? ':' ('readonly')? type initializer?
| identifier initializer
;
initializer:
'=' expression
;
When a variable is introduced by a variable declaration, type T of the variable is determined as follows:
T is the type specified in a type annotation (if any) of the declaration.
If ‘?’ is used after the name of the variable, then the actual type T of the variable is type | undefined.
If the declaration also has an initializer, then the initializer expression type must be compatible with T (see Type Compatibility with Initializer).
If no type annotation is available, then T is inferred from the initializer expression (see Type Inference from Initializer).
1 let a: number // ok
2 let b = 1 // ok, number type is inferred
3 let c: number = 6, d = 1, e = "hello" // ok
4
5 // ok, type of lambda and type of 'f' can be inferred
6 let f = (p: number) => b + p
7 let x // compile-time error -- either type or initializer
Every variable in a program must have an initial value before it can be used. The initial value can be identified as follows:
The initial value is explicitly specified by an initializer.
Each method or function parameter is initialized to the corresponding argument value provided by the caller of the method or function.
Each constructor parameter is initialized to the corresponding argument value as provided by:
Class instance creation expression (see New Expressions); or
Explicit constructor call (see Explicit Constructor Call).
An exception parameter is initialized to the thrown object (see throw Statements) that represents exception or error.
Each class, instance, local variable, or array element is initialized with a default value (see Default Values for Types) when it is created.
Otherwise, the variable is not initialized, and a compile-time error occurs.
If an initializer expression is provided, then additional restrictions apply to the content of the expression as described in Exceptions and Initialization Expression.
If the type of a variable declaration has the prefix readonly, then the type must be of array kind, and the restrictions on its operations are applied to the variable as described in Readonly Parameters.
A compile-time error occurs if a non-array type has the prefix readonly.
1 function foo (p: number[]) {
2 let x: readonly number [] = p
3 x[0] = 666 // Compile-time error as array itself is readonly
4 console.log (x[0]) // read operation is OK
5 }
4.7.2. Constant Declarations¶
A constant declaration introduces a named variable with a mandatory explicit value.
The value of a constant cannot be changed by an assignment expression (see Assignment). If the constant is an object or array, then its properties or items can be modified.
constantDeclarations:
'const' constantDeclarationList
;
constantDeclarationList:
constantDeclaration (',' constantDeclaration)*
;
constantDeclaration:
identifier (':' type)? initializer
;
The type T of a constant declaration is determined as follows:
If T is the type specified in a type annotation (if any) of the declaration, then the initializer expression must be compatible with T (see Type Compatibility with Initializer).
If no type annotation is available, then T is inferred from the initializer expression (see Type Inference from Initializer).
If ‘?’ is used after the name of the constant, then the type of the constant is T | undefined, regardless of whether T is identified explicitly or via type inference.
1 const a: number = 1 // ok
2 const b = 1 // ok, int type is inferred
3 const c: number = 1, d = 2, e = "hello" // ok
4 const x // compile-time error -- initializer is mandatory
5 const y: number // compile-time error -- initializer is mandatory
Additional restrictions on the content of the initializer expression are described in Exceptions and Initialization Expression.
4.7.3. Type Compatibility with Initializer¶
If a variable or constant declaration contains type annotation T and initializer expression E, then the type of E must be compatible with T, see Assignment-like Contexts.
4.7.4. Type Inference from Initializer¶
If a declaration does not contain an explicit type annotation, then its type is inferred from the initializer as follows:
If the initializer expression is the null literal, then the type is Object | null.
If the initializer expression is of union type comprised of numeric literals only, then the type is the smallest numeric type all numeric literals fit into.
If the initializer expression is of union type comprised of literals of a single type T, then the type is T.
If the type can be inferred from the initializer expression, then the type is that of the initializer expression.
If the type of the initializer cannot be inferred from the expression itself, then a compile-time error occurs (see Object Literal):
1 let a = null // type of 'a' is Object | null
2
3 let cond: boolean = /*something*/
4 let b = cond ? 1 : 2 // type of 'b' is int
5 let c = cond ? 3 : 3.14 // type of 'b' is double
6 let d = cond ? "one" : "two" // type of 'c' is string
7 let e = cond ? 1 : "one" // type of 'e' is 1 | "one"
8
9 let f = {name: "aa"} // compile-time error
4.8. Function Declarations¶
Function declarations specify names, signatures, and bodies when introducing named functions. A function body is a block (see Block).
functionDeclaration:
functionOverloadSignature*
modifiers? 'function' identifier
typeParameters? signature block?
;
modifiers:
'native' | 'async'
;
Function overload signature allows calling a function in different ways (see Function Overload Signatures).
If a function is declared as generic (see Generics), then its type parameters must be specified.
The native
modifier indicates that the function is
a native function (see Native Functions in Experimental Features).
A compile-time error occurs if a native function has a body.
Functions must be declared on the top level (see Top-Level Statements).
4.8.1. Signatures¶
A signature defines parameters and the return type (see Return Type) of a function, method, or constructor.
signature:
'(' parameterList? ')' returnType? throwMark?
;
returnType:
':' type
;
throwMark:
'throws' | 'rethrows'
;
See Throwing Functions for the details of ‘throws
’ marks, and
Rethrowing Functions for the details of ‘rethrows
’ marks.
Overloading (see Function and Method Overloading) is supported for functions and methods. The signatures of functions and methods are important for their unique identification.
4.8.2. Parameter List¶
A signature contains a parameter list that specifies an identifier of each parameter name, and the type of each parameter. The type of each parameter must be explicitly defined.
parameterList:
parameter (',' parameter)* (',' optionalParameters|restParameter)?
| restParameter
| optionalParameters
;
parameter:
identifier ':' 'readonly'? type
;
restParameter:
'...' parameter
;
If a parameter type is prefixed with readonly, then there are additional restrictions on the parameter as described in Readonly Parameters.
The last parameter of a function can be a rest parameter (see Rest Parameter), or a sequence of optional parameters (see Optional Parameters). This construction allows omitting the corresponding argument when calling a function. If a parameter is not optional, then each function call must contain an argument corresponding to that parameter. Non-optional parameters are called the required parameters.
The function below has required parameters:
1 function power(base: number, exponent: number): number {
2 return Math.pow(base, exponent)
3 }
4 power(2, 3) // both arguments are required in the call
A compile-time error occurs if an optional parameter precedes a required parameter in the parameter list.
4.8.3. Readonly Parameters¶
If the parameter type is prefixed with readonly, then the type must be of array type T[]. Otherwise, a compile-time error occurs.
The readonly parameter indicates that the array content cannot be modified by a function or by a method body. A compile-time error if the array content is modified by an operation:
1 function foo(array: readonly number[]) {
2 let element = array[0] // OK, one can get array element
3 array[0] = element // Compile-time error, array is readonly
4 }
It applies to variables as discussed in Variable Declarations.
4.8.4. Optional Parameters¶
There are two forms of optional parameters:
optionalParameters:
optionalParameter (',' optionalParameter)
;
optionalParameter:
identifier ':' 'readonly'? type '=' expression
| identifier '?' ':' 'readonly'? type
;
The first form contains an expression that specifies a default value. That is called a parameter with default value. The value of the parameter is set to the default value if the argument corresponding to that parameter is omitted in a function call.
1 function pair(x: number, y: number = 7)
2 {
3 console.log(x, y)
4 }
5 pair(1, 2) // prints: 1 2
6 pair(1) // prints: 1 7
The second form is a short notation for a parameter of union type T | undefined with the default value undefined. It means that identifier ‘?’ ‘:’ type is equivalent to identifier ‘:’ type | undefined = undefined. If a type is of the value type kind, then implicit boxing must be applied (as in Union Types) as follows: identifier ‘?’ ‘:’ valueType is equivalent to identifier ‘:’ referenceTypeForValueType | undefined = undefined.
For example, the following two functions can be used in the same way:
1 function hello1(name: string | undefined = undefined) {}
2 function hello2(name?: string) {}
3
4 hello1() // 'name' has 'undefined' value
5 hello1("John") // 'name' has a string value
6 hello2() // 'name' has 'undefined' value
7 hello2("John") // 'name' has a string value
8
9 function foo1 (p?: number) {}
10 function foo2 (p: Number | undefined = undefined) {}
11
12 foo1() // 'p' has 'undefined' value
13 foo1(5) // 'p' has an integer value
14 foo2() // 'p' has 'undefined' value
15 foo2(5) // 'p' has an integer value
4.8.5. Rest Parameter¶
Rest parameters allow functions or methods to take unbounded numbers of
arguments. Rest parameters have the symbol ‘...
’ mark before the
parameter name:
1 function sum(...numbers: number[]): number {
2 let res = 0
3 for (let n of numbers)
4 res += n
5 return res
6 }
A compile-time error occurs if a rest parameter:
Is not the last parameter in a parameter list;
Has a type that is not an array type.
A function that has a rest parameter of type T[] can accept any number of arguments of type T:
1 function sum(...numbers: number[]): number {
2 let res = 0
3 for (let n of numbers)
4 res += n
5 return res
6 }
7
8 sum() // returns 0
9 sum(1) // returns 1
10 sum(1, 2, 3) // returns 6
If an argument of type T[] is prefixed with the spread operator
‘...
’, then only one argument can be accepted:
1 function sum(...numbers: number[]): number {
2 let res = 0
3 for (let n of numbers)
4 res += n
5 return res
6 }
7
8 let x: number[] = [1, 2, 3]
9 sum(...x) // returns 6
4.8.6. Shadowing Parameters¶
If the name of a parameter is identical to the name of a top-level variable accessible within the body of a function or a method with that parameter, then the name of the parameter shadows the name of the top-level variable within the body of that function or method:
1 class T1 {}
2 class T2 {}
3 class T3 {}
4
5 let variable: T1
6 function foo (variable: T2) {
7 // 'variable' has type T2 and refers to the function parameter
8 }
9 class SomeClass {
10 method (variable: T3) {
11 // 'variable' has type T3 and refers to the method parameter
12 }
13 }
4.8.7. Return Type¶
An omitted function or method return type can be inferred from the function, or the method body. A compile-time error occurs if a return type is omitted in a native function (see Native Functions).
The current version of ArkTS allows inferring return types at least under the following conditions:
If there is no return statement, or if all return statements have no expressions, then the return type is void (see void Type).
If there are k return statements (where k is 1 or more) with the same type expression R, then the R is the return type.
If there are k return statements (where k is 2 or more) with expressions of types (T1,
...
, Tk), and R is the union type (see Union Types) of these types (T1 | … | Tk), and its normalized version (see Union Types Normalization) is the the return type.If the function is async, the return type is inferred by using the rules above, and the type T is not Promise type, then the return type is Promise<T>.
Future compiler implementations are to infer the return type in more cases. The type inference is presented in the example below:
// Explicit return type
function foo(): string { return "foo" }
// Implicit return type inferred as string
function goo() { return "goo" }
class Base {}
class Derived1 extends Base {}
class Derived2 extends Base {}
function bar (condition: boolean) {
if (condition)
return new Derived1()
else
return new Derived2()
}
// Return type of bar will be Derived1|Derived2 union type
function boo (condition: boolean) {
if (condition) return 1
}
// That is a compile time error as there is an execution path with no return
If a particular type inference case is not recognized by the compiler, then a corresponding compile-time error occurs.
If the function return type is not void and there is an execution path in the function or method body which has no return statement (see return Statements), then a compile-time error occurs.
4.8.8. Function Overload Signatures¶
The ArkTS language allows specifying a function that can have several overload signatures with the same name followed by one implementation function body:
functionOverloadSignature:
'async'? 'function' identifier typeParameters? signature
;
A compile-time error occurs if the function implementation is missing, or does not immediately follow the declaration.
A call of a function with overload signatures is always a call of the implementation function.
The example below has overload signatures defined (one is parameterless, and the other two have one parameter each):
1 function foo(): void // 1st signature
2 function foo(x: string): void // 2nd signature
3 function foo(x?: string): void // 3rd - implementation signature
4 {
5 console.log(x)
6 }
7
8 foo() // ok, call fits 1st and 3rd signatures
9 foo("aa") // ok, call fits 2nd and 3rd signatures
10 foo(undefined) // ok, call fits the 3rd signature
The call of foo()
is executed as a call of the implementation function
with the undefined
argument. The call of foo(x)
is executed as a call
of the implementation function with the x
argument.
The compatibility requirements of overload signatures are described in Overload Signature Compatibility.
A compile-time error occurs unless all overload signatures are either exported or non-exported.