7. Conversions and Reflection

7.1. Autoboxing and Coercing

Coercing is the ability to implicitly cast one (primitive) type to another. Autoboxing is a special kind of coercing in that is the ability to automatically convert a primitive value type, such as string, number, or boolean, to its corresponding Object type version String, Number, Boolean. The capital letters in the latter are an essential distinction.

Conversion between primitives and object-representations of a datatype are not automatic in N4JS. Only in the cases of object-method invocations on a primitive type (for string to call "abc".length, for example) automatic conversion is applied.

Note that N4JS specific primitive types pathselector and i18nkey are handled similarly to string.

7.1.1. Coercing

In [ECMA11a], coercing is defined by means of the abstract specification method ToPrimitive [ECMA11a(p.S9.1)], also see [ECMA11a(p.S9.10)]). Other conversions, such as ToNumber, are not directly supported but reflected in the typing rules of expressions.

We express absence of automatic coercion here by means of subtype relations:

ΓBoolean:booleanΓboolean:BooleanΓNumber:numberΓnumber:NumberΓString:stringΓstring:String

and for the N4JS specific types:

ΓpathSelector<T><:string
Γi18nKey<:string

If a conversion between primitive and object type is desired, we require the user of N4JS to actively convert the values. The reason for that is the notably different behavior of object- and primitive-variants of a type in expression evaluation:

var bool: boolean = false;
var Bool: Boolean = new Boolean( false );

console.log( bool ? "true" : "false"); // prints "false"
console.log( Bool ? "true" : "false"); // prints "true"!

Conversion between a primitive type to its object-variant is achieved by the new operator. The valueOf() method converts the object-variant back to a primitive value.

// objects from literals:
var bo: Boolean = new Boolean( true ); // typeof bo: object
var no: Number = new Number( 42 ); // typeof no: object
var so: String = new String( "foo" ); // typeof so: object

// to primitive
var b: boolean = bo.valueOf(); // typeof b: boolean -- true
var n: number = no.valueOf(); // typeof n: number -- 42
var s: string = so.valueOf(); // typeof s: string -- "foo"

// to object-type
bo = new Boolean( b );
no = new Number( n );
so = new String( s );

Conversion of variables of type Object or from one primitive type to another is expressed in terms of typing rules for expressions. That is, it is not possible to convert any Object to a primitive in general, but it is possible to do so in the context of certain expressions such as additive operator. The applied conversions are described in Auto-Conversion of Class Instances

7.1.2. Autoboxing of Primitives

In [ECMA11a], autoboxing is defined by ToObject [ECMA11a(p.S9.9)].

Autoboxing is not directly supported in N4JS. Instead, primitive types virtually have the same members as their corresponding object types. It is then possible to use the Autoboxing feature when calling a member. In general, Autoboxing is only supported for accessing built-in read-only (immutable) properties. For example, "some string value".split(" "); is supported but "some string value".foo=1; will be rejected as String does not allow properties to be added (cf. String vs. String+, see [_dynamic]).

Autoboxing often leads to problems, in particular in combination with dynamic types – this is why it is not directly supported in N4JS.

var s: String+ = "Hello"; (1)

s.prop = 1;
console.log(s.prop); (2)
1 will produce an error to prevent the following scenario:
2 prints "undefined"

7.1.3. Autoboxing of Function Expressions and Declarations

Function expressions and declarations always define an object of type Function, thus coercing or Autoboxing is not required in case of functions:

It is always possible to use a function expression where a Function is required, and to use an object of type Function where a function expression is expected. This is only possible if the function signatures are subtype-compatible, see Function Type for details.

Still, it is always possible to call members of Function, e.g., function(){}.length().

7.2. Auto-Conversion of Objects

7.2.1. Auto-Conversion of Class Instances

All classes defined in N4JS modules implicitly subclass N4Object, which is a plain JavaScript Object type. The same auto-conversion rules defined for JavaScript Object therefore apply to N4Object instances as well.

The basic conversion uses the abstract JavaScript function ToPrimitive [ECMA11a(p.S9.1)], which relays on the specification method Object [ECMA11a(p.S8.12.8)]. DefaultValue calls valueOf or toString methods if they are defined by the class (in the methods-builder).

Note that according to the [ECMA11a], in most cases, objects are first converted into primitives. That is, in most cases, no extra hint is passed to DefaultValue. Thus valueOf usually takes precedence over toString as demonstrated in the following example:

Example 71. Auto-Conversion

Assume some classes and corresponding instances defined as follows:

class A {}
class B{
    @Override public toString(): string { return "MyB"}
}
class C{
    @Override public valueOf(): any { return 10}
}
class D{
    @Override public toString(): string { return "MyD"}
    @Override public valueOf(): any { return 20}
}
var a = new A(), b = new B(), c = new C(), d = new D();

Instances of these classes will be converted as demonstrated as follows:

console.log(a+"");                      // [object Object]
console.log(a+1);                       // [object Object]1

console.log(""+b+"");                   // MyB
console.log(1+b+1);                     // 1MyB1

console.log(c+"");                      // 10
console.log(c+1);                       // 11

console.log(d+"");                      // 20
console.log(d+1);                       // 21
7.2.1.1. Auto-Conversion of Interface Instances

Instances of interfaces actually are instances of classes at runtime. The auto-conversion rules described in Auto-Conversion of Class Instances are applied to instances declared as instances of interfaces as well.

7.2.2. Auto-Conversion of Enum Literals

Enumeration values are objects and thus follow the behavior for ECMAScript Object and Function. They have a custom toString method which returns the name of the enumeration value.

7.3. Type Cast and Type Check

7.3.1. Type Cast

(IDEBUG-56): Casting to TypeVars

Type casts are expressed with the cast expression (as), see Cast (As) Expression for details.

We first define helper rules for the type cast constraints as follows:

μTTEnumClassPrimitiveObjectTypeisCPOETμTClassifierTypeTypeTypeμT.typeRefTypeVariableisCPOETμTInterfaceisInterfaceTμT=TypeVariableT.upperBoundsisBoundTypeVarTμT TEnumPrimitiveμT=ClassT.extensibility=final)isFinalByTypeT

Req. IDE-89: Cast Validation At Compile Time (ver. 1)

Given a type cast expression e in which Γe.expr:S and and target type T, the following constraints must hold:

  1. T must be a classifier, enum, primitive, function type expression, classifier type, type variable, union or intersection type:

    μT{any,Class,Interface,Enum,Primitive,ObjectType,
    FunctionTypeExpression,ClassifierType,TypeVariable,Union,Intersection}

  2. if S is a subtype of T, the cast is unnecessary and a warning will be generated.

  3. if S and T are classes, enums or primitive types, then T must be a subtype of S. This is also true if T is an interface and the type of S cannot have subtypes, or vice versa.

    (isCPOET
    (isCPOES
    μS=IntersectionS'S:isCPOES')
    isInterfaceTisFinalByTypeS
    isFinalByTypeTisInterfaceS)ΓT<:S

  4. if S is a class, enum or primitive type and T is a type-variable, then for each given boundary Tiup of T of type class, enum or primitive S must be a member of the type hierarchy: [47]

    isBoundTypeVarTisCPOES
    TiupT.upperBounds(isCPOETiupΓ(Tiup<:STiup:>S))

  5. if S is a union or intersection type, then the type cast is valid if it is valid for at least one element of S.

  6. if S and T are generics, and if S0=T0, a cast is possible if type arguments are sub- or supertypes of each other: [48]

    μS=ClassifierμT=ClassifierS0=T0
    ( S.typeArgi<:T.typeArgi)( T.typeArgi<:S.typeArgi)

  7. If T is a union type, then the type cast is valid if it is valid for at least one element of T.

  8. If T is an intersection type, then the type cast is valid if it is valid for all elements of T.

any is a supertype of all other types, thus it is always possible to cast a variable of type any to other (non-composed) types.

7.3.2. Type Check

There are basically two ways of testing the type of a variable: typeof and instanceof. N4JS supports type comparison via the ECMAScript instanceof operator. The operator instanceof retains its standard ECMAScript behavior (e.g. checking whether a value is an instance of a constructor function), but has additional functionality when used with N4JS types.

When used with an N4JS class, instanceof also supports checking against an interface. For N4JS enumeration values, it can be used to check whether the value is part of a specific enumeration.

typeof only returns a string with the name of the ECMAScript type, which is Object for all class instances.

N4JS specific string types, that is pathSelector and i18nkey cannot be tested during runtime. These types, therefore, must not be used in instanceof expressions. The same is true for string-based enums and arrays which cannot be tested during runtime, thus string-based enum and array types are not permitted on the right-hand side of instancesof constructs. For all types for which the evaluation result of instanceof could be computed at compile time, the check is unnecessary and thus it is refused by the compiler. Using structural types on the right-hand side of instancesof constructs is also not permitted.

In order to avoid errors at runtime, the instanceof operator defines appropriate constraints, see Relational Expression for details.

Example 72. Type Check Example

Given the following classes and variable:

interface I{}
class S{}
class Sub extends S implements I{}

var x = new Sub();

typeof x will simply return object. The following table shows the difference between plain JavaScript instanceof and N4JS’s instanceof:

Check JavaScript N4JS

x instanceof Sub

true

true

x instanceof S

true

true

x instanceof I

false

true

7.4. Reflection meta-information

All N4JS classes, interfaces and enumerations provide meta-information that is used by the runtime and standard library. All classifiers (including enums) provide meta-information by means of a static getter n4type. Since it is static getter, it is actually an instance getter of the constructor (or classifier) of a type, which is the only way to retrieve that information in case of interfaces. For enums, this can be retrieved from instances as well.

This getter is of type N4Class which is a built-in type just like N4Object. It contains the following members:

fqn

The FQN of the type.

n4superType

The N4Class of the supertype, may be null if supertype is a not an N4Class.

allImplementedInterfaces

List of The FQN of implemented interfaces (transitively but without interfaces implemented by supertype)

get isClass

True if the type is an N4Class.

get isInterface

True if the type is an N4Interface.

7.4.1. Reflection for Classes

The meta-information for classes is available by means of N4Object’s static getter n4type. Since it is static getter, it is actually an instance getter of the constructor of a type.

In addition, the static method of in N4Type is available to retrieve the meta-information for a given instance or constructor. This also allows to retrieve meta-information directly for an instance of some class C without having the constructor of C available, for example because the constructor is not accessible.

Example 73. Reflection with N4class

This example demonstrates how these reflective features are accessed:

class A {}
class B extends A {}
var b = new B();
console.log(B.n4type.fqn);
console.log(b.constructor.n4type.fqn);
console.log(b.constructor.n4type.n4superType.fqn);
console.log(N4Type.of(b));
console.log(N4Type.of(B.n4type).fqn);

Assuming this code is defined in file A, this will output

A.B
A.B
A.A
A.B
N4BuiltInClasses.N4Class

The built-in types N4Object and N4Class are also accessible. They are not defined in a module, thus their FQN returns only their simple name.

Example 74. Reflection with Built-In Types
console.log('N4Object.n4class.fqn:      ' + N4Object.n4class.fqn)
console.log('N4Class.n4class.fqn:       ' + N4Class.n4class.fqn)

class A {}
console.log('A.n4class.fqn:             ' + A.n4class.fqn)
console.log('A.n4class.n4superType.fqn: ' + A.n4class.n4superType.fqn)

Assuming this code is defined in file A, this will output

N4Object.n4class.fqn:      N4Object
N4Class.n4class.fqn:       N4Class
A.n4class.fqn:             A.A
A.n4class.n4superType.fqn: N4Object

Note that classes extending Object do not provide the static n4class getter, hat is

class B extends Object {}
console.log('B.n4class.fqn:             ' + B.n4class.fqn)

would issue an error as cannot be resolved.

Example 75. N4Class.of

The type has a method to retrieve the meta-information from instances (i.e. or enumeration literals using ) without using the constructor.

class C { }
interface I {}  class IImpl implements I {}
enum E { L }

var c: C = new C();
var i: I = new IImpl();
var e: E = E.L;

console.log(C.n4type.fqn);
console.log(N4Class.of(c).fqn);

console.log(I.n4type.fqn);
console.log(N4Class.of(i).fqn);

console.log(E.n4type.fqn);
console.log(N4EnumType.of(e).fqn);

7.4.2. Reflection for Interfaces

The meta-information of an interface X is available via getter n4class defined in the type{X}. This field is of type N4Class as well. Since an interface cannot have a super classs, the property n4superTypes will always be empty. Calling isInterface respectively on the returned N4Class instance will return true.

7.4.3. Reflection for Enumerations

var n: number; var b: boolean; var s: string;

The meta-information for enumerations is available by means of the getter n4class, either statically by using the enumeration type or (in terms of an instance getter) via a literal. Calling isEnum on the returned N4Class instance will return true.

7.5. Conversion of primitive types

Conversion between primitives is given as follows:

var n: number; var b: boolean; var s: string;
From To Conversion Example

string

number

Number…​

n = Number("42");//42

string

boolean

N4Primitives.parseBoolean(…​)

 b=N4Primitives.parseBoolean("false");

number

boolean

Boolean(…​)

b=Boolean(17.5); //true

number

string

Number.toString()

s=42.toString(); //"42"

boolean

number

N4Primitives.toNumber(…​)

n=N4Primitives.toNumber(true);

boolean

string

Boolean.toString()

 s=true.toString();//"true" }

Remarks:

  1. ECMAScript doesn’t define explicit conversion from string content. Implicit handling states all strings with >0==true. N4Primitives.parseBoolean(x) yields true for x.trim().toLowerCase().equals("true")

  2. The call to Boolean(..) for the arguments 0, -0, null, false, NaN, undefined and "" evaluate to false. All other values evaluate to true.

  3. Number has several methods for converting a value to string [ECMA11a(p.S15.7.4)]: toExponential(), to Fixed(), toPrecision().

  4. ECMAScript doesn’t define explicit conversion from boolean to number. Implicit handling states true 1 and false 0, which N4Primitives.toNumber() yields.

Quick Links