27 KiB
TypeScript to JavaScript
Introduction
Anything you can do with Angular in TypeScript, you can also do in JavaScript. Translating from one language to the other is mostly a matter of changing the way you organize your code and access Angular APIs.
TypeScript is a popular language option for Angular development. Most code examples on the Internet as well as on this site are written in TypeScript. This cookbook contains recipes for translating TypeScript code examples to ES6 and to ES5 so that JavaScript developers can read and write Angular apps in their preferred dialect.
Run and compare the live TypeScript and JavaScript code shown in this cookbook.
TypeScript to ES6 to ES5
TypeScript is a typed superset of ES6 JavaScript. ES6 JavaScript is a superset of ES5 JavaScript. ES5 is the kind of JavaScript that runs natively in all modern browsers. The transformation of TypeScript code all the way down to ES5 code can be seen as "shedding" features.
The downgrade progression is as follows:
- TypeScript to ES6-with-decorators.
- ES6-with-decorators to ES6-without-decorators ("plain ES6").
- ES6-without-decorators to ES5.
When translating from TypeScript to ES6-with-decorators, remove
class property access modifiers
such as public
and private
.
Remove most of the
type declarations,
such as :string
and :boolean
but keep the constructor parameter types, which are used for dependency injection.
From ES6-with-decorators to plain ES6, remove all
decorators
and the remaining types.
You must declare properties in the class constructor (this.title = '...'
) rather than in the body of the class.
Finally, from plain ES6 to ES5, the main missing features are import
statements and class
declarations.
For plain ES6 transpilation you can start with a setup similar to the
TypeScript quickstart and adjust the application code accordingly.
Transpile with Babel using the es2015
preset.
To use decorators and annotations with Babel, install the
angular2
preset as well.
{@a modularity}
Importing and Exporting
Importing Angular Code
In both TypeScript and ES6, you import Angular classes, functions, and other members with ES6 import
statements.
In ES5, you access the Angular entities of the the Angular packages
through the global ng
object.
Anything you can import from @angular
is a nested member of this ng
object:
Exporting application code
Each file in a TypeScript or ES6 Angular application constitutes an ES6 module.
When you want to make something available to other modules, you export
it.
ES5 lacks native support for modules.
In an Angular ES5 application, you load each file manually by adding a <script>
tag to index.html
.
The order of <script>
tags is often significant.
You must load a file that defines a public JavaScript entity before a file that references that entity.
The best practice in ES5 is to create a form of modularity that avoids polluting the global scope.
Add one application namespace object such as app
to the global document
.
Then each code file "exports" public entities by attaching them to that namespace object, for example, app.HeroComponent
.
You could factor a large application into several sub-namespaces
which leads to "exports" along the lines of app.heroQueries.HeroComponent
.
Every ES5 file should wrap code in an Immediately Invoked Function Expression (IIFE) to limit unintentional leaking of private symbols into the global scope.
Here is a HeroComponent
as it might be defined and "exported" in each of the four language variants.
Importing application Code
In TypeScript and ES6 apps, you import
things that have been exported from other modules.
In ES5 you use the shared namespace object to access "exported" entities from other files.
Alternatively, you can use a module loader such as Webpack or
Browserify in an Angular JavaScript project. In such a project, you would
use CommonJS modules and the require
function to load Angular framework code.
Then use module.exports
and require
to export and import application code.
{@a class-metadata}
Classes and Class Metadata
Classes
Most Angular TypeScript and ES6 code is written as classes.
Properties and method parameters of TypeScript classes may be marked with the access modifiers
private
, internal
, and public
.
Remove these modifiers when translating to JavaScript.
Most type declarations, for example, :string
and :boolean
, should be removed when translating to JavaScript.
When translating to ES6-with-decorators, do not remove types from constructor parameters!
Look for types in TypeScript property declarations. In general it is better to initialize such properties with default values because many browser JavaScript engines can generate more performant code. When TypeScript code follows this same advice, it can infer the property types and there is nothing to remove during translation.
In ES6-without-decorators, properties of classes must be assigned inside the constructor.
ES5 JavaScript has no classes. Use the constructor function pattern instead, adding methods to the prototype.
Metadata
When writing in TypeScript or ES6-with-decorators,
provide configuration and metadata by adorning a class with one or more decorators.
For example, you supply metadata to a component class by preceding its definition with a
@Component
decorator function whose
argument is an object literal with metadata properties.
In plain ES6, you provide metadata by attaching an annotations
array to the class.
Each item in the array is a new instance of a metadata decorator created with a similar metadata object literal.
In ES5, you also provide an annotations
array but you attach it to the constructor function rather than to a class.
See these variations side-by-side:
External template file
A large component template is often kept in a separate template file.
The component, HeroTitleComponent
in this case, then references the template file in its metadata templateUrl
property:
Note that both the TypeScript and ES6 templateUrl
properties identify the location of the template file relative to the component module.
{@a dsl}
ES5 DSL
This ES5 pattern of creating a constructor and annotating it with metadata is so common that Angular provides a convenience API to make it a little more compact and locates the metadata above the constructor, as you would if you wrote in TypeScript or ES6-with-decorators.
This API (Application Programming Interface) is commonly known as the ES5 DSL (Domain Specific Language).
Set an application namespace property, for example, app.HeroDslComponent
, to the result of an ng.core.Component
function call.
Pass the same metadata object to ng.core.Component
as you did before.
Then chain a call to the Class()
method which takes an object defining the class constructor and instance methods.
Here is an example of the HeroComponent
, re-written with the DSL,
next to the original ES5 version for comparison:
{@a name-constructor}
Name the constructor
A named constructor displays clearly in the console log
if the component throws a runtime error.
An unnamed constructor displays as an anonymous function, for example, class0
,
which is impossible to find in the source code.
{@a getters-setters}
Properties with getters and setters
TypeScript and ES6 support with getters and setters. Here's an example of a read-only TypeScript property with a getter that prepares a toggle-button label for the next clicked state:
This TypeScript "getter" property is transpiled to an ES5 defined property. The ES5 DSL does not support defined properties directly but you can still create them by extracting the "class" prototype and adding the defined property in raw JavaScript like this:
{@a dsl-other}
DSL for other classes
There are similar DSLs for other decorated classes.
You can define a directive with ng.core.Directive
:
and a pipe with ng.core.Pipe
:
{@a interfaces}
Interfaces
A TypeScript interface helps ensure that a class implements the interface's members correctly.
Always try to use Angular interfaces where appropriate.
For example, the component class that implements the ngOnInit
lifecycle hook method
should implement the OnInit
interface.
TypeScript interfaces exist for developer convenience and are not used by Angular at runtime. They have no physical manifestation in the generated JavaScript code. Just implement the methods and ignore interfaces when translating code samples from TypeScript to JavaScript.
{@a io-decorators}
Input and Output Metadata
Input and Output Decorators
In TypeScript and ES6-with-decorators, you often add metadata to class properties with property decorators.
For example, you apply @Input
and @Output
property decorators
to public class properties that will be the target of data binding expressions in parent components.
There is no equivalent of a property decorator in ES5 or plain ES6.
Fortunately, every property decorator has an equivalent representation in a class decorator metadata property.
A TypeScript @Input
property decorator can be represented by an item in the Component
metadata's inputs
array.
You already know how to add Component
or Directive
class metadata in any JavaScript dialect so
there's nothing fundamentally new about adding another property.
But note that what would have been separate @Input
and @Output
property decorators for each class property are
combined in the metadata inputs
and outputs
arrays.
In the previous example, one of the public-facing binding names, cancelMsg
,
differs from the corresponding class property name, notOkMsg
.
That's OK but you must tell Angular about it so that it can map an external binding of cancelMsg
to the component's notOkMsg
property.
In TypeScript and ES6-with-decorators, you specify the special binding name in the argument to the property decorator.
In ES5 and plain ES6 code, convey this pairing with the propertyName: bindingName
syntax in the class metadata.
{@a dependency-injection}
Dependency injection
Angular relies heavily on Dependency Injection to provide services to the objects it creates. When Angular creates a new component, directive, pipe or another service, it sets the class constructor parameters to instances of services provided by an Injector.
The developer must tell Angular what to inject into each parameter.
{@a injection-class-type}
Injection by class type
The easiest and most popular technique in TypeScript and ES6-with-decorators is to set the constructor parameter type to the class associated with the service to inject.
The TypeScript transpiler writes parameter type information into the generated JavaScript. Angular reads that information at runtime and locates the corresponding service in the appropriate Injector. The ES6-with-decorators transpiler does essentially the same thing using the same parameter-typing syntax.
ES5 and plain ES6 lack types so you must identify "injectables" by attaching a parameters
array to the constructor function.
Each item in the array specifies the service's injection token.
As with TypeScript, the most popular token is a class,
or rather a constructor function that represents a class in ES5 and plain ES6.
The format of the parameters
array varies:
- Plain ES6—nest each constructor function in a sub-array.
- ES5—simply list the constructor functions.
When writing with ES5 DSL, set the Class.constructor
property to
an array whose first parameters are the injectable constructor functions and whose
last parameter is the class constructor itself.
This format should be familiar to AngularJS developers.
Injection with the @Inject decorator
Sometimes the dependency injection token isn't a class or constructor function.
In TypeScript and ES6-with-decorators, you precede the class constructor parameter
by calling the @Inject()
decorator with the injection token.
In the following example, the token is the string 'heroName'
.
The other JavaScript dialects add a parameters
array to the class contructor function.
Each item constains a new instance of Inject
:
- Plain ES6—each item is a new instance of
Inject(token)
in a sub-array. - ES5—simply list the string tokens.
When writing with ES5 DSL, set the Class.constructor
property to a function definition
array as before. Create a new instance of ng.core.Inject(token)
for each parameter.
Additional Injection Decorators
You can qualify injection behavior with injection decorators from @angular/core
.
In TypeScript and ES6-with-decorators, you precede the constructor parameters with injection qualifiers such as:
@Optional
sets the parameter tonull
if the service is missing.@Attribute
to inject a host element attribute value.@ContentChild
to inject a content child.@ViewChild
to inject a view child.@Host
to inject a service in this component or its host.@SkipSelf
to inject a service provided in an ancestor of this component.
In plain ES6 and ES5, create an instance of the equivalent injection qualifier in a nested array within the parameters
array.
For example, you'd write new Optional()
in plain ES6 and new ng.core.Optional()
in ES5.
When writing with ES5 DSL, set the Class.constructor
property to a function definition
array as before. Use a nested array to define a parameter's complete injection specification.
In the example above, there is no provider for the 'titlePrefix'
token.
Without @Optional()
, Angular would raise an error.
With @Optional()
, Angular sets the constructor parameter to null
and the component displays the title without a prefix.
{@a host-binding}
Host Binding
Angular supports bindings to properties and events of the host element, which is the element whose tag matches the component selector.
Host Decorators
In TypeScript and ES6-with-decorators, you can use host property decorators to bind a host
element to a component or directive.
The @HostBinding
decorator
binds host element properties to component data properties.
The @HostListener
decorator binds
host element events to component event handlers.
In plain ES6 or ES5, add a host
attribute to the component metadata to achieve the
same effect as @HostBinding
and @HostListener
.
The host
value is an object whose properties are host property and listener bindings:
- Each key follows regular Angular binding syntax:
[property]
for host bindings or(event)
for host listeners. - Each value identifies the corresponding component property or method.
Host Metadata
Some developers prefer to specify host properties and listeners in the component metadata. They'd rather do it the way you must do it ES5 and plain ES6.
The following re-implementation of the HeroComponent
shows that any property metadata decorator
can be expressed as component or directive metadata in both TypeScript and ES6-with-decorators.
These particular TypeScript and ES6 code snippets happen to be identical.
{@a view-child-decorators}
View and Child Decorators
Several property decorators query a component's nested view and content components.
View children are associated with element tags that appear within the component's template.
Content children are associated with elements that appear between the component's element tags;
they are projected into an <ng-content>
slot in the component's template.
The @ViewChild
and
@ViewChildren
property decorators
allow a component to query instances of other components that are used in
its view.
In ES5 and ES6, you access a component's view children by adding a queries
property to the component metadata.
The queries
property value is a hash map.
- Each key is the name of a component property that will hold the view child or children.
- Each value is a new instance of either
ViewChild
orViewChildren
.
The @ContentChild
and
@ContentChildren
property decorators
allow a component to query instances of other components that have been projected
into its view from elsewhere.
They can be added in the same way as @ViewChild
and
@ViewChildren
.
In TypeScript and ES6-with-decorators you can also use the queries
metadata
instead of the @ViewChild
and @ContentChild
property decorators.
{@a aot}
AOT Compilation in TypeScript only
Angular offers two modes of template compilation, JIT (just-in-time) and AOT (ahead-of-time). Currently the AOT compiler only works with TypeScript applications because, in part, it generates TypeScript files as an intermediate result. AOT is not an option for pure JavaScript applications at this time.