We compare DScript (Ds) and TypeScript (Ts). In particular we compare Ts scripts that are targeted for Node.js, i.e.,
scripts that will be compiled with "-m commonjs
" option.
There are plenty of differences between Ds and Ts. In this document we describe only the major differences.
In Ts, modules (or files) and namespaces are code blocks that are executed from top to bottom. The code block can consist of variable and type declarations, as well as, statements (or expression statements). In Ds, modules (or files) and namespace are unordered sets of variable and type declarations.
For example, in Ts the following script prints Hello
:
function print(s: string): number {
Console.writeLine(s); // console.log(s) -- Node.js
return s === undefined ? -1 : s.length;
}
let x = "Hello";
print(x);
In Ds, this script is syntactically invalid because print(x)
is not a declaration; it's an expression statement.
Similarly, in Ts the following script prints Hello
, but is syntactically invalid Ds script:
namespace N {
function print(s: string): number {
Console.writeLine(s); // console.log(s) -- Node.js
return s === undefined ? -1 : s.length;
}
let x = "Hello";
print(x);
}
To make the above script valid Ds script, we put print(x)
in a variable declaration:
namespace N {
function print(s: string): number {
Console.writeLine(s); // console.log(s) -- Node.js
return s === undefined ? -1 : s.length;
}
let x = "Hello";
let y = print(x);
}
Now, Ds will print Hello
.
The order of declarations/statements in Ts matters, while in Ds, the declarations are unordered.
For example, if we swap the order of declarations of x
and y
, i.e.,
namespace N {
function print(s: string): number {
Console.writeLine(s); // console.log(s) -- Node.js
return s === undefined ? -1 : s.length;
}
let y = print(x);
let x = "Hello";
}
in Ts the evaluation prints undefined
, while in Ds the evaluation prints Hello
.
In Ds, one can think of module (or file) and namespace as symbol tables mapping symbols (the left-hand side variables) to expressions.
In Ds the evaluations of the right-hand side expressions of variable declarations are cached and on-demand. Let's have two files:
// file: project1.ts
import * as N from './project2.ts';
let x = N.y + N.y;
// file: project2.ts
function print(s: string): number {
Console.writeLine(s); // console.log(s) -- Node.js
return s === undefined ? -1 : s.length;
}
let x = "Hello";
export let y = print(x);
let z = "World";
export let w = print(z);
Let's evaluate project1.ts
. In Ts the evaluation prints Hello
followed by World
in the subsequent line, while
in Ds the evaluation prints only Hello
. That is, since w
in project2.ts
is not needed by project1.ts
,
the evaluation doesn't print World
. Moreover, although there are two references of N.y
in project1.ts
, y
in
project2.ts
is not evaluated twice. It's evaluated once, and the result is cached.
In Ts, namespaces can be nested, e.g.,
namespace M {
namespace N {
}
}
which can be written as: where
namespace M.N {
}
In Ts, namespaces are mergable, e.g.,
namespace M {
let w = -1;
}
namespace M.N {
let x = 0;
}
namespace M.N {
let y = 1;
}
namespace M.O {
let z = N.y;
}
can be written as
namespace M {
let w = -1;
namespace N {
let x = 0;
let y = 1;
}
namespace O {
let z = N.y;
}
}
In Ds, namespaces are flat. A namespace in Ds can have dotted identifiers, like M.N
, as its name, but that does not mean
that there is a namespace N
nested inside a namespace M
. One can think of the namespace as having the string "M.N"
as
its name. Such a name is called a namespace identifier in Ds.
One can still write namespaces like
namespace M {
let w = -1;
namespace N {
let x = 0;
let y = 1;
}
namespace O {
let z = M.N.y;
}
}
but Ds will translate it to
namespace M {
let w = -1;
}
namespace M.N {
let x = 0;
}
namespace M.N {
let y = 1;
}
namespace M.O {
let z = M.N.y;
}
Note that, in the declaration of z
, we can no longer refer to y
as N.y
because there is no namespace with name N
; y
resides
in a namespace name M.N
.
In Ds there are two kinds of identifier: standard identifier and namespace identifier. A standard identifier is like identifiers in typical programming languages, but it cannot start with an upper-case letter. This kind of identifier is used by variable and function declarations. A namespace identifier is dotted identifiers, each starts with an upper-case letter, as exemplified in the previous section. Thus, this script
namespace m {
let X = 0;
}
is not a valid Ds script because the namespace name starts with a small-case letter m
, and the declared variable X
starts with
an upper-case letter.
One peculiar aspect of having module identifiers is the selector/projection operator <expr>.selector
. The projection x.p
consists
of x
as the lhs expression and p
as the selector, and x
should evaluate to an javascript object literal { ... }
. If p
is not in that object literal, then the evaluation results in undefined
. The projection M.N.x
consists of M.N
as the lhs expression,
and x
as the selector. M.N
should evaluate to a namespace, and x
must exist in that namespace, otherwise an error is thrown.
Enum types are treated as namespaces, and as a consequence, its enum elements cannot start with upper-case letters, e.g.,
enum E {
e1,
e2,
e3
}
In Ts, all imports must be named. Let's have three scripts:
// file: project1.ts
import * as Other from './project2.ts';
namespace P1 {
export let x = Other.P2.y + Other.w;
}
// file: project2.ts
import {P3 as OtherP, v as otherV} from './project3.ts';
export let w = 7;
namespace P2 {
export let y = 1;
export let p3z = OtherP.z;
}
// file: project3.ts
export let v = 13;
namespace P3 {
export let z = 0;
}
In project1.ts
we import the file project2.ts
as a module, and name it Other
. In project2.ts
we import project3.ts
,
but we specifically choose to import the namespace P3
and the variable v
, and name them OtherP
and otherV
, respectively.
In Ds we introduce the infamous unnamed import import * from <file>
. Now, we can modify project1.ts
as follows:
// file: project1.ts
import * from './project2.ts';
namespace P1 {
export let x = P2.y + w;
}
Suddenly all exported values in project2.ts
become available in project1.ts
. One can think of this unnamed import as C
's #include
.
Admittedly, unnamed imports makes authoring build spec convenient, and have been used pervasively during our self-host effort.
For convenience, unlike Ts, Ds allows an import to be re-exported. Let's change project2.ts
as follows:
// file: project2.ts
export import {P3 as OtherP, v as otherV} from './project3.ts';
export import * from './project4.ts';
export let w = 7;
namespace P2 {
export let y = 1;
export let p3z = OtherP.z;
}
where project4.ts
is as follows:
// file: project3.ts
export var p = 0;
namespace P4 {
export let q = 0;
}
The export
modifier of import
means re-export what are imported by the import
declaration. Now, in project1.ts
, we can access
more values as shown below:
// file: project1.ts
import * from './project2.ts';
namespace P1 {
export let x = P2.y + w + OtherP.z + otherV + p + P4.q;
}
To achieve the same thing in Ts, one has to re-export manually all imported values, which can be cumbersome.
Note that, with unnamed imports and re-exported imports, we completely lose discoverability of values. For example, if you look at project1.ts
only,
you have to follow the transitive relation of export-import to find where p
is declared.
In Ts, when you target Node.js, imports do not cause namespace merging because all imports are named. For example,
// file: project1.ts
import {N} from './project2.ts';
namespace N {
export let x = y + z;
export let z = 42;
}
// file: project2.ts
namespace N {
export let y = 0;
export let z = 0;
}
causes duplicate declaration of N
in project1.ts
. In Ds, we merge the namespace N
in project1.ts
. Thus, for the expression y + z
in project1.ts
, y
resolves to N.y
in project2.ts
, while z
resolves to the N.z
in project1.ts
.
Let's extend the example a bit to three files:
// file: project1.ts
import {N} from './project2.ts';
import {N} from './project3.ts';
namespace N {
export let x = y + z + w;
export let z = 42;
}
// file: project2.ts
namespace N {
export let y = 0;
export let z = 0;
}
// file: project3.ts
namespace N {
export let w = 0;
}
Due to merging of namespaces in project1.ts
, w
in y + z + w
resolves to N.w
in project3.ts
. If we now slightly change project3.ts
to
// file: project3.ts
namespace N {
export let y = 0;
}
then the resolution of y
in y + z + w
results in ambiguity; is it N.y
in project2.ts
or in project3.ts
? But if, instead of y + z + w
,
we just have z + w
, then the evaluation in Ds doesn't give rise to an error. That is, ambiguous references in Ds are detected lazily upon uses.
Ds' imports can be parameterized with qualifiers. For example:
import * as X86 from './detours.ts' with {platform: "x86"};
import * as X64 from './detours.ts' with {platform: "x64"};
namespace N {
export let dlls = {x86: X86.dll, x64: X64.dll};
}
In Ds, globally declared variables are not modifiable. For example, this script
let x = 0;
let x = 1;
results in duplicate declaration in Ds, while in Ts, you can modify x
as follows:
let x = 0;
x = 1;
console.log(x);
The above script prints 1
.
In Ds, in a function body, one can only modify the values of local variables. That is, the left-hand side of an assignment must be an identifier expression. Object and arrays are immutable in Ds. For example,
function f(arg) {
arg = 42; // OK
let x = 0;
x = 7; // OK
let array = [1, 2, 3];
array[0] = 0; // Not OK.
array = [4, 5, 6]; // OK.
let o = { a: 1, b: 2 };
o.b = 3 // Not OK.
o = { a: 4, b: 5 }; // OK
}
Captured variables are not modifiable by the capturing lambda. For example:
function f() {
let x = 0;
let g = () => { let y = x; return y; }; // OK
let h = () => { let y = 0; x = y; return y; }; // Not OK, x is captured and modified.
}
Unlike Ts, Ds distinguishes paths from strings, syntactically and semantically. Paths cannot be converted to strings, and strings
cannot be converted to paths. Paths in Ds are absolute. Ds denotes paths by single quotes '
and strings by double quotes "
.
In a file C:/path/to/file.ts
, an absolute path can be written as a relative path, for example, in this script
// file: C:/path/to/file.ts
let x = 'path/to/somewhere';
let y = "path/to/somewhere";
x
evaluates to the absolute path 'C:/path/to/relative/to/somewhere'
, while y
evaluates to string "path/to/somewhere"
.
A relative denotation of path in Ds can start with '/'
, e.g., '/path/to/another'
. The leading /
means the directory of
the root of the file. The root in Ds is identified by the configuration file or a package file.
Ds also distinguishes path atoms from strings. That is, in the following script:
let x = 'path/to/somewhere';
let y = x.getName() === "somewhere";
y
evaluates to false
because x.getName()
evaluates to the path atom somewhere
which is different from string "somewhere"
.
To create a path atom, one uses PathAtom.create("somewhere")
, and then in this script
let x = 'path/to/somewhere';
let y = x.getName() === PathAtom.create("somewhere");
y
evaluates to true
.
Distinguishing paths and path atoms from strings is beneficial for cross platform; one OS compares files case-insensitively, while others case-sensitively. Moreover, it is also important for (distributed) caches.
Some minor differences:
- Numbers are integers in Ds, but double precision float in Ts.
Ds has a well-defined build organization consisting of configuration, package/package descriptor, and projects. Ts is not a build system, and hence has no build organization.
This document represents thoughts and requirements related to the DScript compatibilities with TypeScript and JavaScript features.
TODO: change the structure of this document to reflect TypeScript spec structure. This will provide more systematic overview of the language and will simplify readability and future changes.
Additional information about TypeScript features can be found in the TypeScript Specification.
DScript is a subset of the TypeScript language with minor (if any) syntax differences. Although the syntax is compatible, semantic could be different from TypeScript language in some regards.
DScript is a subset of the TypeScript with stronger requirements for type safety and immutability.
All objects in DScript considered immutable (or, rather, init-once). Unfortunately there is no way to express immutability in TypeScript type system, so we'll make additional changes to TypeScript compiler/IDE integration to enforce immutability (TBD: should we check immutability in runtime? We'll definitely 'fix' TypeScript compiler to emit errors in the IDE for any assignments but runtime semantic is still not clear for me!)
As a general rule, any uses of non-supported features should lead to build failure (TBD: should we separate 'compile-time build failure' from 'runtime build failure'?) and should lead to errors from custom language service in IDE.
Automatic Semicolon Insertion at ECMA-262 spec section 7.9.
JavaScript and TypeScript has special rules around statement separation, EOL and ';'. In most cases semicolon is optional and EOL symbol could be used to determine a new statement in those languages. But there are cases where JavaScript/TypeScript compilers DOES NOT consider EOL as a statements terminator. This is well-known as Automatic Semicolon Insertion (ASI). Consider the following example:
// define a function
var fn = function () {
//...
} // semicolon missing at this line
// then execute some code inside a closure
(function () {
//...
})();
In this case TS/JS compilers will treat this code as one statement because open EOL + paren (
is not considered as a statement separator in JavaScript.
To avoid this confusion and complications, DScript requires to use semicolon to denote different statements. This lead to minor syntax inconsistency between DScript and TypeScript, but the team intentionally decided to go in this direction to simplify implementation and reduce potential confusion for the user.
This means that following code has different number of statements in DScript and in TypeScript:
let x = 21;
let y = x * 2
+x;
In TypeScript/JavaScript there is 3 statements, but in DScript there are only 2 of them. This difference means that running this code will produce different results. In TypeScript y
would be 42
but in DScript it would be 63
.
TODO: add a set of rules for this.
Primitive Types at TypeScript specification.
DScript has almost the same set of primitive types that TypeScript has, but with minor differences.
The Number Type at TypeScript specification.
The Number
primitive type corresponds to the System.Int32
type from .NET Framework and represents signed 32-bit integer.
Using floating point literals in any form will lead to an error.
Unlike TypeScript/JavaScript all arithmetic operations on numbers are running in 'checked' context. If the result of an operation does not fits in 32 bits integer, runtime exception will occur.
The Boolean Type at TypeScript specification.
The Boolean
type corresponds to the similar JavaScript/TypeScript primitive type and represents logical values that are either true
or false
.
The String Type at TypeScript specification.
The String
primitive type corresponds to the similar JavaScript/TypeScript primitive type and represents an immutable sequence of characters stored as Unicode UTF-16 code units.
String literals could be restricted to double-quotes and backticks only and single quotes could be used to represent a Path
type!
The Symbol Type at TypeScript specification.
The Symbol
type is not supported for now.
The Path
type is a new primitive type introduced by DScript.
Currently the Path
literal is represented by the custom format function name P
and later single-quoted strings could be used instead. To support single-quoted string as path literals we will add an extension to the typescript intellisense in the IDE's to support the user with this.
The Void Type at TypeScript specification.
The void
type, referenced by the void
keyword, represents the absence of the value and is used as the return type of the function with no return value.
Unlike TypeScript, DScript disallows variable with void
type. Following code is a valid TypeScript code but should lead to an error from DScript:
let x: void;
The Null Type at TypeScript specification.
The Null
type corresponds to the similar named JavaScript primitive type and it is not supported by the DScript. DScript users should use undefined
to denote absence of the value.
The Undefined Type at TypeScript specification.
The Undefined
type corresponds to the similarly named JavaScript primitive type and is the type of the undefined
literal. This is the only way to represent absence of a value in DScript.
RegExp objects at ECMA Script specification.
JavaScript and TypeScript has an ability to use special string literals (wrapped in slashes, '/') for regular expressions:
// Looking for 0 or more digits in case-insensitive manner
let regEx = /\d*/i
// matched
console.log(`'42' is '${regEx.test('42')}'`);
// not matched
console.log(`'not a number' is '${regEx.test('not a number')}'`);
DScript does not support this feature, but should emit a clear error message for them.
Notes for implementer: potentially, this could be a tricky deal, because regular expression literals introduces ambiguity. For instance, following regular expression literal contains a closing comment token: /\d+*/i
.
TypeScript and JavaScript has a notion of primitive types, aliases and wrapper objects. From TypeScript/JavaScript perspective String
is a primitive type and string
is an alias. Primitive types and aliases are very similar concepts but they have semantic differences.
TypeScript assignment compatibility at TypeScript specification.
From TypeScript perspective primitive types and aliases are almost interchangeable and equivalent from runtime perspective:
let n1: number = 42;
let n2: Number = 42;
let s1: string = "foo";
let s2: String = "foo";
But primitive types and aliases are not equivalent from assignment compatibility:
let n1: number = 42;
let n2: Number = 42;
let n3: Number = n1; // OK
let n4: number = n2; // Error "Type 'Number' is not assignable to type 'number'"
Primitive types and aliases are very similar from type system perspective, but they're not the same when instances are involved created via new
operator.
Consider following examples:
let b = new Boolean(false);
if (b) {
console.log('true');
}
else {
console.log('false');
}
Previous code will print true
because any objects (and instances of primitive types are objects) are Truthy.
The same equality issues could be faced with other primitive types as well:
let n1: Number = 42;
let n2: Number = new Number(42);
let n3: number = 42;
alert(n1 == n2); // true
alert(n1 === n2); // false
alert(n1 === n3); // true
To avoid confusion and unnecessary complexity, DScript will only allow aliases like number
, string
, boolean
and path
but not equivalent primitive types - Number
, String
, Boolean
or Path
.
Binary Operators at TypeScript specification.
DScript does support only small subset of all available TypeScript/JavaScript operators. Following sections describes what operators are supported and what are not.
The binary *
operator supported only for numbers. For any other types it compile time error will occur (including any
).
The binary +
operator supported only for numbers and strings. Using this operator with any other types (including any
) will lead to compile-time error.
Any attempts to mix types in this operators (including strings and numbers) also lead to an error.
// Numbers
let x = 42;
let n1 = x + 2; // OK
let n2 = x + '2'; // Compile time error
let n3 = x + true; // Compile time error
let n4 = x + undefined; // Compile time error
let y : number = undefined;
let n5 = x + y; // Runtime error, n5 IS NOT undefined!
// Strings
let s1 = y.toString() + '2'; // OK
let s2 = y.toString() + 2; // Compile time error
+
operator can't be used with enums as well:
// Enums
let e = CustomEnum.Value1;
let n6 = x + e; // Compile time error!
Please note, that using +
operator when any operand is undefined
will lead to an error during runtime!
Those binary operators are not supported. Yes, multiplication is supported but division - is not! The reason for this decision is rounding problem.
Unary +
and unary -
are not supported.
Following operators are supported only on numbers and const enum members: <<
, >>
, |
, &
, ^
.
TBD: we should decide which version of the right shift to support: >>
or >>>
. First uses sign bit and another one - 0
for shifted bits.
TBD: does it means that for consistency reasons we should support arithmetic operations on enum values? Or enums will have only bitwise operators?
- Following operators are supported:
===
,!===
,<
,<=
,>
,>=
. TODO: Add description for<
operators: how they'll behave for strings. - Following operators are not supported:
==
,!=
.
Logical operators (&&
and ||
) in boolean context (if statement, ternary operator, while loops) are supported only for booleans. There is no implicit conversion from objects to booleans!. Using this operator with any other types (including any
) else inside boolean context will lead to compile time error.
Outside the boolean context, following operators could be used to emulate Null-coalescing operator a.k.a. ??
and Null-propagating operator a.k.a. ?.
:
let obj = {
x: {
y: 42,
z: undefined,
}
}
let z = (obj && obj.x && obj.x.z) || 0;
This code is semantically equivalent to the following C# code:
var obj = new { X = new { Z = (int?)null }};
var z = obj?.X?.Z ?? 0;
Please note, that operators &&
and ||
should follow common short-circuiting rules.
The Conditional operator at TypeScript specification.
Conditional operator is supported by DScript but requires that test
should be a boolean expression: test ? foo : boo
Assignment Operators at TypeScript specification.
An assignment of the form v = expr
requires v
to be an identifier and expr
should be assignable to the type of v
(see Assignment Compatibility section), otherwise compile-time error occurs.
TODO: refine this section. The idea that types in DScript should be immutable (or should allow one-time-assignment, because object literals technically uses assignment as well).
TODO: add this as a valid case: a = b = 42;
Those operators are not supported in DScript.
All compound assignment operators (*=
, /=
, %=
, +=
, -=
, <<=
, >>=
, >>>=
, &=
, ^=
, |=
) are prohibited in DScript.
Destructuring Assignment at TypeScript specification.
A destructuring assignment is an assignment operation in which the left hand operand is a destructuring assignment pattern as defined by the AssignmentPattern production in the ECMAScript 6 specification.
Destructuring Assignment is not supported in DScript. At least in the near future.
- Comma operator (
,
): NOT SUPPORTED. instanceof
: NOT SUPPORTED.typeof
: SUPPORTED. (TBD: Maybe we need to support this if we need it for typesafe module declaration. I saw an example of typeof in our examples).void
: NOT SUPPORTED. Allows to discard expression results.in
: NOT SUPPORTED. Allows to check that property exists in the object (let b = "foo" in customObject;
).delete
: NOT SUPPORTED. This is a JavaScript operator that allows to delete properties from objects.
Following literals are supported by DScript:
- Decimal literals (only based on 10)
- String Literals
'Single-quoted string'
"Double-quoted string"
Backticked string with interpolation
- Path Literal
- P
foo.xml
(this could be changed to single-quoted string in the future).
- P
- Object Literals (with some restrictions) (Object Literals at TypeScript specification).
Following literals are not supported:
- Floating points (in any notation)
- Regular Expression Literals
Object types are composed from properties, call signatures, and index signatures, collectively called members.
Class and interface type references, array types, tuple types, function types, and constructor types are all classified as object types.
Not all of the existing object types from TypeScript/JavaScript are supported in the DScript. TODO: add more stuff here. We can mention key concepts here as well, like union types, interfaces lack of classes etc.
Structuring Subtyping at TypeScript specification.
DScript inherits structural typing nature from TypeScript. This has its own pros and cons and DScript (and their users) should live with that.
In structural typing world notions of sub-type/super-type/type-equality are not based on explicit inheritance chain and name equivalence. The easiest way to describe structural typing is to show few examples:
// 1. Type equivalence
interface Foo {
x: number;
}
function foo(f: Foo): void {}
// Object literal is assignable to Foo interface
// because both of them have the same structure
foo({x: 42});
// 2. Subtyping
// Newly created object is a subtype of Foo because
// it declares at least the same members that are required by Foo
// TODO: this will lead to an error with TypeScript 1.6! Refine the sample!
foo({x: 42, y: -1});
The same idea is working for functions as well:
function foo(x: number, y: number): number {return 42;}
let f1 : (x: number, y: number, z: string) => void;
// f1 is compatible with foo
f1 = foo;
let f2: (x: number) => void;
// f2 is incompatible with foo
f2 = foo;
let f3: (x: number, y: number) => string;
// f3 is incompatible with foo, because there is no
// sub-type relationship between return types string and number
f3 = foo;
interface Foo {
print(f: Foo);
}
let fooObj : Foo = {
// print() is compatible with foo(Foo)!
print() {}
}
Assignment Compatibility at TypeScript specification.
TODO: Finish this section! Just to note, assignment compatibility is relies on structuring typing.
TypeScript type system is not sound and there is a lots of ways how to violate type safety. One way to do this is by using optional properties:
interface Foo {
x?: number;
}
interface Boo {
x: number;
}
function consumeBoo(b: Boo) {}
// TODO: It seems that this behavior has been changed in 1.6! Double check this example!
let f : Foo = {
}
// Argument of type Foo is not assignable to parameter of type Boo
consumeBoo(f);
// Just fine!
consumeBoo({x: f.x});
TBD: are we going to restrict this behavior in runtime somehow?
Excess Properties at TypeScript specification.
The subtype and assignment compatibility relationships require that source types have no excess properties with respect to their target types. The purpose of this check is to detect excess or misspelled properties in object literals.
Consider following example. Suppose we have following API for running C# compiler:
interface CscArguments {
sources: File[],
references?: File[],
}
function compile(args: CscArguments): Result {
// ... implementation
}
Starting from TypeScript 1.6 user would not be able specify any properties that are not in CscArguments
during compile
method call:
// OK
let ok = Csc.compile({
sources: ['foo.cs']
});
// Error, there is no property called refs!
let failure = Csc.compile({
sources: ['foo.cs'],
refs: ['System.dll']
});
Array Types at TypeScript specification.
Array types represent immutable arrays in the DScript. Unlike JavaScript/TypeScript arrays in DScript are immutable and all mutation functions always producing a new instance.
Any attempts to mutate an array instance via ar[0] = 42
will lead to compile-time error from both DScript interpreter and from custom DScript language service.
To avoid common errors with immutable types, custom analysis could be added to emit warning/error when the result of the operation is ignored.
For instance, DScript interpreter, custom language service or both should emit warning/error for this code:
let ar = [42];
// push in DScript returns new array
// Warning/Error should be provided to the user: return object was ignored for pure operation.
ar.push(1);
Array literals can contain spread elements to invoke concat
method. For example, following assignments:
let s = ['file1.cs'];
let sources = ['file2.cs', 'file3.cs', ...s, 'file4.cs'];
Are equivalent to:
let s = ['file1.cs'];
let sources = ['file2.cs', 'file3.cs'].concat(s, ['file4.cs']);
Null-ness semantic and spread operator
Due to ECMA Script specification (TODO: proof?) when spread object is null
or undefined
runtime exception should occur. In current TypeScript implementation (version 1.6) null
/undefined
element would be inserted into the target array.
TBD: DScript should decide on runtime behavior: We can treat undefined
arrays as an empty array (this can simplify some use cases) or follow ECMA Script semantic and throw runtime error.
Team is considering to add special array type that should hold at least one element. This is TBD for now.
Classes at TypeScript specification.
Classes are not supported by DScript.
Interfaces at TypeScript specification.
Interfaces are the main building blocks for DScript and they provide the ability to name and parameterize object types and to compose existing named object types into new ones.
DScript inherits structural nature of the type system and follows TypeScript rules in terms of assignment compatibility and type inference. Also, interfaces in DScript (as in TypeScript) are "open-ended". This means that interface declarations with the same qualified name relative to a common root contributes to a single interface.
DScript supports following object literals:
interface Foo {
"x": number;
}
let f : Foo = {
"x": 42,
}
DScript DOES NOT SUPPORT brackets ([]
) to access interface members:
interface Foo {
x: number;
}
let f : Foo = {
x: 42;
}
console.log(f['x']);
(Please note, that TypeScript still checks and infers types for this type of property access, but it gives up if the property is unknown).
TypeScript/JavaScript supports getter and setters in object literals. Consider the following example:
interface Foo {
x: number;
}
function print(f: Foo): void {
console.log(`x: ${f.x}`);
}
let f = {
// Defining read-only property
get x() {return 42;}
}
print(f);
This code is absolutely legitimate, but DScript does not support it.
Interfaces in TypeScript could behave similar to functors from C++, i.e. they could be callable:
interface Func<T> {
() : T;
}
// Callable interfaces are assignable from functions with compatible signature
let f : Func<number> = () => 42;
f(); //42
But more interesting stuff will happen when callable interfaces will declare additional members:
interface Callable {
x: number;
() : number;
}
let stub : any = () => 42;
stub.x = -1;
let callable : Callable = stub;
callable(); // 42
callable.x; // -1
To avoid potential confusion callable interfaces are not supported by DScript.
In TypeScript interfaces could be declared in a local scope. This feature is not supported in DScript.
The this
Keyword at TypeScript specification.
The this
keyword is prohibited in DScript.
There are few reasons for that. First of all, this
keyword is ubiquitous in OO world and because DScript does not support classes, this concept is no longer critical for DScript. this
keyword also can be used in any function declaration and this feature is a well-known source of confusion in both JavaScript and TypeScript.
Even outside a free functions this
keyword could be confusing. For instance, interface methods could be passed around (they are a first class citizens) and legitimate this
keyword will lead to weird behavior in runtime. Consider the following example:
interface Foo {
x: number;
y: number;
print();
}
let f : Foo = {
x: 42,
y: -1,
print() {
console.log(`x: ${this.x}, y: ${this.y}`);
}
}
Calling f.print()
will lead to appropriate and highly expected result but user still can capture print
method and assign it to any other compatible function:
let func : () => void;
func = f.print;
// x: undefined, y: undefined
func();
To avoid such confusion, this
keyword is prohibited.
To emulate member functions, following pattern could be used:
interface Foo {
x: number;
y: number;
print(f: Foo);
}
let f : Foo = {
x: 42,
y: -1,
print(self: Foo) {
alert(`x: ${f.x}, y: ${f.y}`);
}
}
f.print(f);
The new
Operator at TypeScript specification.
DScript does not support classes so new
operator is not supported as well.
This means that following code is also illegal:
// Constructors for wrapper objects are not supported
let s = new String();
// Custom factory methods are not supported
interface CustomFactory {
new(n: number): string;
}
function foo(f: CustomFactory) : string {
return new f(42);
}
TypeScript/JavaScript uses new
keyword to instantiate object of a wrapper types. Wrapper objects are very close to primitive types but has slightly different semantic. This code will print 'false' because wrapper objects are not the same as regular primitive types. The same is true for Truthiness/Falsiness, following code is truthy:
let b = new Boolean(false);
if (b) {
console.log('truthy');
}
To prevent such kind of confusion, calls to wrapper's object constructors explicitly should lead to an error in DScript!
Generic Types and Functions at TypeScript specification.
TODO: add more details here, generics are complex!
Union Types at TypeScript specification.
TypeScript support a way to declare union types:
type Arg = string | number;
type Text = string | { text: string };
This is very useful way of composition and DScript supports this.
type CustomString = string;
Intersection Types at TypeScript specification
DScript does not support intersection types, at least for now. For more information see 3.5 Intersection Types at TypeScript specification.
Tuple Types at TypeScript specification.
Tuple types represent JavaScript arrays with individually tracked element types. Currently this feature is not supported by TypeScript. For more information, see 3.3.3 Tuple Types.
TBD: This feature could be useful especially for our immutable design. Consider pop
method from array that can return a tuple: last element and new array.
Function Types at TypeScript specification.
An object type containing one or more call signatures is said to be a function type. DScript supports only function type literals and arrow-functions but with some restrictions: Function type literals could be used only at the top level and could not be declared in inner scopes. In this case arrow-functions should be used!
Consider the following examples:
// Foo.ds
// Allowed: top-level scope!
function foo() {return 42;}
function foo2() {
// Is not allowed! Only arrow-functions could be used in the inner contexts!
function innerFoo() {return 42;}
}
function foo3() {
// Allowed! Arrow-functions could be declared inside other functions!
let lambda = () => 42;
}
TODO: if we're restricting the language and does not allow this
keyword, do we really need this restriction for inner functions?
Overload Resolution at TypeScript specification.
DScript does not support overloads.
DScript does support default arguments.
Arrow Functions at TypeScript specification.
Just for the sake of completeness, consider the following valid TypeScript code (that could be used as an additional test case for DScript):
function foo() {return 42;}
let f : () => number = foo;
f(); //42
This code is valid, because () => number
is a part of type declaration.
TBD: parameters reassignment What would DScript do in a following case:
function foo(x: number): void {
x = 42; //??
}
If this would be an error, then DScript should allow only const
declarations and prohibit even let
s. But if it would be possible outer variable should not be modified.
Constructor Function Types at TypeScript specification.
Constructor types and constructor literals are not supported in DScript.
Enums at TypeScript specification.
TypeScript support two types of enums: regular enums and const enums. To avoid potential confusion only const enums are supported by the DScript. Regular enums allow arbitrary (non-compile-time) expression on the right hand side, but const enums supports only compile-time-constants. Consider the following example (typescript):
let x = 0;
function getValue() {return x++;}
enum CustomEnum {
Value1 = getValue(),
Value2 = getValue(),
Value3 = getValue(),
}
enum ConstCustomEnum {
// In 'const' enum declarations member initialization must be constant expression
Value1 = getValue(),
}
Please note, that 'flag' enums are still supported in a const enums:
enum CustomEnum {
None,
First = 1,
Second = 2,
FirstAndSecond = First | Second,
Third = 4,
Fourth = 8,
All = FirstAndSecond | Third | Fourth,
}
let x = CustomEnum.All;
if ((x & CustomEnum.All) === CustomEnum.All) {
console.log("All bits were set!")
}
Instead of using bitwise operator ambient function could be introduced in the prelude: Enum.hasFlag(x, CustomEnum.all)
.
TODO: previous statement is subjective. Discussion is required. Maybe we would be able to extend the type system to support hasFlag
natively.
TBD: still unclear relationship between enums and numbers. Would DScript support implicit conversion from enum values to numbers?
Type Assertions at TypeScript specification.
TypeScript supports 3 different ways to check/change the object type:
- Cast operator (
<SomeType>expr
) - As operator (
expr as SomeType
) - instanceof operator (
expr instanceof SomeType ? expr.member : 'unknown'
)
DScript (at least at the beginning) supports only the first version.
In JavaScript/TypeScript constructors are not regular functions in many aspects, and one of them is 'braces rule'. For instance, following code is equivalent:
let o1 = new Object();
let o2 = new Object;
Although DScript will emit an error for primitive type constructor calls, parser should recognize previous constructs properly and emit the same error in both cases.
Scopes at TypeScript specification.
TypeScript support 3 ways of local declarations: var
, let
and const
.
DScript does not support var
and will force to use ES6 features with more predictable behavior (just to remind, var
declarations are lifted at the beginning of the function/module and are not lexically scoped).
This restriction will prohibit some silly errors like this:
let y = x + 2;
var z = 42;
In TypeScript compiles this code and y
would be undefined
during runtime. Changing z
declaration from var
to let
will lead to compile-time error.
TBD: what about assignments? Are we support following code:
let x = 42;
x = 36;
If not, then only const
declarations should be allowed.
This section is under design and implementation right now. DScript has slightly different requirements and existing TypeScript module amd namespace concepts could not be used as is..
TypeScript support variable number of arguments using following syntax:
function push<T>(...args: T[]) {}
This helps to pass arbitrary number of arguments:
push(1, 2, 3);
push(1);
But the user can't pass an array instance directly. In this case spread operator is required:
let ar = [1,2,3];
push(ar); // compile-time error
push(...ar); // fine!
This is a very powerful feature that will be used all over the places for fun consumers.
for
loop: SUPOPRTED (For statement at TypeScript specification)for ... of
: SUPPORTED (this one is similar toforeach
construct from C#) (For-Of Statement at TypeScript specification)switch
: SUPPORTED (TODO: maybe even with additional checks for completeness for enums?) (Switch Statement at TypeScript specification)for .. in
: NOT SUPPORTED (For-In Statement at TypeScript specification.)while
anddo ... while
- NOT SUPPORTED (If, Do, and While Statements at TypeScript specification)
In TypeScript/JavaScript labels could be used with if
, switch
, loops. DScript does not support them at all.
No try/catch/finally
in DScript. (Try Statement at TypeScript specification)
- Iterators/Generators: NO (Generator Functions)
- async/await: NO
- Triple slash references: YES
- Ambients: YES (Ambients at TypeScript specification)
DScript does not support majority of JavaScript features.
- Global variables: NO
- Type Coercion: NO
arguments
: NOeval
: NOwith
: NO- getters/setters in object literals: NO
- Prototypes: NO