From the point of view of traditional type theory, ECMAScript has either no types or a single type
dynamic, i.e. ECMAScript is either untyped or uni-typed. To make matters worse, the ECMAScript Specification also uses not one but two different kinds of types in the specification.
Part of the confusion is that the term "type" is used differently by different people. In traditional Programming Language Theory, "type" means "static type" and there is no other kind of type, period. But, there are also Computer Scientists who oppose this dogmatic view. For example, in his influential paper on Typeful Programming, Luca Cardelli admits that static typing is limited and thus dynamic typing is necessary (but argues that as much as possible should be statically typed and dynamically only if there is no other choice).
This gets even more confusing in languages like Java where both the static and the dynamic types are relevant, for example, overload resolution depends on the static type but virtual method dispatch depends on the dynamic type.
Over the years, there have been several attempts at creating a static type system for ECMASCript or an ECMAScript-like language:
- ECMAScript 4 was an abandoned revision of ECMAScript. ES4 included a whole smorgasbord of features – which is ultimately why it collapsed under its own weight – including static typing. ES4 tried to be everything for everyone, it included both delegation-based and inheritance-based code re-use, both prototype-based and class-based OOP (with interfaces thrown in for good measure), both standalone constructors and constructors with are members of classes, both dynamic and static types and everything in-between, where the static typing rules can be both nominative and structural and the types can be either manifest or inferred. It had both packages and namespaces (which, to add even more confusion, are not what you think they are). And if that weren't enough, it also contained the entirety of ECMAScript for XML (E4X).
- ActionScript was the programming language for Flash. It started out as a variant of ECMASCript, over time, it added classes and static types, then it became a superset of ECMASCript 4.
- JScript.NET was an ECMAScript-like language for the .NET platform based on JScript with static types.
- Closure is an ECMAScript compiler / transpiler / optimizer / minifier / type checker by Google.
- Flow is a static type checker and type annotation language for ECMAScript by Facebook.
- TypeScript is a superset of ECMAScript with static typing by Microsoft.
- plus dozens of academic research projects …
- Most recently, there is a proposal to add type annotations to ECMAScript proper. However, this is not a type system, its purpose is merely to make it easier to work with systems like Flow or TypeScript by essentially having ECMAScript treat type annotations as comments and ignore them.
Of these, the two most widely-used ones are Flow and TypeScript. I am most familiar with TypeScript, so I will focus on that.
If you want to understand type systems related to ECMAScript, it makes sense to study the type systems that have been designed by people much smarter than me. All of these type systems tend to be rather complex and feature-rich with many advanced and cutting-edge features, since they are designed to capture highly dynamic and reflective programming patterns of a language that was never designed to have a static type system.
TypeScript has no fewer than 7 different "special" types, 6 of which could be reasonably classified as either a bottom type or a top type. In fact, some of those types even act as both at the same time and sometimes, the behavior depends on a configuration parameter.
any: ⊤ and ⊥
object: ⊤ for objects
null: ⊥, kinda, if
strictNullchecks = false
undefined: ⊥, kinda, if
strictNullchecks = false
void: unit, sort-of but not really
never: uninhabited ⊥
any is a type that is designed to, effectively, "turn off" the type system. A value of type
any can be assigned to a reference of any arbitrary type and a value of any arbitrary type can be assigned to a reference of type
any. A value of type
any can be passed as an argument for any arbitrary parameter type. A value of type
any can be used with any arbitrary operator, and you can access any arbitrary property on a value of type
any. Depending on the setting of the configuration parameter
noImplicitAny, an operation whose type the type checker cannot figure out will either raise a type error or just be typed as
any is assignment-compatible with everything and everything is assignment-compatible with
any acts both like a top type and like a bottom type.
The main use case for
any is when you want to adopt typing for a large, previously untyped, code base or you are interacting with an untyped third-party code base that you have no control over. Usually, if you add typing to a previously untyped code base, you will be greeted with thousands, if not tens of thousands of type errors. You can quickly "fix" every type error by annotating the type as
any, and then refine those annotations over time, preventing you to get buried under a mountain of type errors without making progress on your application.
unknown is the tamer, type-safe, cousin of
unknown was introduced relatively late in the development of TypeScript. Its use is somewhat similar to
any in that operations whose type TypeScript can't figure out can be configured to have type
unknown. Also similar to
unknown acts as a top type, meaning, every type is assignment-compatible with
unknown. But the opposite is not true: unlike
any, a value of type
unknown can only be assigned to
any. Also, you cannot access any properties on
You always have to narrow the type first (using a conditional runtime type check, type assertion, etc.), after which TypeScript's flow-sensitive type system will no longer consider the type to be
object is the top type for all object types, but not primitive types.
undefined denote the absence of a value. If
strictNullChecks = false, they behave as bottom types which are inhabited by the values also called
undefined. The fact that there are two types in TypeScript is because there are two values in ECMAScript. The fact that there are two values in ECMAScript is due to hysterical raisins. According to the ECMAScript specification,
undefined is for un-initialized variables and
null is for intentionally absent values.
void has similar semantics to
void in C, Java, or C#. It is used the same way that unit is used in impure functional languages, i.e. to denote that an expression does not evaluate to a useful value (or, in other words, that the expression is evaluated purely for its side-effects, or in even other words, that the expression is not an expression at all but a statement). However, unit typically only has a single, useless, value (hence why it is called "unit"), whereas in TypeScript, there is no
void value. OTOH, a function typed as
void is actually allowed to return any value (so, one could say that all values are
void values) but on the other other hand, one is not allowed to do anything with a value of type
never is the "impossible" bottom type, i.e. it represents a value that will never exist. For example, a function that never returns would have the type