6. Contexts and Conversions¶
Every expression written in the ArkTS programming language has a type that is inferred at compile time.
In most contexts, an expression must be compatible with a type expected in that context. This type is called the target type.
If no target type is expected in the context, then the expression is called a standalone expression:
1 let a = expr // no target type is expected
2
3 function foo(): void {
4 expr // no target type is expected
5 }
Otherwise, the expression is a non-standalone expression:
1 let a: number = expr // target type of 'expr' is number
2
3 function foo(s: string) {}
4 foo(expr) // target type of 'expr' is string
The type of some expressions cannot be inferred from the expression itself (see Object Literal as an example). A compile-time error occurs if such an expression is used as a standalone expression.
There are two ways to facilitate the compatibility of a non-standalone expression with its surrounding context:
The type of some non-standalone expressions can be inferred from the target type (expression types can be different in different contexts).
If the inferred expression type is different from the target type, then performing an implicit conversion can ensure type compatibility. The conversion from type S to type T causes a type S expression to be handled as a type T expression at compile time.
A compile-time error occurs if neither produces an appropriate expression type.
The rules that determine whether a target type allows an implicit conversion vary for different kinds of contexts and types of expressions. The target type can influence not only the type of the expression but also, in some cases, its runtime behavior.
Some cases of conversion require action at runtime to check the validity of conversion, or to translate the runtime expression value into a form that is appropriate for the new type T.
Contexts can be of the following kinds:
Assignment-like Contexts where the expression value is bound to a variable.
String Operator Contexts with
string
concatenation (+ operator).Numeric Operator Contexts with all numeric operators (+, -, etc.).
Casting Contexts and Conversions. i.e., the conversion of an expression value to a type explicitly specified by a cast expression (see Cast Expressions).
6.1. Assignment-like Contexts¶
Assignment-like contexts include the following:
Declaration contexts that allow setting an initial value to a variable (see Variable Declarations), a constant (see Constant Declarations), or a field (see Field Declarations) with an explicit type annotation;
Assignment contexts that allow assigning (see Assignment) an expression value to a variable;
Call contexts that allow assigning an argument value to a corresponding formal parameter of a function, method, constructor or lambda call (see Function Call Expression, Method Call Expression, Explicit Constructor Call, and New Expressions);
Composite literal contexts that allow setting an expression value to an array element (see Array Type Inference from Context), a class, or an interface field (see Object Literal);
The examples are presented below:
1 // declaration contexts:
2 let x: number = 1
3 const str: string = "done"
4 class C {
5 f: string = "aa"
6 }
7
8 // assignment contexts:
9 x = str.length
10 new C().f = "bb"
11
12 // call contexts:
13 function foo(s: string) {}
14 foo("hello")
15
16 // composite literal contexts:
17 let a: number[] = [str.length, 11]
In all these cases, the expression type either must be equal to the target type or can be converted to the target type by using one of the conversions discussed below. Otherwise, a compile-time error occurs.
Assignment-like contexts allow using of one of the following:
If there is no applicable conversion, then a compile-time error occurs.
6.2. String Operator Contexts¶
String context applies only to a non-string operand of the binary +
operator if the other operand is a string.
String conversion for a non-string operand is evaluated as follows:
The operand of nullish type that has a nullish value is converted as described below:
The operand
null
is converted to string null.The operand
undefined
is converted to string undefined.
An operand of reference type or enum type is converted by applying the method call toString().
An operand of integer type (see Integer Types and Operations) is converted to type string with a value that represents the operand in the decimal form;
An operand of floating-point type (see Floating-Point Types and Operations) is converted to type string with a value that represents the operand in the decimal form (without the loss of information);
An operand of type boolean is converted to type string with the values
true
orfalse
;An operand of type char is converted by using Character to String Conversions;
An operand of an enumeration type (see Enumerations) is converted to type string with the value of the corresponding enumeration constant if values of enumeration are of type string.
If there is no applicable conversion, then a compile-time error occurs.
The target type in this context is always string:
1 console.log("" + null) // prints "null"
2 console.log("value is " + 123) // prints "value is 123"
3 console.log("BigInt is " + 123n) // prints "BigInt is 123"
4 console.log(15 + " steps") // prints "15 steps"
5 let x: string | null = null
6 console.log("string is " + x) // prints "string is null"
7 let c = "X"
8 console.log("char is " + c) // prints "char is X"
6.3. Numeric Operator Contexts¶
Numeric contexts apply to the operands of an arithmetic operator. Numeric contexts use combinations of predefined numeric types conversions (see Primitive Types Conversions), and ensure that each argument expression can be converted to target type T while the arithmetic operation for the values of type T is being defined.
An operand of an enumeration type (see Enumerations) can be used in the numeric context if values of this enumeration are of type int. The type of this operand is assumed to be int.
The numeric contexts are actually the forms of the following expressions:
Unary (see Unary Expressions);
Multiplicative (see Multiplicative Expressions);
Additive (see Additive Expressions);
Shift (see Shift Expressions);
Relational (see Relational Expressions);
Equality (see Equality Expressions);
Bitwise and Logical (see Bitwise and Logical Expressions);
Conditional-And (see Conditional-And Expression);
Conditional-Or (see Conditional-Or Expression).
6.4. Casting Contexts and Conversions¶
Casting contexts are applied to cast expressions (Cast Expressions), and rely on the application of casting conversions.
Casting conversion is the conversion of an operand in a cast expression to an explicitly specified target type by using one of the following:
identity conversion, if the target type is the same as the expression type;
If there is no applicable conversion, then a compile-time error occurs.
6.4.1. Numeric Casting Conversions¶
A numeric casting conversion occurs if the target type and the expression type are both numeric or char:
1 function process_int(an_int: int) { ... }
2
3 let pi = 3.14
4 process_int(pi as int)
These conversions never cause runtime errors.
Numeric casting conversion of an operand of type double to target type float is performed in compliance with the IEEE 754 rounding rules.
A double NaN is converted to a float NaN.
A double infinity is converted to the same-signed floating-point infinity. This conversion can lose precision or range, resulting in the following:
Float zero from a nonzero double; and
Float infinity from a finite double.
A numeric casting conversion of a floating-point type operand to types short, byte, or char is performed in the following two steps:
First, the casting conversion to int is done;
Then, the int operand is casted to the target type.
A numeric casting conversion of a floating-point type operand to target types long or int is performed by the following rules:
If the operand is NaN, then the result is 0 (zero).
If the operand is positive infinity, or if the operand is too large for the target type, then the result is the largest representable value of the target type.
If the operand is negative infinity, or if the operand is too small for the target type, then the result is the smallest representable value of the target type.
Otherwise, the result is the value that rounds toward zero by using IEEE 754 ‘round-toward-zero’ mode.
6.4.2. Narrowing Reference Casting Conversions¶
A narrowing reference casting conversion converts an expression of a supertype (superclass or superinterface) to a subclass or subinterface:
1 class Base {}
2 class Derived extends Base {}
3
4 let b: Base = new Derived()
5 let d: Derived = b as Derived
A runtime error occurs (TBD: name it) during these conversion if the type of a converted expression cannot be converted to the target type:
1 class Base {}
2 class Derived1 extends Base {}
3 class Derived2 extends Base {}
4
5 let b: Base = new Derived1()
6 let d = b as Derived2 // runtime error
6.4.3. Casting Conversions from Union¶
A casting conversion from union converts an expression of union type to one of the types of the union, or to a type that is derived from such one type.
For union type U = T1 | … | TN, the casting conversion from union converts an expression of type U to some type TT (target type).
A compile-time error occurs if the target type TT is not one of Ti, and not derived from one of Ti.
1 class Cat { sleep () {}; meow () {} }
2 class Dog { sleep () {}; bark () {} }
3 class Frog { sleep () {}; leap () {} }
4 class Spitz extends Dog { override sleep() { /* 18-20 hours a day */ } }
5
6 type Animal = Cat | Dog | Frog | number
7
8 let animal: Animal = new Spitz()
9 if (animal instanceof Frog) {
10 let frog: Frog = animal as Frog // Use 'as' conversion here
11 frog.leap() // Perform an action specific for the particular union type
12 }
13 if (animal instanceof Spitz) {
14 let dog = animal as Spitz // Use 'as' conversion here
15 dog.sleep()
16 // Perform an action specific for the particular union type derivative
17 }
These conversions can cause a runtime error (TBD: name it) if the runtime type of an expression is not the target type.
6.5. Implicit Conversions¶
This section describes all implicit conversions that are allowed. Each conversion is allowed in a particular context (for example, if an expression that initializes a local variable is subject to Assignment-like Contexts, then the rules of this context define what specific conversion is implicitly chosen for the expression).
6.5.1. Primitive Types Conversions¶
A primitive type conversion is one of the following:
6.5.2. Widening Primitive Conversions¶
Widening primitive conversions converts the following:
Values of a smaller numeric type to a larger type (see Numeric Types Hierarchy);
Values of type byte to type char (see Character Type and Operations);
Values of type char to types int, long, float, and double;
Values of an enumeration type to types int, long, float, and double (if enumeration constants of this type are of type int).
From |
To |
---|---|
byte |
short, int, long, float, double, or char |
short |
int, long, float, or double |
int |
long, float, or double |
long |
float or double |
float |
double |
char |
int, long, float, or double |
enumeration |
int, long, float, or double |
These conversions cause no loss of information about the overall magnitude of a numeric value. Some least significant bits of the value can be lost only in conversions from integer type to floating-point type if the IEEE 754 ‘round-to-nearest’ mode is used correctly. The resultant floating-point value is properly rounded to the integer value.
Widening primitive conversions never cause runtime errors.
6.5.3. Constant Narrowing Integer Conversions¶
Constant narrowing integer conversion converts an expression of an integer type or of type char to a value of a smaller integer type provided that:
The expression is a constant expression (see Constant Expressions);
The value of the expression fits into the range of the smaller type.
1 let b: byte = 127 // ok, int -> byte conversion
2 let c: char = 0x42E // ok, int -> char conversion
3 b = 128 // compile-time-error, value is out of range
4 b = 1.0 // compile-time-error, floating-point value cannot be converted
These conversions never cause runtime errors.
6.5.4. Boxing Conversions¶
Boxing conversions handle primitive type expressions as expressions of a corresponding reference type.
If the unboxed target type is larger than the expression type, then a widening primitive conversion is performed as the first step of a boxing conversion of numeric types and type char.
For example, a boxing conversion converts i of primitive value type int into a reference n of class type Number:
1 let i: int = 1
2 let n: Number = i // int -> number -> Number
3
4 let c: char = 'a'
5 let l: Long = c // char -> long -> Long
These conversions can cause OutOfMemoryError thrown if the storage available for the creation of a new instance of the reference type is not sufficient.
6.5.5. Unboxing Conversions¶
Unboxing conversions handle reference type expressions as expressions of a corresponding primitive type.
If the target type is larger than the unboxed expression type, then a widening primitive conversion is performed as the second step of the unboxing conversion of numeric types and type char.
For example, the unboxing conversion converts i of reference type Int into type long:
1 let i: Int = 1
2 let l: long = i // Int -> int -> long
Unboxing conversions never cause runtime errors.
6.5.6. Widening Union Conversions¶
There are three options of widening union conversions:
Conversion from a union type to a wider union type;
Conversion from a non-union type to a union type;
Conversion from a union type that consists of literals only to a non-union type.
These conversions never cause runtime errors.
Union type U (U1 | … | Un) can be converted into a different union type V (V1 | … | Vm) if the following is true after normalization (see Union Types Normalization):
For every type Ui (i in 1..n-normalized) there is at least one type Vj (i in 1..m-normalized), when Ui is compatible with Vj (see Type Compatibility).
For every value Ui there is a value Vj, when Ui == Vj.
Note: If union type normalization issues a single type or value, then this type or value is used instead of the initial set of union types or values.
This concept is illustrated by the example below:
1 let u1: string | number | boolean = true
2 let u2: string | number = 666
3 u1 = u2 // OK
4 u2 = u1 // compile-time error as type of u1 is not compatible with type of u2
5
6 let u3: 1 | 2 | boolean = 3
7 // compile-time error as there is no value 3 among values of u3 type
8
9 class Base {}
10 class Derived1 extends Base {}
11 class Derived2 extends Base {}
12
13 let u4: Base | Derived1 | Derived2 = new ...
14 let u5: Derived1 | Derived2 = new ...
15 u4 = u5 // OK, u4 type is Base after normalization and Derived1 and Derived2
16 // are compatible with Base as Note states
17 u5 = u4 // compile-time error as Base is not compatible with both
18 // Derived1 and Derived2
Non-union type T can be converted to union type U = U1 | … | Un if T is compatible with one of Ui types.
1 let u: number | string = 1 // ok
2 u = "aa" // ok
3 u = true // compile-time error
Union type U (U1 | … | Un) can be converted into non-union type T if each Ui is a literal that can be implicitly converted to type T.
1 let a: 1 | 2 = 1
2 let b: int = a // ok, literals fit type 'int'
3 let c: number = a // ok, literals fit type 'number'
4
5 let d: 3 | 3.14 = 3
6 let e: number = d // ok
7 let f: int = d // compile-time error, 3.14 cannot be converted to 'int'
6.5.7. Widening Reference Conversions¶
A widening reference conversion handles any subtype as a supertype. It requires no special action at runtime, and never causes an error.
1 interface BaseInterface {}
2 class BaseClass {}
3 interface DerivedInterface extends BaseInterface {}
4 class DerivedClass extends BaseClass implements BaseInterface
5 {}
6 function foo (di: DerivedInterface) {
7 let bi: BaseInterface = new DerivedClass() /* DerivedClass
8 is a subtype of BaseInterface */
9 bi = di /* DerivedInterface is a subtype of BaseInterface
10 */
11 }
The only exception is the cast to type never that is forbidden. This cast is a compile-time error as it can cause type-safety violations:
1 class A { a_method() {} }
2 let a = new A
3 let n: never = a as never // compile-time error: no object may be assigned
4 // to a variable of the never type
5
6 class B { b_method() {} }
7 let b: B = n // OK as never is a subtype of any type
8 b.b_method() // this breaks type-safety if as cast to never is allowed
The conversion of array types (see Array Types) also works in accordance with the widening style of the type of array elements as shown below:
1 class Base {}
2 class Derived extends Base {}
3 function foo (da: Derived[]) {
4 let ba: Base[] = da /* Derived[] is assigned into Base[] */
5 }
This array assignment can cause ArrayStoreError at runtime if an object of incorrect type is included in the array. The runtime system performs runtime checks to ensure type-safety as show below:
1 class Base {}
2 class Derived extends Base {}
3 class AnotherDerived extends Base {}
4 function foo (da: Derived[]) {
5 let ba: Base[] = da // Derived[] is assigned into Base[]
6 ba[0] = new AnotherDerived() // This assignment of array
7 element will cause *ArrayStoreError*
8 }
6.5.8. Character to String Conversions¶
Character to string conversion converts a value of type char to type string. The resultant new string has the length equal to 1. The converted char is the single element of the new string:
1 let c: char = c'X'
2 let s: string = c // s contains "X"
This conversion can cause OutOfMemoryError thrown if the storage available for the creation of a new string is not sufficient.
6.5.9. Constant String to Character Conversions¶
Constant string to character conversion converts an expression of type string to char type. The initial string type expression must be a constant expression (see Constant Expressions) with a length equal to 1.
The resultant char is the first character of the converted string.
This conversion never causes runtime errors.
6.5.10. Function Types Conversions¶
Function types conversion is the conversion of one function type to another. A function types conversion is valid if the following conditions are met:
Parameter types are converted by using contravariance.
Return types are converted by using covariance.
See Type Compatibility for details.
1 class Base {}
2 class Derived extends Base {}
3
4 type FuncTypeBaseBase = (p: Base) => Base
5 type FuncTypeBaseDerived = (p: Base) => Derived
6 type FuncTypeDerivedBase = (p: Derived) => Base
7 type FuncTypeDerivedDerived = (p: Derived) => Derived
8
9 function (
10 bb: FuncTypeBaseBase, bd: FuncTypeBaseDerived,
11 db: FuncTypeDerivedBase, dd: FuncTypeDerivedDerived\
12 ) {
13 bb = bd
14 /* OK: identical (invariant) parameter types, and compatible return type */
15 bb = dd
16 /* Compile-time error: compatible parameter type(covariance), type unsafe */
17 db = bd
18 /* OK: contravariant parameter types, and compatible return type */
19 }
20
21 // Examples with lambda expressions
22 let foo1: (p: Base) => Base = (p: Base): Derived => new Derived()
23 /* OK: identical (invariant) parameter types, and compatible return type */
24
25 let foo2: (p: Base) => Base = (p: Derived): Derived => new Derived()
26 /* Compile-time error: compatible parameter type(covariance), type unsafe */
27
28 let foo2: (p: Derived) => Base = (p: Base): Derived => new Derived()
29 /* OK: contravariant parameter types, and compatible return type */
A throwing function type variable can have a non-throwing function value.
A compile-time error occurs if a throwing function value is assigned to a non-throwing function type variable.
6.5.11. Enumeration to Int Conversions¶
A value of an enumeration type is converted to type int if enumeration constants of this type are of type int.
This conversion never causes runtime errors.
1 enum IntegerEnum {a, b, c}
2 let ie: IntegerEnum = IntegerEnum.a
3 let n: number = ie // n will get the value of 0
6.5.12. Enumeration to String Conversions¶
A value of enumeration type is converted to type string if enumeration constants of this type are of type string.
This conversion never causes runtime errors.
1 enum StringEnum {a = "a", b = "b", c = "c"}
2 let se: StringEnum = StringEnum.a
3 let s: string = se // n will get the value of "a"