15. Semantic Rules¶
This Chapter contains semantic rules to be used throughout the Specification document.
Note that the description of the rules is more or less informal.
Some details are omitted to simplify the understanding. See the formal language description for more information.
15.1. Subtyping¶
The subtype relationships are binary relationships of types.
The subtyping relation of S as a subtype of T is recorded as S <: T. It means that any object of type S can be safely used in any context in place of an object of type T.
By the definition of S <: T, type T belongs to the set of supertypes of type S. The set of supertypes includes all direct supertypes (see below), and all their respective supertypes.
More formally speaking, the set is obtained by reflexive and transitive closure over the direct supertype relation.
Direct supertypes of a non-generic class, or of the interface type C are all of the following:
The direct superclass of C (as mentioned in its extension clause, see Class Extension Clause) or type Object if C has no extension clause specified.
The direct superinterfaces of C (as mentioned in the implementation clause of C, see Class Implementation Clause).
Type Object if C is an interface type with no direct superinterfaces (see Superinterfaces and Subinterfaces).
Direct supertypes of the generic type C <F1,…, Fn> (for a generic class or interface type declaration C <F1,…, Fn> with n>0) are all of the following:
The direct superclass of C <F1,…, Fn>.
The direct superinterfaces of C <F1,…, Fn>.
Type Object if C <F1,…, Fn> is a generic interface type with no direct superinterfaces.
The direct supertype of a type parameter is the type specified as the constraint of that type parameter.
15.2. Override-Equivalent Signatures¶
Two functions, methods, or constructors M and N have the same signature if their names, type parameters (if any, see Generic Declarations), and formal parameter types are the same—after the formal parameter types of N are adapted to type parameters of M.
Formal definition for the same signatures is given below:
M < T1, … TMm > ( U1 , … UMn ): RM
N < T1, … TNm > ( U1 , … UNn ): RN
Mm = Nm and for any i in 1 .. Mm => Constraint ( TMi ) fits Constraint ( TNi )
Mn = Nn and for any i in 1 .. Mn => Constraint ( UMi ) fits Constraint ( UNi )
where Constraint of any type except type parameter returns the type itself, and
fits means the following:
The types are identical;
The data value sets of the first and the second ones have an intersection that leads to potential call ambiguities; or
Type T and rest parameters of T[] have a potential ambiguity.
type T1 = A | B
type T2 = A | C
// Types T1 and T2 fit each other as their data values have intersection with data values of type A
Signatures S1 and S2 are override-equivalent only if S1 and S2 are the same.
A compile-time error occurs if:
A package declares two or more functions with override-equivalent signatures.
A class declares two or more methods or constructors with override-equivalent signatures.
An interface declares two or more methods with override-equivalent signatures.
The examples below illustrate the concept:
// The same signatures
foo <T1, T2> ()
foo <G1, G2> ()
// The same number of type parameters and their constraints are identical
foo <T extends U1> (p1: U1, p2: U2)
foo <V extends U1> (r1: V, r2: U2)
/* The same number of parameters and their types are identical replacing
type parameter with its constraint */
foo (p1: U1, p2: U2): R1
foo (q1: U1, q2: U2): R2
// The same number of parameters and their types are identical
class Base {}
class Derived extends Base {}
foo (p: Base)
foo (p: Derived)
/* The same number of parameters and intersection of data values of Derived
and Base produce data set of Derived values */
foo (p: A | B)
foo (p: A | C)
/* The same number of parameters and intersection of data values of A | B
and A | C produce data set of A values */
foo (p1: String)
foo (...p2: String[])
// The same due to ambiguity of the type String and rest parameter String[]
// foo("some string") fits both signatures
// Different signatures
foo (p1: String)
foo (p2: String[])
// As signatures String and String[] do not lead to call ambiguities
// foo ("some string") fits the first signature
// foo (["some string"]) fits the second one
15.3. Compatible Signature¶
Signature S1 with n parameters is compatible with the signature S2 with m parameters if:
n <= m;
All n parameter types in S2 are compatible (see Type Compatibility) with parameter types in the same positions in S1; and
All S2 parameters in positions from m - n up to m are optional (see Optional Parameters).
A return type, if available, is present in both signatures, and the return type of S1 is compatible (see Type Compatibility) with the return type of S2.
15.4. Overload Signature Compatibility¶
If several functions, methods, or constructors share the same body (implementation) or the same method with no implementation in an interface, then all first signatures without body must fit the last signature with or without the actual implementation for the interface method. Otherwise, a compile-time error occurs.
Signature S1 with n parameters fits signature S2 if:
S1 has n parameters, S2 has m parameters; and:
n <= m;
All n parameter types in S1 are compatible (see Type Compatibility) with parameter types in the same positions in S2; and
If n < m, then all S2 parameters in positions from n + 1 up to m are optional (see Optional Parameters).
Both S1 and S2 have return types, and the return type of S2 is compatible with the return type of S1 (see Type Compatibility).
It is illustrated by the example below:
1class Base { ... }
2class Derived1 extends Base { ... }
3class Derived2 extends Base { ... }
4class SomeClass { ... }
5
6interface Base1 { ... }
7interface Base2 { ... }
8class Derived3 implements Base1, Base2 { ... }
9
10function foo (p: Derived2): Base1 // signature #1
11function foo (p: Derived1): Base2 // signature #2
12function foo (p: Derived2): Base1 // signature #1
13function foo (p: Derived1): Base2 // signature #2
14// function foo (p: SomeClass): SomeClass
15 // Error as 'SomeClass' is not compatible with 'Base'
16// function foo (p: number)
17 // Error as 'number' is not compatible with 'Base' and implicit return type 'void' also incompatible with Base
18function foo (p1: Base, p2?: SomeClass): Derived3 // // signature #3: implementation signature
19 { return p }
15.5. Type Compatibility¶
Type T1 is compatible with type T2 if:
T1 is the same as T2, or
There is an implicit conversion (see Implicit Conversions) that allows converting type T1 to type T2.
15.6. Compatibility Features¶
Some features are added to ArkTS in order to support smooth TypeScript compatibility. Using this features is not recommended in most cases while doing the ArkTS programming.
15.6.1. Extended Conditional Expressions¶
ArkTS provides extended semantics for conditional-and and conditional-or
expressions to ensure better alignment with TypeScript. It affects the semantics of
conditional expressions (see Conditional Expressions), while
and
do
statements (see while Statements and do Statements), for
statements (see for Statements), if
statements (see
if Statements), and assignment (see Simple Assignment Operator).
This approach is based on the concept of truthiness that extends the Boolean logic to operands of non-Boolean types, while the result of an operation (see Conditional-And Expression, Conditional-Or Expression, Logical Complement) is kept boolean. Depending on the kind of the value type, the value of any valid expression can be handled as true or false as described in the table below:
Value Type |
When false |
When true |
ArkTS Code |
---|---|---|---|
string |
empty string |
non-empty string |
s.length == 0 |
boolean |
false |
true |
x |
enum |
enum constant treated as ‘false’ |
enum constant treated as ‘true’ |
x.getValue() |
number (double/float) |
0 or NaN |
any other number |
n != 0 && n != NaN |
any integer type |
== 0 |
!= 0 |
i != 0 |
char |
== 0 |
!= 0 |
c != c’0’ |
let T - is any non-nullish type |
|||
T | null |
== null |
!= null |
x != null |
T | undefined |
== undefined |
!= undefined |
x != undefined |
T | undefined | null |
== undefined or == null |
!= undefined and != null |
x != undefined && x != null |
Boxed primitive type (Boolean, Char, Int …) |
primitive type is false |
primitive type is true |
new Boolean(true) == true new Int (0) == 0 |
any other nonNullish type |
never |
always |
new SomeType != null |
The example below illustrates the way this approach works in practice. Any nonzero number is handled as true. The loop continues until it becomes zero that is handled as false:
1 for (let i = 10; i; i--) {
2 console.log (i)
3 }
4 /* And the output will be
5 10
6 9
7 8
8 7
9 6
10 5
11 4
12 3
13 2
14 1
15 */