angular-docs-cn/aio/content/guide/ts-to-js.md

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:

app.MyDirective = ng.core.Directive({ selector: '[myDirective]' }).Class({ ... });

and a pipe with ng.core.Pipe:

app.MyPipe = ng.core.Pipe({ name: 'myPipe' }).Class({ ... });

{@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 constructor function. Each item constrains 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 to null 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 or ViewChildren.

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.