5. Generics¶
Class, interface, method, constructor, and function are program entities that can be generalized in the ArkTS language. Generalization is parameterizing an entity by one or several types. A generalized entity is introduced by a generic declaration (called generic for brevity).
5.1. Generic Declarations¶
Types used as generic parameters in a generic are called type parameters.
A generic must be instantiated in order to be used. Generic instantiation is the action that converts a generic into a real program entity: ordinary class, interface, function, etc. Instantiation can be performed either explicitly or implicitly.
Explicit generic instantiation is the language construct that specifies real types, which substitute type parameters of a generic. Real types specified in the instantiation are called type arguments.
In an implicit instantiation, type arguments are not specified explicitly. They are inferred from the context the generic is referred in. Implicit instantiation is possible only for functions and methods.
The result of instantiation is a real, non-parameterized program entity: class, interface, method, constructor, or function. The entity is handled exactly as an ordinary class, interface, method, constructor, or function.
Conceptually, a generic class, an interface, a method, a constructor, or a function defines a set of classes, interfaces, methods, constructors, or functions respectively (see Generic Instantiations).
5.2. Generic Parameters¶
A class, an interface, or a function must be parameterized by at least one type parameter to be a generic. The type parameter is declared in the type parameter section. It can be used as an ordinary type inside a generic.
Syntactically, a type parameter is an unqualified identifier (see Scopes for the scope of type parameters). Each type parameter can have a constraint (see Type Parameter Constraint). A type parameter can have a default type (see Type Parameter Default).
typeParameters:
'<' typeParameterList '>'
;
typeParameterList:
typeParameter (',' typeParameter)*
;
typeParameter:
('in' | 'out')? identifier constraint? typeParameterDefault?
;
constraint:
'extends' typeReference | keyofType
;
typeParameterDefault:
'=' typeReference
;
A generic class, interface, method, constructor, or function defines a set of parameterized classes, interfaces, methods, constructors, or functions respectively (see Generic Instantiations). One type argument can define only one set for each possible parameterization of the type parameter section.
5.3. Type Parameter Constraint¶
If a type parameter has restrictions, or constraints, then such constraints must be followed by the corresponding type argument in a generic instantiation.
In every type parameter, a constraint can follow the keyword extends
. The
constraint is denoted as a single type parameter T. If no constraint is
declared, then the type parameter is not compatible with Object
, and
has no methods or fields available for use. Lack of constraint effectively
means extends Object|null|undefined
.
If type parameter T has type constraint S, then the actual type of the
generic instantiation must be a subtype of S. If the constraint S is a
non-nullish type (see Nullish Types), then T is non-nullish too.
If the type parameter is constrained with the keyof T, then valid
instantiations of this parameter can be the values of the union type created
from string names of T or the union type itself:
1 class Base {}
2 class Derived extends Base { }
3 class SomeType { }
4
5 class G<T extends Base> { }
6
7 let x: G<Base> // correct
8 let y: G<Derived> // also correct
9 let z: G<SomeType> // error: SomeType is not a subtype of Base
10
11 class A {
12 f1: number = 0
13 f2: string = ""
14 f3: boolean = false
15 }
16 class B<T extends keyof A> {}
17 let b1 = new B<'f1'> // OK
18 let b2 = new B<'f0'> // Compile-time error as "f0" does not satisfy the constraint 'keyof A'
19 let b3 = new B<keyof A> // OK
A type parameter of a generic can depend on another type parameter of the same generic.
If S constrains T, then the type parameter T directly depends on the type parameter S, while T directly depends on the following:
S; or
Type parameter U that depends on S.
A compile-time error occurs if a type parameter in the type parameter section depends on itself.
1 class Base {}
2 class Derived { }
3 class SomeType { }
4
5 class G<T, S extends T> {}
6
7 let x: G<Base, Derived> // correct: the second argument directly
8 // depends on the first one
9 let y: G<Base, SomeType> // error: SomeType doesn't depend on Base
10
11 class A0<T> {
12 data: T
13 constructor (p: T) { this.data = p }
14 foo () {
15 let o: Object = this.data // error: as type T is not compatible with Object
16 console.log (this.data.toString()) // error: type T has no methods or fields
17 }
18 }
19
20 class A1<T extends Object> extends A0<T> {
21 constructor (p: T) { this.data = p }
22 override foo () {
23 let o: Object = this.data // OK!
24 console.log (this.data.toString()) // OK!
25 }
26 }
5.4. Generic Instantiations¶
As mentioned before, a generic class, interface, or function declaration defines a set of corresponding non-generic entities. A generic entity must be instantiated in order to get a non-generic entity out of it. The explicit instantiation is specified by providing a list of type arguments that substitute corresponding type parameters of the generic:
G < T1, ...
, Tn>
—where <T1, ...
, Tn> is the list of type arguments
for the generic declaration G.
If C1, ...
, Cn is the constraint for the corresponding
type parameters T1, ...
, Tn of a generic declaration,
then Ti is a subtype of each constraint type Ci (see
Subtyping). All subtypes of the type listed in the corresponding
constraint have each type argument Ti of the parameterized
declaration ranging over them.
A generic instantiation G < T1, ...
, Tn> is
well-formed if all of the following is true:
The generic declaration name is G.
The number of type arguments equals that of G’s type parameters.
All type arguments are subtypes of a corresponding type parameter constraint.
A compile-time error occurs if an instantiation is not well-formed.
Unless explicitly stated otherwise in appropriate sections, this specification discusses generic versions of class type, interface type, or function.
Any two generic instantiations are considered provably distinct if:
Both are parameterizations of distinct generic declarations; or
Any of their type arguments is provably distinct.
5.5. Type Parameter Default¶
Type parameters of generic types can have defaults. This situation allows dropping a type argument when a particular type of instantiation is used. However, a compile-time error occurs if a type parameter without a default value follows a type parameter with a default value in the declaration of a generic type.
The examples below illustrate this for both classes and functions:
1 class SomeType {}
2 interface Interface <T1 = SomeType> { }
3 class Base <T2 = SomeType> { }
4 class Derived1 extends Base implements Interface { }
5 // Derived1 is semantically equivalent to Derived2
6 class Derived2 extends Base<SomeType> implements Interface<SomeType> { }
7
8 function foo<T = number>(): T {
9 // ...
10 }
11 foo() // this call is semantically equivalent to the call below
12 foo<number>()
13
14 class C1 <T1, T2 = number, T3> {}
15 // That is a compile-time error, as T2 has default but T3 does not
16
17 class C2 <T1, T2 = number, T3 = string> {}
18 let c1 = new C2<number> // equal to C2<number, number, string>
19 let c2 = new C2<number, string> // equal to C2<number, string, string>
20 let c3 = new C2<number, Object, number> // all 3 type arguments provided
5.6. Type Arguments¶
Type arguments can be reference types or wildcards.
If a value type is specified as a type argument in the generic instantiation, then the boxing conversion applies to the type (see Boxing Conversions).
typeArguments:
'<' typeArgumentList '>'
;
A compile-time error occurs if type arguments are omitted in a parameterized function.
typeArgumentList:
typeArgument (',' typeArgument)*
;
typeArgument:
typeReference
| arrayType
| wildcardType
;
wildcardType:
'in' typeReference
| 'out' typeReference?
;
The variance for type arguments can be specified with wildcards (use-site variance). It allows changing type variance of an invariant type parameter.
Note: This description of use-site variance modifiers is tentative. The details are to be specified in the future versions of ArkTS.
The syntax to signify a covariant type argument, or a wildcard with an upper bound (T is a typeReference) is as follows:
out
TThis syntax restricts the methods available, and allows accessing only the methods that do not use T, or use T in out-position.
The syntax to signify a contravariant type argument, or a wildcard with a lower bound (T is a typeReference) is as follows:
in
TThis syntax restricts the methods available, and allows accessing only the methods that do not use T, or use T in in-position.
The unbounded wildcard out
, and the wildcard out Object | null
are
equivalent.
A compile-time error occurs if:
A wildcard is used in a parameterization of a function; or
A covariant wildcard is specified for a contravariant type parameter; or
A contravariant wildcard is specified for a covariant type parameter.
The rules below apply to the subtyping (see Subtyping) of two non-equivalent types A <: B, and an invariant type parameter F in case of use-site variance:
T <out A> <: T <out B>;
T <in A> :> T <in B>;
T <A> <: T <out A>;
T <A> <: T <in A>.
Any two type arguments are considered provably distinct if:
The two arguments are not of the same type, and neither is a type parameter nor a wildcard; or
One type argument is a type parameter or a wildcard with an upper bound of S, the other T is not a type parameter and not a wildcard, and neither is a subtype of the other (see Subtyping); or
Each type argument is a type parameter, or wildcard with upper bounds S and T, and neither is a subtype of the other (see Subtyping).
5.7. Utility Types¶
ArkTS supports several embedded types, called ‘utility’ types. They allow constructing new types, and extend their functionality.
5.7.1. Partial Utility Type¶
The type Partial<T> constructs a type with all properties of T set to optional. T must be a class or an interface type:
1 interface Issue {
2 title: string
3 description: string
4 }
5
6 function process(issue: Partial<Issue>) {
7 if (issue.title != undefined) {
8 /* process title */
9 }
10 }
11
12 process({title: "aa"}) // description is undefined
In the example above, the type Partial<Issue> is transformed to a distinct type that is analogous:
1 interface /*some name*/ {
2 title?: string
3 description?: string
4 }
5.7.2. Required Utility Type¶
The type Required<T> is opposite to Partial<T>. It constructs a type with all properties of T set to be required (not optional). T must be a class or an interface type.
1 interface Issue {
2 title?: string
3 description?: string
4 }
5
6 let c: Required<Issue> = { // CTE: 'description' should be defined
7 title: "aa"
8 }
The type defined in the example above, the type Required<Issue> is transformed to a distinct type that is analogous:
1 interface /*some name*/ {
2 title: string
3 description: string
4 }
5.7.3. Readonly Utility Type¶
The type Readonly<T> constructs a type with all properties of T set to readonly. It means that the properties of the constructed value cannot be reassigned. T must be a class or an interface type:
1 interface Issue {
2 title: string
3 }
4
5 const myIssue: Readonly<Issue> = {
6 title: "One"
7 };
8
9 myIssue.title = "Two" // compile-time error: readonly property
5.7.4. Record Utility Type¶
The type Record<K, V> constructs a container that maps keys (of type K) to values (of type V).
The type K is restricted to number types, string types, union types constructed from these types, and also literals of these types.
A compile-time error occurs if any other type, or literal of any other type is used in place of this type:
1 type R1 = Record<number, string> // ok
2 type R2 = Record<boolean, string> // compile-time error
3 type R3 = Record<1 | 2, string> // ok
4 type R4 = Record<"salary" | "bonus", number> // ok
5 type R4 = Record<1 | true, number> // compile-time error
There are no restrictions on type V.
A special form of object literals is supported for instances of Record types (see Object Literal of Record Type).
Access to Record<K, V>
values is performed by an indexing expression
like r[index], where r is an instance of the type Record, and index
is the expression of type K. The result of an indexing expression is of type
V if K is a union that contains literal types only; otherwise, it is of
type V | undefined. See Record Indexing Expression for details.
1 type Keys = 'key1' | 'key2' | 'key3'
2
3 let x: Record<Keys, number> = {
4 'key1': 1,
5 'key2': 2,
6 'key3': 4,
7 }
8 console.log(x['key2']) // prints 2
9 x['key2'] = 8
10 console.log(x['key2']) // prints 8
In the example above, K is a union of literal types. The result of an indexing expression is of type V. In this case it is number.