Handbook of Typescript
Handbook of Typescript
Introduction
For programs to be useful, we need to be able to work with
some of the simplest units of data: numbers, strings,
structures, boolean values, and the like. In TypeScript, we
support much the same types as you would expect in
JavaScript, with a convenient enumeration type thrown in to
help things along.
Boolean
The most basic datatype is the simple true/false value, which
JavaScript and TypeScript call a boolean value.
let isDone: boolean = false;
Number
As in JavaScript, all numbers in TypeScript are floating point
values. These floating point numbers get the type number. In
addition to hexadecimal and decimal literals, TypeScript also
supports binary and octal literals introduced in ECMAScript
2015.
let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;
String
Another fundamental part of creating programs in JavaScript
for webpages and servers alike is working with textual data.
As in other languages, we use the type string to refer to
these textual datatypes. Just like JavaScript, TypeScript also
uses double quotes (") or single quotes (') to surround string
data.
let color: string = "blue";
color = 'red';
You can also use template strings, which can span multiple
lines and have embedded expressions. These strings are
surrounded by the backtick/backquote (`) character, and
embedded expressions are of the form ${ expr }.
let fullName: string = `Bob Bobbington`;
let age: number = 37;
let sentence: string = `Hello, my name is ${ fullName }.
Array
TypeScript, like JavaScript, allows you to work with arrays
of values. Array types can be written in one of two ways. In
the first, you use the type of the elements followed by [] to
denote an array of that element type:
let list: number[] = [1, 2, 3];
Tuple
Tuple types allow you to express an array where the type of a
fixed number of elements is known, but need not be the
same. For example, you may want to represent a value as a
pair of a string and a number:
// Declare a tuple type
let x: [string, number];
// Initialize it
x = ["hello", 10]; // OK
// Initialize it incorrectly
x = [10, "hello"]; // Error
alert(colorName);
Any
We may need to describe the type of variables that we do not
know when we are writing an application. These values may
come from dynamic content, e.g. from the user or a 3rd party
library. In these cases, we want to opt-out of type-checking
and let the values pass through compile-time checks. To do
so, we label these with the any type:
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean
The any type is also handy if you know some part of the
type, but perhaps not all of it. For example, you may have an
array but the array has a mix of different types:
let list: any[] = [1, true, "free"];
list[1] = 100;
Void
void is a little like the opposite of any: the absence of
having any type at all. You may commonly see this as the
return type of functions that do not return a value:
function warnUser(): void {
alert("This is my warning message");
}
Never
The never type represents the type of values that never
occur. For instance, never is the return type for a function
expression or an arrow function expression that always
throws an exception or one that never returns; Variables also
acquire the type never when narrowed by any type guards
that can never be true.
The never type is a subtype of, and assignable to, every
type; however, no type is a subtype of, or assignable
to, never(except never itself). Even any isnt assignable
to never.
Some examples of functions returning never:
// Function returning never must have unreachable end point
function error(message: string): never {
throw new Error(message);
}
Type assertions
Sometimes youll end up in a situation where youll know
more about a value than TypeScript does. Usually this will
happen when you know the type of some entity could be
more specific than its current type.
Type assertions are a way to tell the compiler trust me, I
know what Im doing. A type assertion is like a type cast in
other languages, but performs no special checking or
restructuring of data. It has no runtime impact, and is used
purely by the compiler. TypeScript assumes that you, the
programmer, have performed any special checks that you
need.
Type assertions have two forms. One is the angle-bracket
syntax:
let someValue: any = "this is a string";
The two samples are equivalent. Using one over the other is
mostly a choice of preference; however, when using
TypeScript with JSX, only as-style assertions are allowed.
var declarations
Declaring a variable in JavaScript has always traditionally
been done with the var keyword.
var a = 10;
As you mightve figured out, we just declared a variable
named a with the value 10.
We can also declare a variable inside of a function:
function f() {
var message = "Hello, world!";
return message;
}
var g = f();
g(); // returns '11'
a = 2;
var b = g();
a = 3;
return b;
function g() {
return a;
}
}
return x;
}
return sum;
}
Maybe it was easy to spot out for some, but the inner for-
loop will accidentally overwrite the
variable i because irefers to the same function-scoped
variable. As experienced developers know by now, similar
sorts of bugs slip through code reviews and can be an endless
source of frustration.
Block-scoping
When a variable is declared using let, it uses what some
call lexical-scoping or block-scoping. Unlike variables
declared with var whose scopes leak out to their containing
function, block-scoped variables are not visible outside of
their nearest containing block or for-loop.
function f(input: boolean) {
let a = 100;
if (input) {
// Still okay to reference 'a'
let b = a + 1;
return b;
}
let a;
For more information on temporal dead zones, see relevant
content on the Mozilla Developer Network.
if (true) {
var x;
}
}
function g() {
let x = 100;
var x = 100; // error: can't have both declarations of 'x'
}
return x;
}
return sum;
}
if (true) {
let city = "Seattle";
getCity = function() {
return city;
}
}
return getCity();
}
const declarations
const declarations are another way of declaring variables.
const numLivesForCat = 9;
// Error
kitty = {
name: "Danielle",
numLives: numLivesForCat
};
// all "okay"
kitty.name = "Rory";
kitty.name = "Kitty";
kitty.name = "Cat";
kitty.numLives--;
Destructuring
Another ECMAScript 2015 feature that TypeScript has is
destructuring. For a complete reference, see the article on the
Mozilla Developer Network. In this section, well give a
short overview.
Array destructuring
The simplest form of destructuring is array destructuring
assignment:
let input = [1, 2];
let [first, second] = input;
console.log(first); // outputs 1
console.log(second); // outputs 2
Or other elements:
let [, second, , fourth] = [1, 2, 3, 4];
Object destructuring
You can also destructure objects:
let o = {
a: "foo",
b: 12,
c: "bar"
};
let { a, b } = o;
Property renaming
You can also give different names to properties:
let { a: newName1, b: newName2 } = o;
Confusingly, the colon here does not indicate the type. The
type, if you specify it, still needs to be written after the entire
destructuring:
let { a, b }: { a: string, b: number } = o;
Default values
Default values let you specify a default value in case a
property is undefined:
function keepWholeObject(wholeObject: { a: string, b?: number }) {
let { a, b = 1001 } = wholeObject;
}
Function declarations
Destructuring also works in function declarations. For simple
cases this is straightforward:
type C = { a: string, b?: number }
function f({ a, b }: C): void {
// ...
}
Spread
The spread operator is the opposite of destructuring. It allows
you to spread an array into another array, or an object into
another object. For example:
let first = [1, 2];
let second = [3, 4];
let bothPlus = [0, ...first, ...second, 5];
Optional Properties
Not all properties of an interface may be required. Some
exist under certain conditions or may not be there at all.
These optional properties are popular when creating patterns
like option bags where you pass an object to a function that
only has a couple of properties filled in.
Heres an example of this pattern:
interface SquareConfig {
color?: string;
width?: number;
}
Readonly properties
Some properties should only be modifiable when an object is
first created. You can specify this by
putting readonlybefore the name of the property:
interface Point {
readonly x: number;
readonly y: number;
}
readonly vs const
Keep in mind that for simple code like above, you probably
shouldnt be trying to get around these checks. For more
complex object literals that have methods and hold state, you
might need to keep these techniques in mind, but a majority
of excess property errors are actually bugs. That means if
youre running into excess property checking problems for
something like option bags, you might need to revise some
of your type declarations. In this instance, if its okay to pass
an object with both a color or colour property
to createSquare, you should fix up the definition
of SquareConfig to reflect that.
Function Types
Interfaces are capable of describing the wide range of shapes
that JavaScript objects can take. In addition to describing an
object with properties, interfaces are also capable of
describing function types.
To describe a function type with an interface, we give the
interface a call signature. This is like a function declaration
with only the parameter list and return type given. Each
parameter in the parameter list requires both name and type.
interface SearchFunc {
(source: string, subString: string): boolean;
}
Indexable Types
Similarly to how we can use interfaces to describe function
types, we can also describe types that we can index into
like a[10], or ageMap["daniel"]. Indexable types have
an index signature that describes the types we can use to
index into the object, along with the corresponding return
types when indexing. Lets take an example:
interface StringArray {
[index: number]: string;
}
Class Types
Implementing an interface
One of the most common uses of interfaces in languages like
C# and Java, that of explicitly enforcing that a class meets a
particular contract, is also possible in TypeScript.
interface ClockInterface {
currentTime: Date;
}
Extending Interfaces
Like classes, interfaces can extend each other. This allows
you to copy the members of one interface into another, which
gives you more flexibility in how you separate your
interfaces into reusable components.
interface Shape {
color: string;
}
interface PenStroke {
penWidth: number;
}
Hybrid Types
As we mentioned earlier, interfaces can describe the rich
types present in real world JavaScript. Because of
JavaScripts dynamic and flexible nature, you may
occasionally encounter an object that works as a combination
of some of the types described above.
One such example is an object that acts as both a function
and an object, with additional properties:
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
class Image {
select() { }
}
class Location {
select() { }
}
Classes
Lets take a look at a simple class-based example:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
Inheritance
In TypeScript, we can use common object-oriented patterns.
Of course, one of the most fundamental patterns in class-
based programming is being able to extend existing classes
to create new ones using inheritance.
Lets take a look at an example:
class Animal {
name: string;
constructor(theName: string) { this.name = theName; }
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
sam.move();
tom.move(34);
Understanding private
class Employee {
private name: string;
constructor(theName: string) { this.name = theName; }
}
animal = rhino;
animal = employee; // Error: 'Animal' and 'Employee' are not compatible
Understanding protected
public getElevatorPitch() {
return `Hello, my name is ${this.name} and I work in $
{this.department}.`;
}
}
public getElevatorPitch() {
return `Hello, my name is ${this.name} and I work in $
{this.department}.`;
}
}
Readonly modifier
You can make properties readonly by using
the readonly keyword. Readonly properties must be
initialized at their declaration or in the constructor.
class Octopus {
readonly name: string;
readonly numberOfLegs: number = 8;
constructor (theName: string) {
this.name = theName;
}
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // error! name is readonly.
Parameter properties
In our last example, we had to declare a readonly
member name and a constructor parameter theName in
the Octopusclass, and we then immediately
set name to theName. This turns out to be a very common
practice. Parameter properties let you create and initialize a
member in one place. Heres a further revision of the
previous Octopus class using a parameter property:
class Octopus {
readonly numberOfLegs: number = 8;
constructor(readonly name: string) {
}
}
class Employee {
private _fullName: string;
Static Properties
Up to this point, weve only talked about
the instance members of the class, those that show up on the
object when its instantiated. We can also
create static members of a class, those that are visible on the
class itself rather than on the instances. In this example, we
use static on the origin, as its a general value for all grids.
Each instance accesses this value through prepending the
name of the class. Similarly to prepending this. in front of
instance accesses, here we prepend Grid. in front of static
accesses.
class Grid {
static origin = {x: 0, y: 0};
calculateDistanceFromOrigin(point: {x: number; y: number;}) {
let xDist = (point.x - Grid.origin.x);
let yDist = (point.y - Grid.origin.y);
return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
}
constructor (public scale: number) { }
}
Abstract Classes
Abstract classes are base classes from which other classes
may be derived. They may not be instantiated directly.
Unlike an interface, an abstract class may contain
implementation details for its members.
The abstract keyword is used to define abstract classes as
well as abstract methods within an abstract class.
abstract class Animal {
abstract makeSound(): void;
move(): void {
console.log("roaming the earth...");
}
}
Methods within an abstract class that are marked as abstract
do not contain an implementation and must be implemented
in derived classes. Abstract methods share a similar syntax to
interface methods. Both define the signature of a method
without including a method body. However, abstract methods
must include the abstract keyword and may optionally
include access modifiers.
abstract class Department {
printName(): void {
console.log("Department name: " + this.name);
}
constructor() {
super("Accounting and Auditing"); // constructors in derived classes
must call super()
}
printMeeting(): void {
console.log("The Accounting Department meets each Monday at 10am.");
}
generateReports(): void {
console.log("Generating accounting reports...");
}
}
Advanced Techniques
Constructor functions
When you declare a class in TypeScript, you are actually
creating multiple declarations at the same time. The first is
the type of the instance of the class.
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter;
greeter = new Greeter("world");
console.log(greeter.greet());
Functions
To begin, just as in JavaScript, TypeScript functions can be
created both as a named function or as an anonymous
function. This allows you to choose the most appropriate
approach for your application, whether youre building a list
of functions in an API or a one-off function to hand off to
another function.
To quickly recap what these two approaches look like in
JavaScript:
// Named function
function add(x, y) {
return x + y;
}
// Anonymous function
let myAdd = function(x, y) { return x+y; };
function addToZ(x, y) {
return x + y + z;
}
Function Types
Typing the function
Lets add types to our simple examples from earlier:
function add(x: number, y: number): number {
return x + y;
}
A functions type has the same two parts: the type of the
arguments and the return type. When writing out the whole
function type, both parts are required. We write out the
parameter types just like a parameter list, giving each
parameter a name and a type. This name is just to help with
readability. We could have instead written:
let myAdd: (baseValue:number, increment:number) => number =
function(x: number, y: number): number { return x + y; };
and
function buildName(firstName: string, lastName = "Smith") {
// ...
}
Rest Parameters
Required, optional, and default parameters all have one thing
in common: they talk about one parameter at a time.
Sometimes, you want to work with multiple parameters as a
group, or you may not know how many parameters a
function will ultimately take. In JavaScript, you can work
with the arguments directly using the arguments variable
that is visible inside every function body.
In TypeScript, you can gather these arguments together into a
variable:
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
this
Even better, TypeScript will warn you when you make this
mistake if you pass the --noImplicitThis flag to the
compiler. It will point out
that this in this.suits[pickedSuit] is of type any.
this parameters
Unfortunately, the type of this.suits[pickedSuit] is
still any. Thats because this comes from the function
expression inside the object literal. To fix this, you can
provide an explicit this parameter. this parameters are
fake parameters that come first in the parameter list of a
function:
function f(this: void) {
// make sure `this` is unusable in this standalone function
}
Overloads
JavaScript is inherently a very dynamic language. Its not
uncommon for a single JavaScript function to return different
types of objects based on the shape of the arguments passed
in.
let suits = ["hearts", "spades", "clubs", "diamonds"];
Generic Types
In previous sections, we created generic identity functions
that worked over a range of types. In this section, well
explore the type of the functions themselves and how to
create generic interfaces.
The type of generic functions is just like those of non-generic
functions, with the type parameters listed first, similarly to
function declarations:
function identity<T>(arg: T): T {
return arg;
}
Generic Classes
A generic class has a similar shape to a generic interface.
Generic classes have a generic type parameter list in angle
brackets (<>) following the name of the class.
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
alert(stringNumeric.add(stringNumeric.zeroValue, "test"));
Generic Constraints
If you remember from an earlier example, you may
sometimes want to write a generic function that works on a
set of types where you have some knowledge about what
capabilities that set of types will have. In
our loggingIdentityexample, we wanted to be able to
access the .length property of arg, but the compiler could
not prove that every type had a .length property, so it
warns us that we cant make this assumption.
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // Error: T doesn't have .length
return arg;
}
let x = { a: 1, b: 2, c: 3, d: 4 };
class ZooKeeper {
nametag: string;
}
class Animal {
numLegs: number;
}
createInstance(Lion).keeper.nametag; // typechecks!
createInstance(Bee).keeper.hasMask; // typechecks!
Enums
Enums
Enums allow us to define a set of named numeric constants.
An enum can be defined using the enum keyword.
enum Direction {
Up = 1,
Down,
Left,
Right
}
is compiled to:
var Enum;
(function (Enum) {
Enum[Enum["A"] = 0] = "A";
})(Enum || (Enum = {}));
var a = Enum.A;
var nameOfA = Enum[Enum.A]; // "A"
Ambient enums
Ambient enums are used to describe the shape of already
existing enum types.
declare enum Enum {
A = 1,
B,
C = 2
}
Basics
In TypeScript, there are several places where type inference
is used to provide type information when there is no explicit
type annotation. For example, in this code
let x = 3;
For the code above to give the type error, the TypeScript type
checker used the type of
the Window.onmousedownfunction to infer the type of the
function expression on the right hand side of the assignment.
When it did so, it was able to infer the type of
the mouseEvent parameter. If this function expression were
not in a contextually typed position,
the mouseEvent parameter would have type any, and no
error would have been issued.
If the contextually typed expression contains explicit type
information, the contextual type is ignored. Had we written
the above example:
window.onmousedown = function(mouseEvent: any) {
console.log(mouseEvent.buton); //<- Now, no error is given
};
class Person {
name: string;
}
let p: Named;
// OK, because of structural typing
p = new Person();
Starting out
The basic rule for TypeScripts structural type system is
that x is compatible with y if y has at least the same
members as x. For example:
interface Named {
name: string;
}
let x: Named;
// y's inferred type is { name: string; location: string; }
let y = { name: "Alice", location: "Seattle" };
x = y;
y = x; // OK
x = y; // Error
// Should be OK!
items.forEach(item => console.log(item));
Now lets look at how return types are treated, using two
functions that differ only by their return type:
let x = () => ({name: "Alice"});
let y = () => ({name: "Alice", location: "Seattle"});
x = y; // OK
y = x; // Error because x() lacks a location property
Enums
Enums are compatible with numbers, and numbers are
compatible with enums. Enum values from different enum
types are considered incompatible. For example,
enum Status { Ready, Waiting };
enum Color { Red, Blue, Green };
Classes
Classes work similarly to object literal types and interfaces
with one exception: they have both a static and an instance
type. When comparing two objects of a class type, only
members of the instance are compared. Static members and
constructors do not affect compatibility.
class Animal {
feet: number;
constructor(name: string, numFeet: number) { }
}
class Size {
feet: number;
constructor(numFeet: number) { }
}
let a: Animal;
let s: Size;
a = s; //OK
s = a; //OK
Generics
Because TypeScript is a structural type system, type
parameters only affect the resulting type when consumed as
part of the type of a member. For example,
interface Empty<T> {
}
let x: Empty<number>;
let y: Empty<string>;
Advanced Topics
Subtype vs Assignment
So far, weve used compatible, which is not a term defined
in the language spec. In TypeScript, there are two kinds of
compatibility: subtype and assignment. These differ only in
that assignment extends subtype compatibility with rules to
allow assignment to and from any and to and from enum
with corresponding numeric values.
Different places in the language use one of the two
compatibility mechanisms, depending on the situation. For
practical purposes, type compatibility is dictated by
assignment compatibility even in the cases of
the implements and extendsclauses. For more
information, see the TypeScript spec.
Advanced Types
Intersection Types
An intersection type combines multiple types into one. This
allows you to add together existing types to get a single type
that has all the features you need. For example, Person &
Serializable & Loggable is
a Person andSerializable and Loggable. That means
an object of this type will have all members of all three
types.
You will mostly see intersection types used for mixins and
other concepts that dont fit in the classic object-oriented
mold. (There are a lot of these in JavaScript!) Heres a
simple example that shows how to create a mixin:
function extend<T, U>(first: T, second: U): T & U {
let result = <T & U>{};
for (let id in first) {
(<any>result)[id] = (<any>first)[id];
}
for (let id in second) {
if (!result.hasOwnProperty(id)) {
(<any>result)[id] = (<any>second)[id];
}
}
return result;
}
class Person {
constructor(public name: string) { }
}
interface Loggable {
log(): void;
}
class ConsoleLogger implements Loggable {
log() {
// ...
}
}
var jim = extend(new Person("Jim"), new ConsoleLogger());
var n = jim.name;
jim.log();
Union Types
Union types are closely related to intersection types, but they
are used very differently. Occasionally, youll run into a
library that expects a parameter to be either a number or
a string. For instance, take the following function:
/**
* Takes a string and adds "padding" to the left.
* If 'padding' is a string, then 'padding' is appended to the left side.
* If 'padding' is a number, then that number of spaces is added to the left
side.
*/
function padLeft(value: string, padding: any) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
interface Fish {
swim();
layEggs();
}
Union types can be a bit tricky here, but it just takes a bit of
intuition to get used to. If a value has the type A | B, we
only know for certain that it has members that
both A and B have. In this example, Bird has a member
named fly. We cant be sure whether a variable typed
as Bird | Fish has a fly method. If the variable is really
a Fish at runtime, then calling pet.fly() will fail.
if ((<Fish>pet).swim) {
(<Fish>pet).swim();
}
else {
(<Bird>pet).fly();
}
if (isFish(pet)) {
pet.swim();
}
else {
pet.fly();
}
function getRandomPadder() {
return Math.random() < 0.5 ?
new SpaceRepeatingPadder(4) :
new StringPadder(" ");
}
Nullable types
TypeScript has two special types, null and undefined,
that have the values null and undefined respectively. We
mentioned these briefly in the Basic Types section. By
default, the type checker
considers null and undefinedassignable to anything.
Effectively, null and undefined are valid values of every
type. That means its not possible to stop them from being
assigned to any type, even when you would like to prevent it.
The inventor of null, Tony Hoare, calls this his billion
dollar mistake.
The --strictNullChecks flag fixes this: when you
declare a variable, it doesnt automatically
include null or undefined. You can include them
explicitly using a union type:
let s = "foo";
s = null; // error, 'null' is not assignable to 'string'
let sn: string | null = "bar";
sn = null; // ok
The null elimination is pretty obvious here, but you can use
terser operators too:
function f(sn: string | null): string {
return sn || "default";
}
Type Aliases
Type aliases create a new name for a type. Type aliases are
sometimes similar to interfaces, but can name primitives,
unions, tuples, and any other types that youd otherwise have
to write by hand.
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
if (typeof n === "string") {
return n;
}
else {
return n();
}
}
interface Person {
name: string;
}
You can pass any of the three allowed strings, but any other
string will give the error
Argument of type '"uneasy"' is not assignable to parameter of type '"ease-in"
| "ease-out" | "ease-in-out"'
Discriminated Unions
You can combine string literal types, union types, type
guards, and type aliases to build an advanced pattern
called discriminated unions, also known as tagged
unions or algebraic data types. Discriminated unions are
useful in functional programming. Some languages
automatically discriminate unions for you; TypeScript
instead builds on JavaScript patterns as they exist today.
There are three ingredients:
1. Types that have a common, string literal property
the discriminant.
2. A type alias that takes the union of those types
the union.
3. Type guards on the common property.
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
radius: number;
}
Exhaustiveness checking
We would like the compiler to tell us when we dont cover
all variants of the discriminated union. For example, if we
add Triangle to Shape, we need to update area as well:
type Shape = Square | Rectangle | Circle | Triangle;
function area(s: Shape) {
switch (s.kind) {
case "square": return s.size * s.size;
case "rectangle": return s.height * s.width;
case "circle": return Math.PI * s.radius ** 2;
}
// should error here - we didn't handle case "triangle"
}
Since the class uses this types, you can extend it and the
new class can use the old methods with no changes.
class ScientificCalculator extends BasicCalculator {
public constructor(value = 0) {
super(value);
}
public sin() {
this.value = Math.sin(this.value);
return this;
}
// ... other operations go here ...
}
Index types
With index types, you can get the compiler to check code
that uses dynamic property names. For example, a common
Javascript pattern is to pick a subset of properties from an
object:
function pluck(o, names) {
return names.map(n => o[n]);
}
interface Person {
name: string;
age: number;
}
let person: Person = {
name: 'Jarid',
age: 35
};
let strings: string[] = pluck(person, ['name']); // ok, string[]
Mapped types
A common task is to take an existing type and make each of
its properties optional:
interface PersonPartial {
name?: string;
age?: number;
}
Lets take a look at the simplest mapped type and its parts:
type Keys = 'option1' | 'option2';
type Flags = { [K in Keys]: boolean };
let obj = {
[sym]: "value"
};
console.log(obj[sym]); // "value"
class C {
[getClassNameSymbol](){
return "C";
}
}
Well-known Symbols
In addition to user-defined symbols, there are well-known
built-in symbols. Built-in symbols are used to represent
internal language behaviors.
Here is a list of well-known symbols:
Symbol.hasInstance
for..of statements
for..of loops over an iterable object, invoking
the Symbol.iterator property on the object. Here is a
simple for..of loop on an array:
let someArray = [1, "string", false];
Code generation
Targeting ES5 and ES3
Introduction
Starting with the ECMAScript 2015, JavaScript has a
concept of modules. TypeScript shares this concept.
Modules are executed within their own scope, not in the
global scope; this means that variables, functions, classes,
etc. declared in a module are not visible outside the module
unless they are explicitly exported using one of
the exportforms. Conversely, to consume a variable,
function, class, interface, etc. exported from a different
module, it has to be imported using one of
the import forms.
Modules are declarative; the relationships between modules
are specified in terms of imports and exports at the file level.
Modules import one another using a module loader. At
runtime the module loader is responsible for locating and
executing all dependencies of a module before executing it.
Well-known modules loaders used in JavaScript are
the CommonJS module loader for Node.js and require.js for
Web applications.
In TypeScript, just as in ECMAScript 2015, any file
containing a top-level import or export is considered a
module.
Export
Exporting a declaration
Any declaration (such as a variable, function, class, type
alias, or interface) can be exported by adding
the exportkeyword.
Validation.ts
export interface StringValidator {
isAcceptable(s: string): boolean;
}
ZipCodeValidator.ts
export const numberRegexp = /^[0-9]+$/;
Export statements
Export statements are handy when exports need to be
renamed for consumers, so the above example can be written
as:
class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
export { ZipCodeValidator };
export { ZipCodeValidator as mainValidator };
Re-exports
Often modules extend other modules, and partially expose
some of their features. A re-export does not import it locally,
or introduce a local variable.
ParseIntBasedZipCodeValidator.ts
export class ParseIntBasedZipCodeValidator {
isAcceptable(s: string) {
return s.length === 5 && parseInt(s).toString() === s;
}
}
Import
Importing is just about as easy as exporting from a module.
Importing an exported declaration is done through using one
of the import forms below:
Default exports
Each module can optionally export a default export.
Default exports are marked with the keyword default; and
there can only be one default export per
module. default exports are imported using a different
import form.
default exports are really handy. For instance, a library
like JQuery might have a default export of jQuery or $,
which wed probably also import under the
name $ or jQuery.
JQuery.d.ts
declare let $: JQuery;
export default $;
App.ts
import $ from "JQuery";
Test.ts
import validator from "./ZipCodeValidator";
Test.ts
import validate from "./StaticZipCodeValidator";
Log.ts
import num from "./OneTwoThree";
console.log(num); // "123"
Test.ts
import zip = require("./ZipCodeValidator");
// Validators to use
let validator = new zip();
UMD SimpleModule.js
(function (factory) {
if (typeof module === "object" && typeof module.exports === "object") {
var v = factory(require, exports); if (v !== undefined) module.exports
= v;
}
else if (typeof define === "function" && define.amd) {
define(["require", "exports", "./mod"], factory);
}
})(function (require, exports) {
var mod_1 = require("./mod");
exports.t = mod_1.something + 1;
});
System SimpleModule.js
System.register(["./mod"], function(exports_1) {
var mod_1;
var t;
return {
setters:[
function (mod_1_1) {
mod_1 = mod_1_1;
}],
execute: function() {
exports_1("t", t = mod_1.something + 1);
}
}
});
Simple Example
Below, weve consolidated the Validator implementations
used in previous examples to only export a single named
export from each module.
To compile, we must specify a module target on the
command line. For Node.js, use --module commonjs; for
require.js, use --module amd. For example:
tsc --module commonjs Test.ts
LettersOnlyValidator.ts
import { StringValidator } from "./Validation";
ZipCodeValidator.ts
import { StringValidator } from "./Validation";
Test.ts
import { StringValidator } from "./Validation";
import { ZipCodeValidator } from "./ZipCodeValidator";
import { LettersOnlyValidator } from "./LettersOnlyValidator";
// Validators to use
let validators: { [s: string]: StringValidator; } = {};
validators["ZIP code"] = new ZipCodeValidator();
validators["Letters only"] = new LettersOnlyValidator();
if (needZipValidation) {
let ZipCodeValidator: typeof Zip = require("./ZipCodeValidator");
let validator = new ZipCodeValidator();
if (validator.isAcceptable("...")) { /* ... */ }
}
if (needZipValidation) {
require(["./ZipCodeValidator"], (ZipCodeValidator: typeof Zip) => {
let validator = new ZipCodeValidator.ZipCodeValidator();
if (validator.isAcceptable("...")) { /* ... */ }
});
}
if (needZipValidation) {
System.import("./ZipCodeValidator").then((ZipCodeValidator: typeof Zip) =>
{
var x = new ZipCodeValidator();
if (x.isAcceptable("...")) { /* ... */ }
});
}
Working with Other JavaScript
Libraries
To describe the shape of libraries not written in TypeScript,
we need to declare the API that the library exposes.
We call declarations that dont define an implementation
ambient. Typically, these are defined in .d.ts files. If
youre familiar with C/C++, you can think of these
as .h files. Lets look at a few examples.
Ambient Modules
In Node.js, most tasks are accomplished by loading one or
more modules. We could define each module in its
own .d.ts file with top-level export declarations, but its
more convenient to write them as one larger .d.ts file. To
do so, we use a construct similar to ambient namespaces, but
we use the module keyword and the quoted name of the
module which will be available to a later import. For
example:
node.d.ts (simplified excerpt)
declare module "url" {
export interface Url {
protocol?: string;
hostname?: string;
pathname?: string;
}
All imports from a shorthand module will have the any type.
import x, {y} from "hot-new-module";
x(y);
UMD modules
Some libraries are designed to be used in many module
loaders, or with no module loading (global variables). These
are known as UMD or Isomorphic modules. These libraries
can be accessed through either an import or a global variable.
For example:
math-lib.d.ts
export const isPrime(x: number): boolean;
export as namespace mathLib;
MyFunc.ts
export default function getThing() { return "thing"; }
Consumer.ts
import t from "./MyClass";
import f from "./MyFunc";
let x = new t();
console.log(f());
Consumer.ts
import * as myLargeModule from "./MyLargeModule.ts";
let x = new myLargeModule.Dog();
Re-export to extend
Often you will need to extend functionality on a module. A
common JS pattern is to augment the original object
with extensions, similar to how JQuery extensions work. As
weve mentioned before, modules do not merge like global
namespace objects would. The recommended solution is
to not mutate the original object, but rather export a new
entity that provides the new functionality.
Consider a simple calculator implementation defined in
module Calculator.ts. The module also exports a helper
function to test the calculator functionality by passing a list
of input strings and writing the result at the end.
Calculator.ts
export class Calculator {
private current = 0;
private memory = 0;
private operator: string;
private evaluate() {
if (this.operator) {
this.memory = this.evaluateOperator(this.operator, this.memory,
this.current);
}
else {
this.memory = this.current;
}
this.current = 0;
}
public getResult() {
return this.memory;
}
}
Red Flags
All of the following are red flags for module structuring.
Double-check that youre not trying to namespace your
external modules if any of these apply to your files:
Introduction
This post outlines the various ways to organize your code
using namespaces (previously internal modules) in
TypeScript. As we alluded in our note about terminology,
internal modules are now referred to as namespaces.
Additionally, anywhere the module keyword was used when
declaring an internal module, the namespace keyword can
and should be used instead. This avoids confusing new users
by overloading them with similarly named terms.
First steps
Lets start with the program well be using as our example
throughout this page. Weve written a small set of simplistic
string validators, as you might write to check a users input
on a form in a webpage or check the format of an externally-
provided data file.
Validators in a single file
interface StringValidator {
isAcceptable(s: string): boolean;
}
// Validators to use
let validators: { [s: string]: StringValidator; } = {};
validators["ZIP code"] = new ZipCodeValidator();
validators["Letters only"] = new LettersOnlyValidator();
Namespacing
As we add more validators, were going to want to have
some kind of organization scheme so that we can keep track
of our types and not worry about name collisions with other
objects. Instead of putting lots of different names into the
global namespace, lets wrap up our objects into a
namespace.
In this example, well move all validator-related entities into
a namespace called Validation. Because we want the
interfaces and classes here to be visible outside the
namespace, we preface them with export. Conversely, the
variables lettersRegexp and numberRegexp are
implementation details, so they are left unexported and will
not be visible to code outside the namespace. In the test code
at the bottom of the file, we now need to qualify the names
of the types when used outside the namespace,
e.g. Validation.LettersOnlyValidator.
Namespaced Validators
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}
// Validators to use
let validators: { [s: string]: Validation.StringValidator; } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();
Multi-file namespaces
Here, well split our Validation namespace across many
files. Even though the files are separate, they can each
contribute to the same namespace and can be consumed as if
they were all defined in one place. Because there are
dependencies between files, well add reference tags to tell
the compiler about the relationships between the files. Our
test code is otherwise unchanged.
Validation.ts
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}
}
LettersOnlyValidator.ts
/// <reference path="Validation.ts" />
namespace Validation {
const lettersRegexp = /^[A-Za-z]+$/;
export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}
}
ZipCodeValidator.ts
/// <reference path="Validation.ts" />
namespace Validation {
const numberRegexp = /^[0-9]+$/;
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
}
Test.ts
/// <reference path="Validation.ts" />
/// <reference path="LettersOnlyValidator.ts" />
/// <reference path="ZipCodeValidator.ts" />
// Validators to use
let validators: { [s: string]: Validation.StringValidator; } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();
Aliases
Another way that you can simplify working with of
namespaces is to use import q = x.y.z to create shorter
names for commonly-used objects. Not to be confused with
the import x = require("name") syntax used to load
modules, this syntax simply creates an alias for the specified
symbol. You can use these sorts of imports (commonly
referred to as aliases) for any kind of identifier, including
objects created from module imports.
namespace Shapes {
export namespace Polygons {
export class Triangle { }
export class Square { }
}
}
Ambient Namespaces
The popular library D3 defines its functionality in a global
object called d3. Because this library is loaded through
a <script> tag (instead of a module loader), its declaration
uses namespaces to define its shape. For the TypeScript
compiler to see this shape, we use an ambient namespace
declaration. For example, we could begin writing it as
follows:
D3.d.ts (simplified excerpt)
declare namespace D3 {
export interface Selectors {
select: {
(selector: string): Selection;
(element: EventTarget): Selection;
};
}
Introduction
This post outlines the various ways to organize your code
using namespaces and modules in TypeScript. Well also go
over some advanced topics of how to use namespaces and
modules, and address some common pitfalls when using
them in TypeScript.
See the Modules documentation for more information about
modules. See the Namespaces documentation for more
information about namespaces.
Using Namespaces
Namespaces are simply named JavaScript objects in the
global namespace. This makes namespaces a very simple
construct to use. They can span multiple files, and can be
concatenated using --outFile. Namespaces can be a good
way to structure your code in a Web Application, with all
dependencies included as <script> tags in your HTML
page.
Just like all global namespace pollution, it can be hard to
identify component dependencies, especially in a large
application.
Using Modules
Just like namespaces, modules can contain both code and
declarations. The main difference is that
modules declare their dependencies.
Modules also have a dependency on a module loader (such
as CommonJs/Require.js). For a small JS application this
might not be optimal, but for larger applications, the cost
comes with long term modularity and maintainability
benefits. Modules provide for better code reuse, stronger
isolation and better tooling support for bundling.
It is also worth noting that, for Node.js applications, modules
are the default and the recommended approach to structure
your code.
Starting with ECMAScript 2015, modules are native part of
the language, and should be supported by all compliant
engine implementations. Thus, for new projects modules
would be the recommended code organization mechanism.
Pitfalls of Namespaces and
Modules
In this section well describe various common pitfalls in
using namespaces and modules, and how to avoid them.
myModules.d.ts
}
myOtherModule.ts
Needless Namespacing
If youre converting a program from namespaces to modules,
it can be easy to end up with a file that looks like this:
shapes.ts
shapeConsumer.ts
shapes.ts
shapeConsumer.ts
Trade-offs of Modules
Just as there is a one-to-one correspondence between JS files
and modules, TypeScript has a one-to-one correspondence
between module source files and their emitted JS files. One
effect of this is that its not possible to concatenate multiple
module source files depending on the module system you
target. For instance, you cant use the outFile option while
targeting commonjs or umd, but with TypeScript 1.8 and
later, its possible to use outFile when
targeting amd or system.
Module Resolution
This section assumes some basic knowledge about modules.
Please see the Modules documentation for more information.
Module resolution is the process the compiler uses to figure
out what an import refers to. Consider an import statement
like import { a } from "moduleA"; in order to check
any use of a, the compiler needs to know exactly what it
represents, and will need to check its definition moduleA.
At this point, the compiler will ask whats the shape
of moduleA? While this sounds
straightforward, moduleA could be defined in one of your
own .ts/.tsx files, or in a .d.ts that your code depends
on.
First, the compiler will try to locate a file that represents the
imported module. To do so the compiler follows one of two
different strategies: Classic or Node. These strategies tell the
compiler where to look for moduleA.
If that didnt work and if the module name is non-relative
(and in the case of "moduleA", it is), then the compiler will
attempt to locate an ambient module declaration. Well cover
non-relative imports next.
Finally, if the compiler could not resolve the module, it will
log an error. In this case, the error would be something
like error TS2307: Cannot find module
'moduleA'.
import "/mod";
Any other import is considered non-relative. Some
examples include:
1. /root/src/folder/moduleB.ts
2. /root/src/folder/moduleB.d.ts
3. /root/src/moduleB.ts
4. /root/src/moduleB.d.ts
5. /root/moduleB.ts
6. /root/moduleB.d.ts
7. /moduleB.ts
8. /moduleB.d.ts
Node
This resolution strategy attempts to mimic
the Node.js module resolution mechanism at runtime. The
full Node.js resolution algorithm is outlined in Node.js
module documentation.
How Node.js resolves modules
1. /root/src/node_modules/moduleB.js
2. /
root/src/node_modules/moduleB/package.jso
n (if it specifies a "main" property)
3. /root/src/node_modules/moduleB/index.js
4. /root/node_modules/moduleB.js
5. /root/node_modules/moduleB/package.json (if
it specifies a "main" property)
6. /root/node_modules/moduleB/index.js
7. /node_modules/moduleB.js
8. /node_modules/moduleB/package.json (if it
specifies a "main" property)
9. /node_modules/moduleB/index.js
Notice that Node.js jumped up a directory in steps (4) and
(7).
You can read more about the process in Node.js
documentation on loading modules from node_modules.
How TypeScript resolves modules
8. /root/node_modules/moduleB.ts
9. /root/node_modules/moduleB.tsx
10. /root/node_modules/moduleB.d.ts
11. /
root/node_modules/moduleB/package.json (if
it specifies a "typings" property)
12. /root/node_modules/moduleB/index.ts
13. /root/node_modules/moduleB/index.tsx
14. /root/node_modules/moduleB/index.d.ts
15. /node_modules/moduleB.ts
16. /node_modules/moduleB.tsx
17. /node_modules/moduleB.d.ts
18. /node_modules/moduleB/package.json (if it
specifies a "typings" property)
19. /node_modules/moduleB/index.ts
20. /node_modules/moduleB/index.tsx
21. /node_modules/moduleB/index.d.ts
Dont be intimidated by the number of steps here -
TypeScript is still only jumping up directories twice at steps
(8) and (15). This is really no more complex than what
Node.js itself is doing.
This tells the compiler for any module import that matches
the pattern "*" (i.e. all values), to look in two locations:
1. "*": meaning the same name unchanged, so
map <moduleName> => <baseUrl>\<moduleName>
2. "generated\*" meaning the module name with an
appended prefix generated, so
map <moduleName> => <baseUrl>\generated\<mo
duleName>
Following this logic, the compiler will attempt to resolve the
two imports as such:
import folder1/file2
generated
templates
views
template1.ts (imports './view2')
Using --noResolve
Basic Concepts
In TypeScript, a declaration creates entities in at least one of
three groups: namespace, type, or value. Namespace-creating
declarations create a namespace, which contains names that
are accessed using a dotted notation. Type-creating
declarations do just that: they create a type that is visible
with the declared shape and bound to the given name. Lastly,
value-creating declarations create values that are visible in
the output JavaScript.
Declaration Type Namespace Type
Namespace X
Class X
Enum X
Interface X
Type Alias X
Function
Variable
Merging Interfaces
The simplest, and perhaps most common, type of declaration
merging is interface merging. At the most basic level, the
merge mechanically joins the members of both declarations
into a single interface with the same name.
interface Box {
height: number;
width: number;
}
interface Box {
scale: number;
}
interface Cloner {
clone(animal: Sheep): Sheep;
}
interface Cloner {
clone(animal: Dog): Dog;
clone(animal: Cat): Cat;
}
Merging Namespaces
Similarly to interfaces, namespaces of the same name will
also merge their members. Since namespaces create both a
namespace and a value, we need to understand how both
merge.
To merge the namespaces, type definitions from exported
interfaces declared in each namespace are themselves
merged, forming a single namespace with merged interface
definitions inside.
To merge the namespace value, at each declaration site, if a
namespace already exists with the given name, it is further
extended by taking the existing namespace and adding the
exported members of the second namespace to the first.
The declaration merge of Animals in this example:
namespace Animals {
export class Zebra { }
}
namespace Animals {
export interface Legged { numberOfLegs: number; }
export class Dog { }
}
is equivalent to:
namespace Animals {
export interface Legged { numberOfLegs: number; }
namespace Animal {
export function doAnimalsHaveMuscles() {
return haveMuscles; // <-- error, haveMuscles is not visible here
}
}
namespace buildLabel {
export let suffix = "";
export let prefix = "Hello, ";
}
alert(buildLabel("Sam Smith"));
namespace Color {
export function mixColor(colorName: string) {
if (colorName == "yellow") {
return Color.red + Color.green;
}
else if (colorName == "white") {
return Color.red + Color.green + Color.blue;
}
else if (colorName == "magenta") {
return Color.red + Color.blue;
}
else if (colorName == "cyan") {
return Color.green + Color.blue;
}
}
}
Disallowed Merges
Not all merges are allowed in TypeScript. Currently, classes
can not merge with other classes or with variables. For
information on mimicking class merging, see the Mixins in
TypeScript section.
Module Augmentation
Although JavaScript modules do not support merging, you
can patch existing objects by importing and then updating
them. Lets look at a toy Observable example:
// observable.js
export class Observable<T> {
// ... implementation left as an exercise for the reader ...
}
// map.js
import { Observable } from "./observable";
Observable.prototype.map = function (f) {
// ... another exercise for the reader
}
// consumer.ts
import { Observable } from "./observable";
import "./map";
let o: Observable<number>;
o.map(x => x.toFixed());
Global augmentation
You can also add declarations to the global scope from inside
a module:
// observable.ts
export class Observable<T> {
// ... still no implementation ...
}
declare global {
interface Array<T> {
toObservable(): Observable<T>;
}
}
Array.prototype.toObservable = function () {
// ...
}
Global augmentations have the same behavior and limits as
module augmentations.
JSX
Introduction
JSX is an embeddable XML-like syntax. It is meant to be
transformed into valid JavaScript, though the semantics of
that transformation are implementation-specific. JSX came to
popularity with the React framework, but has since seen
other applications as well. TypeScript supports embedding,
type checking, and compiling JSX directly into JavaScript.
Basic usage
In order to use JSX you must do two things.
1. Name your files with a .tsx extension
2. Enable the jsx option
TypeScript ships with three JSX modes: preserve, react,
and react-native. These modes only affect the emit stage
- type checking is unaffected. The preserve mode will
keep the JSX as part of the output to be further consumed by
another transform step (e.g. Babel). Additionally the output
will have a .jsx file extension. The react mode will
emit React.createElement, does not need to go through
a JSX transformation before use, and the output will have
a .jsfile extension. The react-native mode is the
equivalent of preserve in that it keeps all JSX, but the
output will instead have a .js file extension.
Output F
Mode Input Output
Extensio
<div /
preserve <div /> .jsx
>
<div / React.createElement("div"
react .js
> )
react- <div /
<div /> .js
native >
You can specify this mode using either the --jsx command
line flag or the corresponding option in
your tsconfig.json file.
Note: The identifier React is hard-coded, so you must make
React available with an uppercase R.
The as operator
Recall how to write a type assertion:
var foo = <foo>bar;
Here we are asserting the variable bar to have the type foo.
Since TypeScript also uses angle brackets for type assertions,
JSXs syntax introduces certain parsing difficulties. As a
result, TypeScript disallows angle bracket type assertions
in .tsx files.
To make up for this loss of functionality in .tsx files, a new
type assertion operator has been added: as. The above
example can easily be rewritten with the as operator.
var foo = bar as foo;
Type Checking
In order to understand type checking with JSX, you must
first understand the difference between intrinsic elements and
value-based elements. Given a JSX
expression <expr />, expr may either refer to something
intrinsic to the environment (e.g. a div or span in a DOM
environment) or to a custom component that youve created.
This is important for two reasons:
1. For React, intrinsic elements are emitted as strings
(React.createElement("div")), whereas a
component youve created is not
(React.createElement(MyComponent)).
2. The types of the attributes being passed in the JSX
element should be looked up differently. Intrinsic
element attributes should be
known intrinsically whereas components will likely want
to specify their own set of attributes.
TypeScript uses the same convention that React does for
distinguishing between these. An intrinsic element always
begins with a lowercase letter, and a value-based element
always begins with an uppercase letter.
Intrinsic elements
Intrinsic elements are looked up on the special
interface JSX.IntrinsicElements. By default, if this
interface is not specified, then anything goes and intrinsic
elements will not be type checked. However, if
interface is present, then the name of the intrinsic element is
looked up as a property on
the JSX.IntrinsicElements interface. For example:
declare namespace JSX {
interface IntrinsicElements {
foo: any
}
}
<foo />; // ok
<bar />; // error
Value-based elements
Value based elements are simply looked up by identifiers that
are in scope.
import MyComponent from "./myComponent";
<MyComponent />; // ok
<SomeOtherComponent />; // error
function MyFactoryFunction() {
return {
render: () => {
}
}
}
class MyComponent {
render() {}
}
function MyFactoryFunction() {
return { render: () => {} }
}
<MyComponent />; // ok
<MyFactoryFunction />; // ok
class NotAValidComponent {}
function NotAValidFactoryFunction() {
return {};
}
class MyComponent {
// specify the property on the element instance type
props: {
foo?: string;
}
}
Embedding Expressions
JSX allows you to embed expressions between tags by
surrounding the expressions with curly braces ({ }).
var a = <div>
{["foo", "bar"].map(i => <span>{i / 2}</span>)}
</div>
React integration
To use JSX with React you should use the React typings.
These typings define the JSX namespace appropriately for
use with React.
/// <reference path="react.d.ts" />
interface Props {
foo: string;
}
tsconfig.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
Decorators
A Decorator is a special kind of declaration that can be
attached to a class declaration, method, accessor, property,
or parameter. Decorators use the form @expression,
where expression must evaluate to a function that will be
called at runtime with information about the decorated
declaration.
For example, given the decorator @sealed we might write
the sealed function as follows:
function sealed(target) {
// do something with 'target' ...
}
Decorator Factories
If we want to customize how a decorator is applied to a
declaration, we can write a decorator factory. A Decorator
Factory is simply a function that returns the expression that
will be called by the decorator at runtime.
We can write a decorator factory in the following fashion:
function color(value: string) { // this is the decorator factory
return function (target) { // this is the decorator
// do something with 'target' and 'value'...
}
}
On a single line:
@f @g x
On multiple lines:
@f
@g
function g() {
console.log("g(): evaluated");
return function (target, propertyKey: string, descriptor:
PropertyDescriptor) {
console.log("g(): called");
}
}
class C {
@f()
@g()
method() {}
}
Decorator Evaluation
There is a well defined order to how decorators applied to
various declarations inside of a class are applied:
1. Parameter Decorators, followed by Method, Accessor,
or Property Decorators are applied for each instance
member.
2. Parameter Decorators, followed by Method, Accessor,
or Property Decorators are applied for each static
member.
3. Parameter Decorators are applied for the constructor.
4. Class Decorators are applied for the class.
Class Decorators
A Class Decorator is declared just before a class declaration.
The class decorator is applied to the constructor of the class
and can be used to observe, modify, or replace a class
definition. A class decorator cannot be used in a declaration
file, or in any other ambient context (such as on
a declare class).
The expression for the class decorator will be called as a
function at runtime, with the constructor of the decorated
class as its only argument.
If the class decorator returns a value, it will replace the class
declaration with the provided constructor function.
NOTE Should you chose to return a new constructor
function, you must take care to maintain the original
prototype. The logic that applies decorators at runtime
will not do this for you.
The following is an example of a class decorator (@sealed)
applied to the Greeter class:
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
We can define the @sealed decorator using the following
function declaration:
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@classDecorator
class Greeter {
property = "property";
hello: string;
constructor(m: string) {
this.hello = m;
}
}
console.log(new Greeter("world"));
Method Decorators
A Method Decorator is declared just before a method
declaration. The decorator is applied to the Property
Descriptor for the method, and can be used to observe,
modify, or replace a method definition. A method decorator
cannot be used in a declaration file, on an overload, or in any
other ambient context (such as in a declare class).
The expression for the method decorator will be called as a
function at runtime, with the following three arguments:
1. Either the constructor function of the class for a static
member, or the prototype of the class for an instance
member.
2. The name of the member.
3. The Property Descriptor for the member.
NOTE The Property Descriptor will be undefined if
your script target is less than ES5.
If the method decorator returns a value, it will be used as
the Property Descriptor for the method.
NOTE The return value is ignored if your script target is
less than ES5.
The following is an example of a method decorator
(@enumerable) applied to a method on the Greeter class:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@enumerable(false)
greet() {
return "Hello, " + this.greeting;
}
}
Accessor Decorators
An Accessor Decorator is declared just before an accessor
declaration. The accessor decorator is applied to the Property
Descriptor for the accessor and can be used to observe,
modify, or replace an accessors definitions. An accessor
decorator cannot be used in a declaration file, or in any other
ambient context (such as in a declare class).
NOTE TypeScript disallows decorating both
the get and set accessor for a single member. Instead, all
decorators for the member must be applied to the first
accessor specified in document order. This is because
decorators apply to a Property Descriptor, which combines
both the get and set accessor, not each declaration
separately.
The expression for the accessor decorator will be called as a
function at runtime, with the following three arguments:
1. Either the constructor function of the class for a static
member, or the prototype of the class for an instance
member.
2. The name of the member.
3. The Property Descriptor for the member.
NOTE The Property Descriptor will be undefined if
your script target is less than ES5.
If the accessor decorator returns a value, it will be used as
the Property Descriptor for the member.
NOTE The return value is ignored if your script target is
less than ES5.
The following is an example of an accessor decorator
(@configurable) applied to a member of the Point class:
class Point {
private _x: number;
private _y: number;
constructor(x: number, y: number) {
this._x = x;
this._y = y;
}
@configurable(false)
get x() { return this._x; }
@configurable(false)
get y() { return this._y; }
}
Property Decorators
A Property Decorator is declared just before a property
declaration. A property decorator cannot be used in a
declaration file, or in any other ambient context (such as in
a declare class).
The expression for the property decorator will be called as a
function at runtime, with the following two arguments:
1. Either the constructor function of the class for a static
member, or the prototype of the class for an instance
member.
2. The name of the member.
NOTE A Property Descriptor is not provided as an
argument to a property decorator due to how property
decorators are initialized in TypeScript. This is because there
is currently no mechanism to describe an instance property
when defining members of a prototype, and no way to
observe or modify the initializer for a property. As such, a
property decorator can only be used to observe that a
property of a specific name has been declared for a class.
If the property decorator returns a value, it will be used as
the Property Descriptor for the member.
NOTE The return value is ignored if your script target is
less than ES5.
We can use this information to record metadata about the
property, as in the following example:
class Greeter {
@format("Hello, %s")
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
let formatString = getFormat(this, "greeting");
return formatString.replace("%s", this.greeting);
}
}
Parameter Decorators
A Parameter Decorator is declared just before a parameter
declaration. The parameter decorator is applied to the
function for a class constructor or method declaration. A
parameter decorator cannot be used in a declaration file, an
overload, or in any other ambient context (such as in
a declare class).
The expression for the parameter decorator will be called as
a function at runtime, with the following three arguments:
1. Either the constructor function of the class for a static
member, or the prototype of the class for an instance
member.
2. The name of the member.
3. The ordinal index of the parameter in the functions
parameter list.
NOTE A parameter decorator can only be used to observe
that a parameter has been declared on a method.
The return value of the parameter decorator is ignored.
The following is an example of a parameter decorator
(@required) applied to parameter of a member of
the Greeterclass:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@validate
greet(@required name: string) {
return "Hello " + name + ", " + this.greeting;
}
}
Metadata
Some examples use the reflect-metadata library which
adds a polyfill for an experimental metadata API. This
library is not yet part of the ECMAScript (JavaScript)
standard. However, once decorators are officially adopted as
part of the ECMAScript standard these extensions will be
proposed for adoption.
You can install this library via npm:
npm i reflect-metadata --save
tsconfig.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
class Point {
x: number;
y: number;
}
class Line {
private _p0: Point;
private _p1: Point;
@validate
set p0(value: Point) { this._p0 = value; }
get p0() { return this._p0; }
@validate
set p1(value: Point) { this._p1 = value; }
get p1() { return this._p1; }
}
@validate
@Reflect.metadata("design:type", Point)
set p0(value: Point) { this._p0 = value; }
get p0() { return this._p0; }
@validate
@Reflect.metadata("design:type", Point)
set p1(value: Point) { this._p1 = value; }
get p1() { return this._p1; }
}
Mixin sample
In the code below, we show how you can model mixins in
TypeScript. After the code, well break down how it works.
// Disposable Mixin
class Disposable {
isDisposed: boolean;
dispose() {
this.isDisposed = true;
}
// Activatable Mixin
class Activatable {
isActive: boolean;
activate() {
this.isActive = true;
}
deactivate() {
this.isActive = false;
}
}
interact() {
this.activate();
}
// Disposable
isDisposed: boolean = false;
dispose: () => void;
// Activatable
isActive: boolean = false;
activate: () => void;
deactivate: () => void;
}
applyMixins(SmartObject, [Disposable, Activatable]);
////////////////////////////////////////
// In your runtime library somewhere
////////////////////////////////////////
// Activatable Mixin
class Activatable {
isActive: boolean;
activate() {
this.isActive = true;
}
deactivate() {
this.isActive = false;
}
}
Next, well create the class that will handle the combination
of the two mixins. Lets look at this in more detail to see how
it does this:
class SmartObject implements Disposable, Activatable {
The first thing you may notice in the above is that instead of
using extends, we use implements. This treats the classes
as interfaces, and only uses the types behind Disposable and
Activatable rather than the implementation. This means that
well have to provide the implementation in class. Except,
thats exactly what we want to avoid by using mixins.
To satisfy this requirement, we create stand-in properties and
their types for the members that will come from our mixins.
This satisfies the compiler that these members will be
available at runtime. This lets us still get the benefit of the
mixins, albeit with some bookkeeping overhead.
// Disposable
isDisposed: boolean = false;
dispose: () => void;
// Activatable
isActive: boolean = false;
activate: () => void;
deactivate: () => void;
Finally, we mix our mixins into the class, creating the full
implementation.
applyMixins(SmartObject, [Disposable, Activatable]);
Generated JS code:
define(["require", "exports", "legacy/moduleA"], function (require, exports,
moduleA) {
moduleA.callStuff()
});