Understanding TypeScript
Understanding TypeScript
1 Introduction
Despite its success, JavaScript remains a poor language for developing and
maintaining large applications. TypeScript is an extension of JavaScript in-
tended to address this deficiency. Syntactically, TypeScript is a superset of Ec-
maScript 5, so every JavaScript program is a TypeScript program. TypeScript
enriches JavaScript with a module system, classes, interfaces, and a static type
system. As TypeScript aims to provide lightweight assistance to programmers,
the module system and the type system are flexible and easy to use. In partic-
ular, they support many common JavaScript programming practices. They also
enable tooling and IDE experiences previously associated with languages such as
C and Java. For instance, the types help catch mistakes statically, and enable
other support for program development (for example, suggesting what methods
might be called on an object). The support for classes is aligned with proposals
currently being standardized for EcmaScript 6.
The TypeScript compiler checks TypeScript programs and emits JavaScript, so
the programs can immediately run in a huge range of execution environments. The
compiler is used extensively in Microsoft to author significant JavaScript applica-
tions. For example, recently1 Microsoft gave details of two substantial TypeScript
This work was done at Microsoft Research, Cambridge.
1
http://blogs.msdn.com/b/typescript/
projects: Monaco, an online code editor, which is around 225kloc, and XBox Mu-
sic, a music service, which is around 160kloc. Since its announcement in late 2012,
the compiler has also been used outside Microsoft, and it is open-source.
The TypeScript type system comprises a number of advanced constructs and
concepts. These include structural type equivalence (rather than by-name type
equivalence), types for object-based programming (as in object calculi), gradual
typing (in the style of Siek and Taha [14]), subtyping of recursive types, and type
operators. Collectively, these features should contribute greatly to a harmonious
programming experience. One may wonder, still, how they can be made to fit
with common JavaScript idioms and codebases. We regard the resolution of this
question as one of the main themes in the design of TypeScript.
Interestingly, the designers of TypeScript made a conscious decision not to
insist on static soundness. In other words, it is possible for a program, even one
with abundant type annotations, to pass the TypeScript typechecker but to fail
at run-time with a dynamic type error—generally a trapped error in ordinary
JavaScript execution environments. This decision stems from the widespread us-
age of TypeScript to ascribe types to existing JavaScript libraries and codebases,
not just code written from scratch in TypeScript. It is crucial to the usability of
the language that it allows for common patterns in popular APIs, even if that
means embracing unsoundness in specific places.
The TypeScript language is defined in a careful, clear, but informal docu-
ment [11]. Naturally, this document contains certain ambiguities. For example,
the language permits subtyping recursive types; the literature contains several
rules for subtyping recursive types, not all sound, and the document does not
say exactly which is employed. Therefore, it may be difficult to know exactly
what is the type system, and in what ways it is sound or unsound.
Nevertheless, the world of unsoundness is not a shapeless, unintelligible mess,
and unsound languages are not all equally bad (nor all equally good). In classical
logic, any two inconsistent theories are equivalent. In programming, on the other
hand, unsoundness can arise from a great variety of sins (and virtues). At a
minimum, we may wish to distinguish blunders from thoughtful compromises—
many language designers and compiler writers are capable of both.
The goal of this paper is to describe the essence of TypeScript by giving a
precise definition of its type system on a core set of constructs of the language.
This definition clarifies ambiguities of the informal language documentation. It
has led to the discovery of a number of unintended inconsistencies and mistakes
both in the language specification and in the compiler, which we have reported to
the TypeScript team; fortunately, these have been relatively minor and easy to
correct. It also helps distinguish sound and unsound aspects of the type system:
it provides a basis for partial soundness theorems, and it isolates and explains
the sources of unsoundness.
Specifically, in this paper, we identify various core calculi, define precisely their
typing rules and, where possible, prove properties of these rules, or discuss why we
cannot. The calculi correspond precisely to TypeScript in that every valid program
in a given calculus is literally an executable TypeScript program. Since our work
Understanding TypeScript 259
took place before the release of TypeScript 1.0, we based it on earlier versions, in
particular TypeScript 0.9.5, which is almost identical to TypeScript 1.0 in most
respects; the main differences concern generics. As the design of generics evolved
until quite recently, in this paper we restrict attention to the non-generic fragment.
Fortunately, for the most part, generics are an orthogonal extension.
The rest of the paper is organized as follows: In §2 we give an informal overview
of the design goals of TypeScript. In §3 we give the syntax for a core, feather-
weight calculus, FTS. In §4 we define safeFTS, a safe, featherweight fragment
of TypeScript, by giving details of a type system. In §5 we give an operational
semantics for FTS and show how safeFTS satisfies a type soundness property.
In §6 we extend the type system of safeFTS obtaining a calculus we refer to as
‘production’ FTS, or prodFTS for short. This calculus should be thought of as the
featherweight fragment of the full TypeScript language, so it is not statically type
sound, by design. We characterize the unsound extensions to help understand
why the language designers added them. In §7 we give an alternative formula-
tion of the assignment compatibility relation for prodFTS that is analogous to
the consistent-subtyping relation of Siek and Taha [14]. We are able to prove
that this relation is equal to our original assignment compatibility relation. We
briefly review related work in §8 and conclude in §9.
Structural types: The TypeScript type system is structural rather than nom-
inal. Whilst structural type systems are common in formal descriptions of
object-oriented languages [1], most industrial mainstream languages, such as
Java and C , are nominal. However, structural typing may be the only rea-
sonable fit for JavaScript programming, where objects are often built from
scratch (not from classes), and used purely based on their expected shape.
Unified object types: In JavaScript, objects, functions, constructors, and ar-
rays are not separate kinds of values: a given object can simultaneously play
several of these roles. Therefore, object types in TypeScript can not only de-
scribe members but also contain call, constructor, and indexing signatures,
describing the different ways the object can be used. In Featherweight Type-
Script, for simplicity, we include only call signatures; constructor and index
signatures are broadly similar.
Type inference: TypeScript relies on type inference in order to minimize the
number of type annotations that programmers need to provide explicitly.
JavaScript is a pretty terse language, and the logic shouldn’t be obscured
by excessive new syntax. In practice, often only a small number of type
annotations need to be given to allow the compiler to infer meaningful type
signatures.
Gradual typing: TypeScript is an example of a gradual type system [14],
where parts of a program are statically typed, and others dynamically typed
through the use of a distinguished dynamic type, written any. Gradual typ-
ing is typically implemented using run-time casts, but that is not practical in
TypeScript, because of type erasure. As a result, typing errors not identified
statically may remain undetected at run-time.
The last point is particularly interesting: it follows from the view that an
unsound type system can still be extremely useful. The significant initial uptake
of TypeScript certainly suggests that this is the case. While the type system
can be wrong about the shape of run-time structures, the experience thus far
indicates that it usually won’t be. The type system may not be good enough
for applications that require precise guarantees (e.g., as a basis for performance
optimizations, or for security), but it is more than adequate for finding and pre-
venting many bugs, and, as importantly, for powering a comprehensive and reli-
able tooling experience of auto-completion, hover tips, navigation, exploration,
and refactoring.
In addition to gradual typing, a few other design decisions deliberately lead
to type holes and contribute to the unsoundness of the TypeScript type system.
Downcasting: The ability to explicitly downcast expressions is common in
most typed object-oriented languages. However, in these languages, a down-
cast is compiled to a dynamic check. In TypeScript, this is not the case, as no
trace of the type system is left in the emitted code. So incorrect downcasts
are not detected, and may lead to (trapped) run-time errors.
Covariance: TypeScript allows unsafe covariance of property types (despite
their mutability) and parameter types (in addition to the contravariance that
is the safe choice). Given the ridicule that other languages have endured for
Understanding TypeScript 261
this decision, it may seem like an odd choice, but there are significant and
sensible JavaScript patterns that just cannot be typed without covariance.
Indexing: A peculiar fact of JavaScript is that member access through dot
notation is just syntactic sugar for indexing with the member name as a
string. Full TypeScript permits specifying indexing signatures, but (in their
absence) allows indexing with any string. If the string is a literal that corre-
sponds to a property known to the type system, then the result will have the
type of that member (as usual with the dot notation). On the other hand, if
the string is not a literal, or does not correspond to a known member, then
the access is still allowed, and typed as any. Again, this aspect of TypeScript
corresponds to common JavaScript usage, and results in another hole in the
type system.
One further source of unsoundness may be the treatment of recursive definitions
of generic type operators. Deciding type equivalence and subtyping in a struc-
tural type system with such definitions is notoriously difficult. Some versions
of these problems are equivalent to the equivalence problem for deterministic
pushdown automata [16], which was proved decidable relatively recently [13],
and which remains a challenging research subject. We do not discuss these points
further because we focus on the non-generic fragment of TypeScript, as explained
above.
3 Featherweight TypeScript
In this section we define the syntax of a core calculus, Featherweight TypeScript
(FTS). As mentioned in the introduction, this core calculus covers the non-
generic part of TypeScript. To elucidate the design of TypeScript we will refactor
the type system into two parts, which we then add to FTS and consider the results
as two separate calculi: a ‘safe’ calculus containing none of the type holes, safeFTS
and a complete, ‘production’ calculus, prodFTS.
Analogously to Featherweight Java [10], our calculi are small and there is a
direct correspondence between our calculi and TypeScript: every safeFTS and
prodFTS program is literally an executable TypeScript program. (We also make
extensive use of the Featherweight Java ‘overbar’ notation.) However, our calculi
are considerably more expressive than Featherweight Java as we retain many
impure features that we view as essential to TypeScript programming, such as
assignments, variables, and statements.
In this section we define the syntax of our core calculus. The safeFTS type
system is defined in §4 and the prodFTS type system is defined in §6.
FTS expressions:
e, f ::= Expressions
x Identifier
l Literal
{ ā } Object literal
e=f Assignment operator
262 G. Bierman, M. Abadi, and M. Torgersen
FTS statements:
s, t ::= Statement
e; Expression statement
if (e) {s̄} else {t̄} If statement
return; Return statement
return e; Return value statement
v; Variable statement
u, v ::= Variable declaration
var x:T Uninitialized typed variable declaration
var x:T = e Initialized typed variable declaration
var x Uninitialized variable declaration
var x = e Initialized variable declaration
2
JavaScript somewhat confusingly supports two primitive values: null (an object) and
undefined which, for example, is returned when accessing a non-existent property.
Understanding TypeScript 263
For the sake of compactness, we support conditional statements but not condi-
tional expressions. Variable declarations are extended from JavaScript to include
optional type annotations.
FTS types:
R, S, T ::= Type
any Any type
P Primitive type
O Object type
P ::= Primitive type
number Number
string String
boolean Boolean type
void Void type
Null Null type
Undefined Undefined type
O ::= Object type
I Interface type
L Literal type
L ::= { M̄ } Object type literal
M, N ::= Type member
n:T Property
(x̄: S̄): T Call signature
FTS types fall into three categories: primitive types, object types, and a dis-
tinguished type, written any.
The primitive types include the run-time primitive types of JavaScript: number
for 64 bit IEEE 754 floating point numbers, string for Unicode UTF-16 strings,
and boolean to denote the boolean values. The void type denotes an absence of
a value, which arises from running a function that simply returns without giving
a result. There are no values of this type. There are two further types, Null and
Undefined, that are expressible but not denotable; we write them in italics to
further emphasize their special status. In other words, these two types cannot
be referenced in valid TypeScript programs, but they arise within the typing
process.
FTS object types consist of interface types and literal types. For compactness,
we do not support classes in FTS. At the level of the type system, classes are
secondary, and do not add any significant new issues, but complicate the formal-
ization of the language and the operational semantics. For that reason we omit
them, but do keep interfaces. Similarly we drop array, function, and constructor
type literals. FTS supports object type literals, whose type members can include
properties and call signatures. The inclusion of call signature properties enable
us to encode function literal types; for example the type (x:S) => T can be en-
coded as the type {(x:S): T}. We refer to an object type literal that contains a
call signature as a callable type, and we assume a predicate callable that returns
264 G. Bierman, M. Abadi, and M. Torgersen
true if the type contains a call signature.3 It is important to note that the type
{ } (i.e., the empty object type literal) is a valid type. In addition, TypeScript
has a number of predefined interfaces that are always in scope in TypeScript
programs. For the purposes of this paper, these interfaces are Object, Function,
String, Number, and Boolean.
FTS declaration:
D ::= Interface declaration
interface I { M̄ }
interface I extends Ī { M̄ } (Ī non-empty)
In this section we define the safeFTS calculus which adds a type system to the
FTS calculus defined in the previous section. As suggested by its name, this type
system, although a subsystem of the full TypeScript type system, has familiar
safety properties. (These properties are treated in §5.)
Our first step is to define an important type relation in TypeScript: assignment
compatibility [11, §3.8.3]. This relation is written S T and captures the intuition
that a value of type S can be assigned to a value of type T. However, the presence
of interfaces immediately makes this relation a little tricky to define. For example,
consider the following interface declaration.
interface I {
a : number,
(x: string): I
}
As TypeScript has a structural type system, this actually defines a type I which
is described by the following equation.
I = { a : number, (x: string): I }
A value of type I is an object with a property a of type number, and a function
that maps strings to values of type I. Clearly this is equivalent to an object
with a property a of type number, and a function that maps strings to objects
with a property a of type number, and a function that maps strings to values of
type I, and so on, ad infinitum. The language specification notes this potential
infinite expansion [11, §3.8.1] but gives few details about how it is to be dealt
with. (Indeed, the discussion of types excludes any mention of interface names,
which are assumed to have been replaced by their definitions.)
Fortunately, this equi-recursive treatment of recursive types has a pleasant,
but slightly less well-known formalization that views types as (finite or infinite)
trees, uses greatest fixed points to define type relationships, and coinduction as
a proof technique. We give only the basic definitions but the excellent survey
article [6] offers further details.
We represent types as possibly infinite trees, with nodes labelled by a sym-
bol from the set O = {any, null, undefined, boolean, number, string, {}, →}. The
branches are labelled with a name taken from the set B = X ∪ N ∪ {ret, cs},
where X is the set of FTS identifiers, N is the set of FTS property names, and
ret∈ X and cs∈ N are distinguished names (used to signify a return type and
a call signature). We write B for the set of sequences of elements b ∈ B. The
266 G. Bierman, M. Abadi, and M. Torgersen
S S T
==== [A-Refl] ======= [A-AnyR] ============== [A-Undef]
SS S any Undefined T
T T = Undefined I(P) T
======================= [A-Null] ======== [A-Prim]
Null T PT
Understanding TypeScript 267
T̄ S̄ R1 = void R0 R1 T̄ S̄ R
============================== [A-CS] ==================== [A-CS-Void]
(x̄:S̄):R0 (ȳ:T̄):R1 (x̄:S̄):R (ȳ:T̄):void
Rule [A-Refl] states that any type can be assigned to itself, and rule [A-AnyR]
that any type can be assigned to any. In rule [A-Undef] the type Undefined
can be assigned to any type; and in rule [A-Null] the type Null can be assigned
to any type except Undefined. The effect of these rules is that, when viewing
assignment compatibility as an order, Undefined is the least type, Null is be-
low any user-defined type, and that all types are below any, which is the top
type. Rule [A-Prop] states that assignment compatibility is invariant on prop-
erty members, and rules [A-CS] and [A-CS-Void] capture the fact that assignment
compatibility is contra-/co-variant on call signatures.
Note that there is no explicit transitivity rule for assignment compatibility,
but for safeFTS it is derivable.
Lemma 1 (Transitivity derived rule)
1. If S T and T U then S U
2. If M0 M1 and M1 M2 then M0 M2
The proof of this lemma is analogous to that of Gapeyev et al.’s Theorem 4.7 [6].
The type system for TypeScript, and hence safeFTS, consists of two inter-
defined typing relations: one where type information is inferred and one where
some type context is taken into account when a type is inferred. In this respect,
TypeScript is reminiscent of local type inference systems [12], but the detail is
different. The first relation is written Γ e : T and is read “given type environ-
ment Γ , the expression e has type T.” The second relation, written Γ e ↓ S : T,
is read “given type environment Γ , the expression e in the context of type S
has type T.” This relation is called ‘contextual typing’ in the language specifica-
tion [11, §4.18].
Expression typing: Γ e : T
[I-Id] [I-Number]
Γ, x : T x : T Γ n : number
[I-String] [I-Bool]
Γ s : string Γ true, false : boolean
[I-Null] [I-Undefined]
Γ null : Null Γ undefined : Undefined
Γ ē : T̄ Γ e: S Γ f ↓ S: T TS
[I-ObLit] [I-Assign]
Γ { n̄: ē } : { n̄: T̄ } Γ e = f: T
Γ e : S0 Γ f : S1 S0 ⊕ S1 = T
[I-Op]
Γ e ⊕ f: T
268 G. Bierman, M. Abadi, and M. Torgersen
Γ e: S lookup(S, n) = T
[I-Prop]
Γ e.n : T
Γ e: S S Object Γ f : string
[I-CompProp]
Γ e[f] : any
Γ e : { (x̄: S̄): R } Γ f̄ ↓ S̄ : T̄ T̄ S̄
[I-Call]
Γ e(f̄) : R
Γ e: S ST
[I-Assert]
Γ <T>e : T
Γ1 , this : any, |p̄| getVars (s̄) ; Γ2 Γ2 s̄ ↓ T : R̄
[I-Func1]
Γ1 function (p̄): T { s̄ } : { (|p̄|): return (R̄) }
On the whole, these rules are routine. In rule [I-Assign], the expression e = f
has type T, if the subexpression e has some type S and the subexpression f in the
context of S has type T. We also check that type T is assignment compatible with
type S(for reasons that should become clearer once contextual typing is defined).
In rule [I-Op], when typing the use of a built-in binary operator ⊕, we overload
notation and use a binary (partial) function ⊕ to calculate the return type given
the types of the two arguments. Interestingly, the current language specification
states that certain combinations of types should be considered both an error and
yield the return type any. The exact details of these type functions [11, §4.15] are
omitted from this paper as they are somewhat orthogonal to our main concerns.
Rule [I-Prop] details typing for property access. It makes use of an auxiliary,
partial function lookup(S, n) that returns the type of property n, if it exists, of
a type S. This process is a little subtle as TypeScript allows primitive types to
have properties, and all object types inherit properties from the Object interface.
The auxiliary function is defined for safeFTS as follows:4
⎧
⎪
⎪lookup(Number, n) if S = number
⎪
⎪
⎪
⎪lookup(Boolean, if S = boolean
⎨ n)
lookup(S, n) = lookup(String, n) if S = string
⎪
⎪
⎪
⎪ if S = { M̄0 ,n:T,M̄1 }
⎪
⎪
T
⎩
lookup(Object, n) if S = { M̄ } and n ∈/ M̄
of types S̄ have types T̄, and that types T̄ are assignment compatible with types
S̄.
In rule [I-Assert] a type assertion <T>e has type T if subexpression e has type S
where type S is assignable to T. In safeFTS the only asserts permitted are those
that are known to be correct.
Rules [I-Func1] and [I-Func2] address typing a function expression. Both rules
assume that the type of this in a function expression is any. Both rules also
make use of an auxiliary function | · | to extract types from the parameters in a
call signature. If a parameter does not have a type annotation, then TypeScript
assumes that it is of type any. One consequence of this design is that TypeScript
does not try to infer types for the parameters of a function expression.
var fact = function (x) {
if (x == 0) { return 1; }
else { return x * fact(x - 1); }
}; // infers type { (x:any): number }
Both rules run into an “awful” feature (using the terminology of Crockford [5])
inherited from JavaScript: all variables declared in a function body are in scope
regardless of nesting levels, order, or even how many times they are declared.
The whole function body (except for functions nested inside it) is treated as a
flat declaration space. In other words, JavaScript does not have block scoping.
Thus the following (buggy) JavaScript code:
var scope = function (p) {
var y = 1;
var a = [y, x, z];
var x = 2;
if (test(p)) { var z = 3; }
else { var z = 4; var w = 5; }
return w + a[2];
};
is treated as if it had instead been written as follows.
var scope = function (p) {
var y = 1;
var x; var z; var w; // implicit
var a = [y, x, z];
var x = 2;
if (test(p)) { var z = 3; }
else { var z = 4; var w = 5; }
return w + a[2];
};
At the level of typing this means that when typing a function expression, we
need two phases: first, we find the types of all the local variables declared in
the function body; and second, we then type the function body using the type
environment extended with the types determined from the first phase.
There is a further complication as TypeScript also infers types of local vari-
ables with initializers. Furthermore, TypeScript, again following JavaScript, sup-
ports mutually recursive variable declarations. We assume a function getVars(s̄)
270 G. Bierman, M. Abadi, and M. Torgersen
that returns the variable declarations in the scope of the sequence of statements
s̄. This function needs to deal with the problem of a collection of untyped ini-
tialized variable declarations that depend on each other. In the case where such
a collection is cyclic, the language specification states that they should all be
treated as if they were explicitly typed as any. A non-cyclic collection of untyped
variable declarations are reordered in reverse dependency order.
Given this sequence of variable declarations, we define a judgement written
Γ1 v̄ ; Γ2 to extend a given type environment Γ1 with the type information
contained in the variable declarations v̄ yielding a new type environment Γ2 .
The chief concern is dealing with repeated variable declarations. Such repetition
is permitted in TypeScript provided that the multiple declarations associate the
same type with the variable [11, §5.1].
Environment extension: Γ1 v̄ ; Γ2
dupOK (Γ1 , x : T) Γ1 (x : T) v̄ ; Γ2
Γ •;Γ Γ1 var x: T; v̄ ; Γ2
dupOK (Γ1 , x : T) Γ1 (x : T) v̄ ; Γ2
Γ1 var x: T = e; v̄ ; Γ2
Γ1 e : T dupOK (Γ1 , x : T) Γ1 (x : T) v̄ ; Γ2
Γ1 var x = e; v̄ ; Γ2
Returning to the typing rules [I-Func1] and [I-Func2] we use an auxiliary func-
tion return(R̄) to calculate the overall return type given the types R̄ inferred from
the return statements in the body of the function. This function is defined as
follows.
void if R̄ = •
return (R̄) =
widen(S) if S = bct (R̄)
Γ ā ↓ L : M̄
[C-ObLit]
Γ { ā } ↓ L : { M̄ }
(x: S) ∈ M̄ Γ e ↓ S: T (x: S) ∈ M̄ Γ e: T
[C-PA1] [C-PA2]
Γ (x: e) ↓ { M̄ } : (x: T) Γ (x: e) ↓ { M̄ } : (x: T)
[C-Inf] Γ e: T
Γ e ↓ S: T
[I-EmpSeq] [I-ExpSt] Γ e : S Γ s̄ : R̄
Γ •: • Γ e; s̄ : R̄
[I-Return] Γ s̄ : R̄ [I-ReturnVal] Γ e: T Γ s̄ : R̄
Γ return; s̄ : void, R̄ Γ return e; s̄ : T, R̄
Γ (x) = S Γ s̄ : R̄
[I-UTVarDec]
Γ var x:S; s̄ : R̄
Γ (x) = S Γ e ↓ S: T TS Γ s̄ : R̄
[I-ITVarDec]
Γ var x:S = e; s̄ : R̄
Γ (x) = any Γ s̄ : R̄
[I-UVarDec]
Γ var x; s̄ : R̄
x ∈ dom(Γ ) Γ e: S Γ x:widen(S) s̄ : R̄
[I-IVarDec]
Γ var x = e; s̄ : R̄
Rule [I-EmpSeq] asserts that the empty sequence is well typed. The rest of
the rules are defined by the form of the first statement in the statement se-
quence; they are routine, so we just describe the typing of return statements.
Understanding TypeScript 273
In rule [I-Return] a return statement with no expression is well typed and has
return type void. In rule [I-ReturnVal] a return statement return e is well typed
and has the return type T if the expression e is of type T.
The second type relation for statement sequences is the analogue of contextual
typing for expressions. It is written Γ s̄ ↓ T : R̄ and is read “given type environ-
ment Γ , the sequence of statements s̄ in the context of type T has (return) types
R̄.” The intention is that this judgement captures both that the statements s̄ are
well typed and that the types R̄ are the types inferred in the context of type T
for any return statements in the sequence.
Statement sequence contextual typing: Γ s̄ ↓ T : R̄
Γ e: S Γ s̄ ↓ T : R̄
[C-EmpSeq] [C-ExpSt]
Γ • ↓ T: • Γ e; s̄ ↓ T : R̄
Γ s̄ ↓ T : R̄ Γ e ↓ T: S ST Γ s̄ ↓ T : R̄
[C-Ret] [C-RetVal]
Γ return; s̄ ↓ T : R̄ Γ return e; s̄ ↓ T : S, R̄
Γ (x) = S Γ s̄ ↓ T : R̄
[C-UTVarDec]
Γ var x:S; s̄ ↓ T : R̄
Γ (x) = S Γ e ↓ S : S1 S1 S Γ s̄ ↓ T : R̄
[C-ITVarDec]
Γ var x:S = e; s̄ ↓ T : R̄
Γ (x) = any Γ s̄ ↓ T : R̄
[C-UVarDec]
Γ var x; s̄ ↓ T : R̄
Most of these rules are routine; the two important rules involve return state-
ments. In rule [C-Ret] we capture the fact that JavaScript permits functions
that return values to also contain return statements with no expressions. In
rule [C-RetVal] a return statement return e is well typed and has return type S
in the context of type T if the expression e in the context of type T has type S
and that type S is assignable to type T.
5 Operational Semantics
def l if H(l, x) ↓
σ(H, l : L, x) =
σ(H, L, x) otherwise
σ(H, L, x) = l
[E-Id] [E-Lit]
H, L, x ⇓ H, (l, x) H, L, l ⇓ H, l
Understanding TypeScript 275
H1 = H0 ∗ [l → new ()]
H1 , L, e1 ⇓v H1 , v1 H2 = H1 [(l, n1 ) → v1 ]
··· Hm , L, em ⇓v Hm , vm H = Hm [(l, nm ) → vm ]
[E-ObLit]
H0 , L, { n1 :e1 , . . . ,nm :em } ⇓ H, l
H1 = H0 ∗ [l → λx̄.{ s̄ }, L ]
[E-Func]
H0 , L, function (x̄) { s̄ } ⇓ H1 , l
H0 , L, e ⇓ H1 , r1
[E-TypeAssert]
H0 , L, <T>e ⇓ H1 , r1
Most of these rules are routine; Gardner et al. [7] give extensive details. We
restrict our attention to just a few of the more important rules. In rule [E-ObLit]
we create at fresh location l a new object map (using an auxiliary function
new ) and update its elements in order. In rule [E-CompProp] we require that the
276 G. Bierman, M. Abadi, and M. Torgersen
The auxiliary function defs searches the statements s̄ for all the declared vari-
ables and makes them in scope in the current local scope object; this is the
operational counterpart to the “awful” feature of JavaScript scoping described
in §4. Rule [E-CallUndef] reflects the JavaScript semantics that functions that
do not specify a return value actually return the undefined value.
The evaluation relation for statement sequences is of the form H0 , L, s̄0 ⇓
H1 , s where s is a statement result, which is a statement of the form return;,
return v ;, or ;. The rules for evaluating statements are routine and omitted.
In order to prove type soundness, we need to extend the notion of typing to the
operational semantics (in the style of [1,3]). A heap type Σ is a partial function
from locations to types (which are either function types or object literal types).
The statement of subject reduction then relies on a number of new judgements.
First, we need a well-formedness relation for a heap H, written H |= . We also
need a judgement that a heap H and scope chain L are compatible, written
H, L |= , which essentially means that all the scope objects in the scope chain
exist in the heap. We use a judgement written Σ |= H that captures that a
heap H is compatible with a heap type Σ. We also make use of a function
context(Σ, L) that builds a typing judgement corresponding to the variables in
the scope chain L, using their types stored in Σ. Using these judgements, we can
then write Σ |= H, L, e : T to mean Σ |= H, H, L |= and context(Σ, L) e : T.
Similarly we can define judgements Σ |= H, L, e ↓ S : T, Σ |= H, L, s̄ : T̄
and Σ |= H, L, s̄ ↓ S : T̄. Finally, we can define two judgements on results of
evaluation, written Σ |= H, r : T and Σ |= H, r ↓ S : T (along with variants
for statement results). We write Σ ⊆ Σ to mean that Σ is an extension of Σ
in the usual sense.
Theorem 1 (Subject reduction)
1. If Σ |= H, L, e : T and H, L, e ⇓ H , r then ∃Σ , T such that Σ ⊆ Σ , Σ |=
H , r : T and T T.
2. If Σ |= H, L, e ↓ S : T and H, L, e ⇓ H , r then ∃Σ , T such that Σ ⊆ Σ ,
Σ |= H , r ↓ S : T and T T.
3. If Σ |= H, L, s̄ : T̄ and H, L, s̄ ⇓ H , s then ∃Σ , T such that Σ ⊆ Σ , Σ |=
H , s : T and T return (T̄).
4. If Σ |= H, L, s̄ ↓ S : T̄ and H, L, s̄ ⇓ H , s then ∃Σ , T such that Σ ⊆ Σ ,
Σ |= H , s ↓ S : T and T return (T̄).
Understanding TypeScript 277
In this section we define prodFTS which can be viewed as the core calculus of the
full TypeScript language. We define it as a series of extensions to the type system
of safeFTS. Each of these extensions is unsound. We organize them according to
the source of unsoundness, along the lines suggested in §2.
Unlike in languages such as Java and C , these downcasts are not automati-
cally checked at runtime, because all type information is erased by the compiler.
The following example illustrates this issue:
interface Shape { ... }
interface Circle extends Shape { ... }
interface Triangle extends Shape { ... }
function createShape(kind: string): Shape {
if (kind === "circle") return buildCircle();
if (kind === "triangle") return buildTriangle();
... }
var circle = <Circle> createShape("circle");
Here, the TypeScript type system will rely on the fact that, after the type asser-
tion, circle is of type Circle. The responsibility of guarding against erroneous
creation of, for example a Triangle, remains with the programmer. Should run-
time checks be needed, the TypeScript programmer would have to simulate them
using JavaScript’s introspection capabilities.
TypeScript has a gradual type system in the style of Siek and Taha [14]. However,
unlike most languages with gradual type systems, dynamic checks are not made
to ensure safety (again, because types are removed by the compiler).
The key to gradual type systems is that the any type is treated specially. This
type serves as the boundary between the statically typed world (code typed
without reference to any) and the dynamically typed world. The fundamental
feature of any is that any type can be implicitly converted to any and any can be
implicitly converted to any other type. The former of these conversions is allowed
in safeFTS via the rule [A-AnyR]. prodFTS includes the following additional rule
in order to support conversions in the opposite direction:
T
=======
any T
278 G. Bierman, M. Abadi, and M. Torgersen
Siek and Taha employ occurrences of these rules in order to inject runtime
checks into code, with the goal of ensuring that the code satisfies type contracts.
Once more, as TypeScript removes all type information, analogous checks are
not made in TypeScript, so runtime type errors are possible.
ST S̄ ∼
= T̄ R1 = void R0 R1 S̄ ∼
= T̄ R
========= ============================== ====================
n:S n:T (x̄:S̄):R0 (ȳ:T̄):R1 (x̄:S̄):R (ȳ:T̄):void
The first rule permits covariance on member typing. The others permit call
signatures to be bivariant (either covariant or contravariant) in their argument
types and covariant in their result types (where S ∼
def
= T = S T or T S).
8 Related Work
Since JavaScript’s recent rise to prominence, there has been considerable work
on providing a suitable type system for the language. Here we can only mention
a subset of that work. Various research efforts have explored sound approaches
to this problem. Thiemann [17] proposed an early type system that uses single-
ton types and first-class record labels, and in the same year Anderson et al. [2]
proposed another type system with a focus on type inference. A number of oth-
ers have proposed systems of increasing complexity to deal with the complicated
programming patterns found in JavaScript; for example, Chugh et al. [4] em-
ployed nested refinements and heap types in DJS, and Guha et al. [9] proposed
a combination of a type system and a flow analysis.
Others have emphasized support for development at scale. In particular, like
TypeScript, the Dart language [8] relaxes soundness in order to support dynamic
programming. Dart is closely related to JavaScript, and can also compile directly
to JavaScript in such a way that all traces of the type system are removed.
However, unlike TypeScript, Dart is an entirely new language.
Whilst TypeScript favours convenience over soundness, our work can be used
as the basis for defining safe variants of TypeScript. Bhargavan et al. [15] ex-
tend a similar safe fragment with a new type to denote values from untrusted
JavaScript code and employ runtime type information instead of type erasure,
focusing on using type-driven wrappers to ensure important security properties.
Further afield, various dynamic languages have been extended with type sys-
tems. For instance, Typed Scheme [18] adds a type system to Scheme. It in-
troduces a notion of occurrence typing and combines a number of type system
features such as recursive types, union types, and polymorphism.
9 Conclusion
This paper describes and analyses the core of the TypeScript language, and in
particular its type system. The work that it represents has been useful in resolv-
ing ambiguities in the language definition, and in identifying minor unintended
inconsistencies and mistakes in the language implementation. It provides a ba-
sis for partial soundness theorems, and it isolates and accounts for sources of
unsoundness in the type system.
Beyond the details of this work (which are specific to TypeScript, and which
may perhaps change, as TypeScript develops further), we hope that our results
will contribute to the principled study of deliberate unsoundness. In this di-
rection, we believe that there are various opportunities for intriguing further
research. In particular, to the extent that any type system expresses program-
mer intent, we would expect that it could be useful in debugging, despite its
unsoundness. Research on blame, e.g., [19], might be helpful in this respect. It
may also be worthwhile to codify programmer guidance that would, over time,
reduce the reliance on dangerous typing rules. Static analysis tools may support
this guidance and complement a type system. These and related projects would
Understanding TypeScript 281
References
1. Abadi, M., Cardelli, L.: A theory of objects. Springer (1996)
2. Anderson, C., Giannini, P., Drossopoulou, S.: Towards type inference for javaScript.
In: Gao, X.-X. (ed.) ECOOP 2005. LNCS, vol. 3586, pp. 428–452. Springer, Hei-
delberg (2005)
3. Bierman, G., Parkinson, M., Pitts, A.: MJ: An imperative core calculus for Java
and Java with effects. Technical Report 563, University of Cambridge Computer
Laboratory (2003)
4. Chugh, R., Herman, D., Jhala, R.: Dependent types for JavaScript. In: Proceedings
of OOSLA (2012)
5. Crockford, D.: JavaScript: The good parts. O’Reilly (2008)
6. Gapeyev, V., Levin, M., Pierce, B.: Recursive subtyping revealed. JFP 12(6), 511–
548 (2002)
7. Gardner, P., Maffeis, S., Smith, G.: Towards a program logic for JavaScript. In:
Proceedings of POPL (2013)
8. Google. Dart programming language, http://www.dartlang.org
9. Guha, A., Saftoiu, C., Krishnamurthi, S.: Typing local control and state using flow
analysis. In: Barthe, G. (ed.) ESOP 2011. LNCS, vol. 6602, pp. 256–275. Springer,
Heidelberg (2011)
10. Igarashi, A., Pierce, B., Wadler, P.: Featherweight Java: A minimal core calculus
for Java and GJ. ACM TOPLAS 23(3), 396–450 (2001)
11. Microsoft Corporation. TypeScript Language Specification, 0.9.5 edn. (2014),
http://typescriptlang.org
12. Pierce, B., Turner, D.: Local type inference. In: Proceedings of POPL (1998)
13. Sénizergues, G.: The equivalence problem for deterministic pushdown automata
is decidable. In: Degano, P., Gorrieri, R., Marchetti-Spaccamela, A. (eds.) ICALP
1997. LNCS, vol. 1256, pp. 671–681. Springer, Heidelberg (1997)
14. Siek, J.G., Taha, W.: Gradual typing for objects. In: Ernst, E. (ed.) ECOOP 2007.
LNCS, vol. 4609, pp. 2–27. Springer, Heidelberg (2007)
15. Swamy, N., Fournet, C., Rastogi, A., Bhargavan, K., Chen, J., Strub, P.-Y., Bier-
man, G.: Gradual typing embedded securely in JavaScript. In: Proceedings of
POPL (2014)
16. Solomon, M.H.: Type definitions with parameters. In: Proceedings of POPL (1978)
17. Thiemann, P.: Towards a type system for analyzing javaScript programs. In: Sagiv,
M. (ed.) ESOP 2005. LNCS, vol. 3444, pp. 408–422. Springer, Heidelberg (2005)
18. Tobin-Hochstadt, S., Felleisen, M.: The design and implementation of Typed
Scheme. In: Proceedings of POPL (2008)
19. Wadler, P., Findler, R.B.: Well-typed programs can’t be blamed. In: Castagna, G.
(ed.) ESOP 2009. LNCS, vol. 5502, pp. 1–16. Springer, Heidelberg (2009)