What do you want to learn?
Skip to main content
Using ES6 with TypeScript
by Steve Ognibene
This course teaches how to use the new syntax features of ES6 today, with TypeScript as a transpiler.
Start CourseBookmarkAdd to Channel
Table of contents
Setting up Atom for TypeScript
New ES6 Syntax
Introduction to Transpiling
Hoisting and Functional Scope with var
Introduction to let
Now that we've reviewed var, let's talk about the new ES6 keyword let. As we said, var hoists and uses function scope. Let is different than var because variables declared with let are not hoisted and they use block-scope. So what does that mean? Well, let's go back to our original example, but change each var keyword to let. Now TypeScript gives us an error, Cannot find name fav. What happened? Well firstly the let keyword in ES6 is block-scoped. Blocks can general by thought of the closest containing set of curly braces. This'll be familiar to those who also know languages like C, C#, or Java. With let the first fav is a variable that is only scoped to these curly braces, and the second fav is a variable that is only scoped to these other curly braces. Because it's outside of the scope of the fav/let declarations, the final fav here doesn't reference anything at all, which is why TypeScript gives us an error. So while the var keyword gives us function scope, the let keyword gives us block-scope. What happens if we fix this by adding a new line to declare the variable up top? Let, fav. Well now this is even worse because we've got three versions of fav, the one we just made in the block-scope bounded by the curly braces on the function, and a separate one in each of the if branches. As written this function will always return undefined because the values are only set on the variable scoped to the branches, not the one declared at the top of the function, which actually gets returned. To get this to work correctly we need to remove the let keyword in each branch. Now there's one fav variable that exists throughout the function and it'll work as intended. The second major difference from var is that the let keyword doesn't hoist. To see this in action let's recreate our eyeball example where we declared the variable after the return statement. Because of hoisting this code worked fine when using var. But what happens when we change it to use let? Now TypeScript gives us an error every time we reference fav. Block-scoped variable fav used before its declaration. The reason we get this error is because variables declared with let are not hoisted. The area within the block above the let declaration is called the temporal dead-zone. That sounds like the name of a heavy metal band, but it's really just a fancy term for saying, you can't use a variable declared with let until after the let keyword has been hit. Because of hoisting it's okay to re-declare a variable with the var keyword inside a scope. The var keywords are hoisted to the top of the scope and the lines are treated as assignments. However, with let re-declaring a variable in the same scope becomes a duplicate identifier error. This can be useful to identify possible bugs in your program.
Using let in a for Loop
Scope Per Iteration Behavior of let
Introduction to const
The const keyword in ES6 is very similar to let. Variables declared with const are also block-scoped and aren't hoisted. The main difference is that the value of a const must be set when it is declared and it can't be changed later in the scope. Let's take a look at an example. In this TypeScript file let myvar is fine. If I change it to const myvar it's an error. In ES6 you have to provide a value for a const when it's declared. Const in ES6 behaves a bit differently than in some other languages though. For example, an ES6 const can be instantiated to the return value of a function or an expression, it doesn't have to be a true constant like a string literal or an number. This means that code like const startTime = Date.now or const answer = confirm Are you sure? works perfectly fine. Const effectively prevents reassigning a new value even when it's used to declare an object. However, there is a wrinkle. Here I'll create an object literal. If I try to set simpleLiteral to a new value TypeScript shows me an error. However, if I try to set the value on a property on simpleLiteral this works fine. So it's important to remember that the value of an ES6 const itself must remain constant, but the value of any of its properties may be changed. Does that mean that there's no way to have the properties of an object be constant via the const keyword? Well, it's possible to achieve this using a TypeScript namespace. Namespace is a new keyword that was introduced in TypeScript 1.5. It replaces the use of the module keyword when using what was previously called internal modules. Namespaces are a TypeScript only concept and not part of the ES6 standard. Let's do a quick test of how properties declared with const work in a TypeScript namespace. I'll create a new namespace called AtomicNumbers. Then I'll add hydrogen and helium to it as exported constants. The export keyword means that this property is available outside the namespace. If I try to re-declare hydrogen, even if I set it to the same value, I get the error, Cannot re-declare block-scoped variable H. If I simply try to change its value I get Left-hand of assignment expression cannot be a constant. I even get his error if I try to modify the namespace from outside, AtomicNumbers.H = 3. If I were to change the keyword on H to let or var this line would work. So this might make you wonder if const works with modules does it also work with ES6 classes? The short answer is no. Early on in the proposal process there were discussions about allowing that, but in TypeScript and the final version of ES6 the const keyword can't be used to declare class members.
When to Use let and const?
So when should you actually use let and const with ES6 and TypeScript 1.6? I think that using the new keywords where appropriate can help to clarify the intended purpose and scope of your variables. As a bonus it's also nice to eliminate the need for hand-coding, IIFEs, and for loops. But as we saw, that feature still requires a second-stage transpiler like Babel if using a version of TypeScript prior to 1.8. If you're looking for a good recommendation I think a fair one is that first you should try to use var when you intend for a variable to be resistant to re-declaration. Mostly this will be for intentionally global variables that are used across files, that scenario should be pretty rare. The second case where you may still wish to var is inside a function if the block-scoping behavior of const or let creates an awkward situation. We'll see an example of this with a try/catch block in just a moment. Pretty much you should use let or const at all other times. The tighter scope of let and const declared variables will hopefully lead to more correct programs that are easier to understand. In addition you should try to use const over let as much as possible. You'd be surprised how often variable don't need to be updated after being initialized, particularly object literals. Don't forget that it's fine to update the properties on a const variable, it's just the variable itself that can't be changed. A cool thing about using TypeScript is that it can tell you instantly if a variable you've declared with const is ever used in the left-hand side of an expression. For these variables you just have to use let instead. To see let and const in action I'm going to open up the coinCounter project in our sample code. And I'll look at coinCounterViewModel. CoinCounter is the program I convert to TypeScript in my other Pluralsight course, Practical TypeScript Migration. This is the main view model for our game. Let's see if we can refactor uses of var to let or const. On line 8 I'm declaring highScoreIndex. It doesn't seem like we're setting it again so we should be safe using const. On line 9 I'm using var to declare a message. We modify the value of message on lines 11 and 14 also so it can't be const, but let's try it anyway. Sure enough, TypeScript tells us that the Left-hand side of assignment expression cannot be a constant. Okay TypeScript, we'll use let instead. I refactored the rest of coinCounterViewModel off camera to change all uses of var to let or const. In 12 uses of var I was able to convert 9 to use const and only 3 had to be let. None were required to remain as var. Even if you're excited about let and const, something I can't recommend doing is a global find and replace from var to let in your code base. This will almost certainly cause problems where your code is perhaps unintentionally relying on hoisting. Even in the simple examples that I showed here we saw that happen when var was used twice within the same variable in a function. Another scenario that you might see is when var is used to declare a variable inside a try/catch block that is then used outside the try/catch block. Here's an example from the open source TypeScript build system grunt-ts. By the way grunt-ts isn't included for the sample code download for this course, but it's available to the public under the MIT license on GitHub at this URL. I'm inside tsconfig.ts, which is inside the tasks-modules folder. This code declares a variable called projectSpec with type ITSConfigFile, and then sets it value by parsing some JSON. The code runs inside a try/catch block that will exit the function if an exception occurs when parsing projectFile text content. If the parsing doesn't raise an exception these other two lines will run. This is an interestring scenario. The value of projectSpec is never changed other than when it's set on line 84. However, if we change it to const we can see that lines 89 and 90 now have errors. This is because const uses block-scoping and now projectSpec is scoped to these curly braces. We could move lines 89 and 90 up into the try/catch block, but then any exceptions thrown by the applyCompilerOptions or resolve_out_and_outDir functions would be incorrectly indicated as JSON parsing errors in the catch block. So we're effectively prevented from using a const here because of our exception handling. We could try using let, but then we'd have the same scoping problems as const. The only way to fix this is to use let to declare the variable outside the try/catch block and then to merely set the value inside the block. I'm not certain that this new code is actually better than just using var. I suppose it's good because the temporal dead-zone at least prevents us from accidently using projectSpec above this line, but now it's an extra line of code to maintain. In this case your team will have to make a decision as to if using let actually gives you benefit versus just using var inside the try/catch. So as you work on your code base feel free to change var to let or const in whatever are you're working on as long as it makes sense. I've found that I can use const most of the time, let a bit less often, and that generally I don't need to fall back on var, but there are times it makes sense. Don't feel any pressure like you need to abandon var, var is not going away, and it's still a perfectly valid way to declare variable in ES6. You're not doing anything wrong by keeping it around where appropriate, especially in working code that you haven't touched recently, or if using let or const don't feel right in a given scenario.
ES6 Arrow Functions
Using Defaults and Deep Destructuring
Destructuring Arrays with Rest and Spread
ES6 String Templates
Tagged String Templates
String templates aren't only about allowing a nicer syntax for concatenation. It's possible to do some preprocessing when on each of the elements when using a template string as well. This is a bit more of an advanced topic, but I think it might be valuable to show just to get you thinking. I'm going to type friend in front of this template to left of the opening backticks. This is called tagging a template. TypeScript gives me an interesting error, Cannot find name friend. Hmmm, if it can't find something that means it must be looking for something named friend. An ES6 tagged template must be accompanied by a function of this same name that takes two parameters, both of which will be string arrays. I'll type function, friend, strings, colons, string array, comma, … substitutions, colon, string array. The signature of a tag function must be a string array followed by a rest parameter string array. The strings parameter will be an array of all the strings that were included in the template, and substitutions will be an array of all the substitutions. ES6 will pass in each substitution to a tagged template processors function as an argument. With the rest operator we're saying, just make all those into an array. So what should the body of a tag function be? Well, technically it can anything you'd like, whatever's returned from the function is what the tag/strings template will be resolved to. However, most commonly the body of a tag function will remember something link there. So we're creating an empty array and then looping though the elements of substitutions. For each substitution we're pushing the element of string that has the same index, and then the substitution itself into our result array. When called the multiGreet, the first element in strings would always be Hello, space. And the first element in substitutions when called with names would be Alice. In our example there's only one substitution so the forEach would end at that point. Then we push the last string, which would be dot, and then return those items all concatenated together as one string used in the join function. Note that if the last part of the template is itself a substitution, such as if we didn't have a period here, and the curly brace was the last character of the tag template, ES6 and TypeScript automatically add a blank string element into the strings argument. This lets us safely skip an undefined check here. Strings will always be exactly one element longer than substitution. This is boilerplate code so let's actually extract it as its own method card called processTaggedTemplate. And then we can call it in our friend/tagged function. ProcessTaggedTemplate strings… substitutions. Okay, now I'm noticing something a bit odd here. Friend is expecting a series of parameters to be passed in that we wrested together in the substitutions argument, that's fine. However, we're then taking that argument and spreading it out again because processTaggedTemplate is also expecting a series of individual parameters. There's no sense doing double work here so let's modify our boilerplate function and make it just take an array by getting rid of the rest operator. Then we can eliminate the spread operator and friend, which means that it'll just pass substitutions as an array, that's better. Oh whoops, I forgot to put return here, let me fix that. Now what do we do inside friend? Well, I'll tell you a story. Fairly recently I received an advertisement email that started with Dear null, well rather than let one of our customers go through an experience like that let's check for a falsey value here and replace it with friend. If not substitutions 0, substitutions 0 = friend. Great. Now if a null, undefined, or blank string is provided as the first substitution to any string template tagged with friend, it'll be replaced with friend. There's plenty of other sorts of tags we could write. I've seen people suggest automated HTML escaping, or URL escaping, or even the creation of well-formatted HTTP headers. Also possible might be conditionally logging any of the passed in substitutions to the console of a debug variable is set. Or how about throwing an exception in the presence of a particular value in the first substitution. It really could be anything, it's quite open. Don't forget though, you don't have to use a tag if you just want the template substitution functionality. If you don't specify a tag on a shrink template it works just like a more terse version of concatenation.
Using the ES6 for of Loop
Module 2 Summary
In this module we covered much of the new syntax that's currently supported by TypeScript. We saw how const and let can clarify the scope of variables, and we discussed when using var may still be appropriate. We looked at arrow functions and talked about how they offer a terse function syntax that also adds predictability to what this references. We covered destructuring of objects and arrays, and discussed the new ES6 rest and spread operators. Then we took a look at ES6 string templates, including tag templates. Finally we took a quick look at the new for of loops syntax in ES6. Because TypeScript acts as a transpiler you can start using these new ES6 syntax features in today's ES5 environments. In the next module of this course we'll move on from general syntax features and spend some time focusing on ES6 modules. That's coming up next in Using ES6 with TypeScript.
ES6 Module Syntax and the Missing Module Loader
Introduction to ES6 Modules
Converting a File to an ES6 Module
Importing an ES6 Module
In this clip we're going to talk about how to import ES6 modules. Now that wowify is a module we'll have to import it anywhere we want to use it. There's two basic ways that we can choose to do the import. The first is to import everything in the wowify module to a placeholder object and then dot off of the placeholder to get what we need. The second is to import only the objects we need from the module into the local variables. For example, if we don't need mehify we don't need to have it in scope. In program.ts I'm now getting the error Cannot find name wowify. As I mentioned before, as soon as you either import something or export something from a TypeScript file that file becomes a module. This means that its objects are no longer available on the global scope so we'll have to import it. The ES6 syntax to import everything that's been exported from a module into a placeholder variable is import star as placeholder name, wowify in this case, from path to module excluding .js. In this case the path will be ./wowify. So now we've imported each thing that was exported form the module that lives in the current folder called Wowify and stored in all in an object called wowify. Funny, our error didn't go away, but now it's something different, Cannot invoke an expression whose type lacks a call signature, do you know why? Well, this syntax says that we want to import everything that was exported into a placeholder called wowify. Each exported object will be a property on that placeholder. It's technically called a module namespace object. This means I can type wowify. and I'll see the completion for both wowify and mehify. So the fix is to change the call to wowify.wowify and then the error goes away. I don't want to have to dot off of a placeholder for wowify though, so let's revert that chain and try something else. If I want wowify to reference the exported function and not the module I can use some syntax that looks a little like destructuring, import, open curly, wowify, close curly, from ./wowify. This syntax instructs the environment that there will be one or more things exported from the module in the same folder called Wowify and we want the exported wowify object to be assigned to a local variable also called wowify. Cool stuff. Now our TypeScript knows that wowify is a function again, though we're accessing it as an exported member of a module. The mehify function isn't in scope anymore, but if we still wanted to get it we could put comma mehify here and it would be imported too. Again, you may think that this looks and sort of feels like a destructuring assignment, but it's not. ES6 module import statements also use the as keyword to rename imported objects instead of the colon, which is what's used to rename things when you're doing destructuring. So I could rename wowify to w, and then access it here using w, and everything would work.
There's one more feature of ES6 module syntax that I want to show you and that's default exports. If you create a module and, if most of the time people are going to want to use one object from it, it's possible to mark that one object as the default. For example, in the wowify module most people are going to want to use the wowify function. I can mark wowify as a default export by typing as default. However, now program.ts is broken with the error, wowify has no exported member wowify. This is true. You may have noticed that the syntax to mark an exported object as a default was suspiciously similar to this syntax to rename an exported object to default. The funny thing about ES6 default exports is that they are literally just an export named default. And I can say literally because we actually wrote it down. This means that I can write default as here and now the default export from wowify, which has been renamed as default, will be imported and renamed back to wowify. Now I think the designers realize that this syntax is a bit awkward, so I'm happy to say that there's a shorthand to import default exports. And that's to just specify the local variable name, import wowify from ./wowify. This is the same as saying default as wowify. While we're at it it's also possible to combine the two. For example, I can put comma, open curly, mehify, close curly here, and mehify would be imported as mehify, and the default export would still be imported as wowify. Depending on the kind of library you're writing, it might be useful to have a default export. The trouble is that it can be awkward to force consumers to use different import syntax for the default object in the library versus any other objects. There's a happy compromise though, and that's with doing a double export for the default object. In wowify.ts I'll add wowify as an export again. Now a module that imports from wowify can access either wowify or mehify using the curly brace syntax, or the developer can choose to access wowify only by the shorthand default export syntax.
ES6 Module Syntax Review
Using AMD and RequireJS
Ambient External Module Declarations
Using CommonJS and Node
In this clip we'll get wowify working with NodeJS. This should be a lot faster since our code is already set up to use external modules. First, I'll open the tsconfig.json file and set the module format to use commonjs, all lower-case, and save. Then I'll copy program.ts to a new file, programNode.ts. I'll open programNode.ts and save it and wowify.ts and save it. If I press Ctrl+Shift+M I'll see the emitted code for wowify. You can identify a commonjs module that exports members because it will add them as properties to a seemingly global exports object. Another trait of commonjs modules can be seen if I view the emitted source for programNode.ts. Here the import statements have been converted to calls for the global require function. Since we're intending to run this code on the console in NodeJS it doesn't make sense to use jquery anymore, so I'll remove that import. To make the output look interesting though I'll use a library called chalk, which I previously fetched from npm along with its TypeScript definition from DefinitelyTyped. Import star as chalk from chalk. Now I can comment the jquery call and instead write console.log, chalk.bold.yellow, open/close. Then I'll copy the result from before and change the join string to be the new line character. On the command line I'll quit the web server with Ctrl+C, that'll run node Modules, programNode.js. Our interesting things are printed in bright yellow, talk about wow. If that seemed a lot easier to set up it's because it was. First of all, NodeJS has a CommonJS module loader built into it, so we didn't' have to set up something like RequireJS ourselves. Secondly, modules obtained from npm follow specific conventions and come with metadata files that help lessen the requires configuration. And thirdly, NodeJS can cheat a bit while resolving modules because it has access to the file system. It's not prohibitively expensive for Node to try a few well-known places on disk and read through configuration files when trying to find a module. On the other hand it would be terrible for performance if RequireJS asked the browser to open multiple HTTP connections per script so that it could search around. The import statement for chalk doesn't include a relative path, meaning it doesn't start with dot or dot, dot. With this style of module identifier NodeJS looks in its node_modules folder for a subfolder of the same name. In this case it can see a file called Index.js in the chalk folder so it loads that. Additional configuration options are available in the package.json file. So there was a lot done for us when using Node due to the conventions of the CommonJS module format versus what was needed with AMD and RequireJS in the browser. There are also tools that allow the use of CommonJS modules in the browser. Two common ones are Browserify and webpack. If you're interested Joe Eames and Jeff Valore have published Pluralsight courses on those packages.
Using the UMD Module Format
There's another module format supported by TypeScript that was added in TypeScript 1.5 called UMD, that stands for Universal Module Definition. This is an interesting module format since UMD-formatted modules can be loaded in either NodeJS or in the browser via RequireJS. I'll open the tsconfig.json file and set the module format to umd, all lower-case. Then I'll open and say wowify.ts, program.ts, and programNode.ts, that's it. If I press Ctrl+Shift+M on wowify.ts I'll be able to see the emitted js. The clever part about universal modules is in this top part. At runtime scripts in the UMD format check for global variables that are distinctive between AMD and CommonJS, and depending upon which globals are found the module will be initialized in the appropriate manner. If you're writing a module that is appropriate to use in both CommonJS and AMD environments you may wish to consider using the UMD format.
Using SystemJS with ES5 Exports
Let's further examine the detail of how the ES6 import star syntax works and how SystemJS chooses to model it when loading ES3 or ES5 libraries like JQuery. Earlier in this module I explained how import star means import each thing that is exported from the module and assign it to a placeholder object, officially a module namespace object. So in this illustration here we have a module that exports properties foo, bar, and baz, and also a property marked as default. If I were to import this module using the syntax, import star as thing from the module path I'd be able to call thing.foo, thing.bar, thing.baz, and thing.default. Let's refer to this as the has a structure. This ES6 module has foo, has bar, has baz, and has default. With ES6 modules module namespace objects are always inert. It's part of the design that anything declared in this middle part should not be accessible externally. In our diagram the circle here represents the code in our module that isn't explicitly exported. However, libraries written prior to ES6 don't expect to follow that rule. Let's use JQuery as a specific example. The JQuery library is structured as a function that has many properties on it. Here I've represented the JQuery function itself as the middle circle. This function is what's commonly aliased as $. I've also illustrated a small sample of the JQuery function's properties, map, ajax, and extend. Because of these properties JQuery follows the has a structure, meaning it has map, it has ajax, it has extend, but it's different than the ES6 module we were discussing earlier because it is also an is a, JQuery is itself a function. So what happens when we make the call import star as $ from jQuery? As before, this means that we want to take everything hanging off of the module on that path, ./jQuery in this case, and assign it as a property of the $ object. Okay, great, so this means I can call $.map, $.ajax, and $.extend, all the has a's work fine, however, the canonical use of JQuery is as a selector, calling the $ function itself directly, $ myDiv. Notice it's not $.something, we're calling $ itself as a function. But ES6 module namespace objects are intended to be inert and SystemJS treats them as such. This explains where we got that $ is not a function error in the last clip. SystemJS was being strict about how ES6 module namespaces objects work in a way that we didn't experience with either RequireJS or the CommonJS implementation built in the NodeJS. SystemJS dutifully imported the properties on the JQuery function, the has a's, but it didn't allow $ to be a function, the is a, so how do we fix this? Well, the way that SystemJS supports is a exports when loading an ES5 or ES3 library is via a synthetic default export. When loading non-ES6 modules the SystemJS module loader automatically promotes the module to work with the default import syntax. Even though there isn't an export called default on JQuery the $ function will still work as expected when using import$ from JQuery along with SystemJS. Let's try that now. Okay, so here we are back in program.ts. I'll change the import line to import $ from jquery. If we open the emitted JS we can see that TypeScript is calling into a default member of the JQuery object. Remember that when loading CommonJS, AMD, or global modules, SystemJS makes the full module available at the default member, so this works correctly to load jquery as long as we're using SystemJS. Let's check the page in the browser, sure enough it works okay. I should mention that TypeScript 1.8 makes this much easier due to a new feature that embraces the default load an functionality of SystemJS. Since the module style is set to system in tsconfig.json the default import syntax works automatically. In TypeScript 1.7 and earlier though you'd get the error Module jquery has no default export, which is technically correct, jquery doesn't have an export name default. If you're using TypeScript 1.7 and you get the Module has no default export error, you're best bet is manually add a new default export to your TypeScript definition files to make TypeScript happy until you have the chance to upgrade. TypeScript 1.8 does make integrating with the SystemJS loader much easier. So that's the Using ES6 module syntax with TypeScript and the basics of how you use RequireJS with AMD, CommonJS with Node, and SystemJS with the System.register format and non-ES6 libraries. We also briefly discussed the UMD module format, which will work any of those loaders at the cost of slightly larger script sizes. In the next module we'll cover another major syntax feature of ES6, classes.
A Multi-paradigm Object Story
Creating and Using an ES6 Class
Using a Constructor
Destructuring in a Constructor Signature
If you want to use an object to instantiate your class, destructuring works great with constructors. I'll comment out and copy the constructor and change the argument to a destructuring assignment for name, phone, and email. Let's leave email optional by assigning a default of undefined. Now I can change my new call to pass an object literal containing the property's name and phone and this'll work. If you're not sure what's going on here please review the clips on destructuring in this course. When a class begins to get complex passing in arguments as named properties on an object quickly becomes the only practical way to keep things straight with different permutations of optional arguments. And destructuring is a great way to accept such objects. I should mention that if you often find yourself copying properties of the same name from one object to another you may wish to look into the extend function that's available as part of JQuery, _JS, and other libraries. The extend function copies name properties from one object to another and can help simplify code like this.
ES6 supports class inheritance via the extends keyword. Let's say I want to create an employee class that inherits from Contact. We want our new employee class to have a name, email, and phone property, as well as a greet function, but we also want our employees to have an employee ID and a hire date. We can do this by writing class Employee extends Contact. Since I'm using TypeScript I can define the employee ID and hire date properties at type annotations so they won't exist on the class unless they're set, but I'll still get IntelliSense support for it in my editor. I can create a new employee by writing const pat = new Employee. And then I need to call it into a constructor. Since I didn't provide a constructor for employee the constructor is still the one on Contact. (Working) However, since Pat is an employee I can now set a hire date. New Date, 2015-01-01. I can print out the hire date by logging pat.hireDate.toUTCString. Notice how TypeScript gives me autocomplete on the date methods. I can also call into the greet function, which part of Contact, pat.greet Renee. Finally, I can check if Pat is an instance of employee and Contact by using the instanceof operator. Console.log, pat instanceof Employee, console.log pat instanceof Contact. Both of these will return true. One thing I want to point out about class declarations is that they're not hoisted. If I move the declaration of Employee above Contact, TypeScript will be happy. However, this will result in a Cannot read prototype of undefined error at runtime with TypeScript. Or, in the future a temporal dead-zone violation. So just be aware that the base class needs to be defined first. ES6 is not like some other languages where class definitions exist in ambient space. I'll move that back.
Calling Code from the Derived Class
ES6 classes provide a convenient syntax for accessors, often referred to as getters and setters. Accessors aren't technically a new ES6 feature, but they work so well with ES6 classes I thought it would be useful to cover them. If you try to use an accessor with TypeScript in ES3 mode it'll raise a compile-time error. Getters and setters are specialized properties on a class that allow custom logic, but are more convenient then using functions. Let's say that in our company employee IDs allow any alpha-numeric character, but all letters must be upper-case. I'll mark the employeeID field as private and change it to _employeeID. The underscore doesn't have any special meaning to TypeScript, but it sometimes uses it a convention to notify a field. Now I can define a getter for employeeID. Get employeeID, open/close parentheses. This works just like a method so I can return this._employeeID. Now we can also create a set routine, set employeeID, value. The value here is the string that we want to pass in. I'll write this._employeeID = value.toLocaleUpperCase. Notice that I'm setting the private variable here, the one with the underscore. If you accidentally try to set the setter you'll get a Stack Overflow error because it'll call itself recursively. One last thing that I'll put or blank string on here in parentheses. This'll illustrate that an actual string will get used if the setter receives a falsey value like null or undefined. Notice that we're still setting this.employeeID in the constructor, so even if the employeeID is passed into the constructor as null, it'll be set as blank string. Let's see that. (Typing) And if I change it to abc123 lower-case, when I run it I see ABC123 with capital letters. One thing to note about getters and setters their values aren't included in a call to JSON.stringify. If I log JSON.stringify, pat, I don't get the employeeID accessor, instead I get the _employeeID field. A common thing to do with accessors is to omit a setter. That makes the corresponding property read-only. Attempting to set a property that has a getter, but not a setter is an error in strict mode. And technically the code inside an ES6 class always operates in strict mode, even it's not set in the containing scope. Just be aware that TypeScript doesn't give an error in this scenario.
ES6 supports some interesting ways to declare and even pass around classes beyond what we've already covered. The first is declaring a class as an expression. Let's say that I have a class called MyWebService with a method called getData. For arguments sake let's say that getData makes some sort of an expensive call based on an ID. While testing our system we won't really want to call MyWebService and waste time and money, So perhaps instead we can make a fake version of MyWebService and test against that instead. I'll change this to Fake_MyWebService and just make this log out the id. So now I've got a new class, but how do I make code that uses MyWebService use Fake_MyWebService instead? Well one way is by converting these declarations to expressions. I'll create a variable here, const useFakes = false. Let's assume that we get that from a config file somehow. Then we can say if not useFakes var MyWebService = this thing, else var MyWebService = this other thing, close curlies. Now down here we can use our web service. Var webService = new MyWebService, WebService.getData, 5. If we run this we should get no result, and that's because our current implementation is just pretending to make an expensive call here. However, if I change useFakes to true and run it, we can see that we get the log message that we called the fake web service with ID5. Class expression support and immediately invoke style 2. This is convenient syntax for creating a singleton. I can put new before the class keyword and open and close parentheses after and now MyWebService will be an instantiated object instead of just a class. I'll do that for the bottom too and change the call appropriately. If my constructor required parameters I'd pass them in here. Now that I've got an instantiated class I don't have to call new on MyWebService anymore. And if I try it, yep, it still works and calls my fake web service that just solved the parameter. I should note that there's bug in TypeScript 1.6 where class expressions require an extra set of parentheses in order to work correctly at runtime. I added those off camera in this demo. This emit issue was fixed in TypeScript 1.7 in the extra set of parentheses are no longer necessary. One thing that I should mention is that you technically don't have to provide a name for the class to find in a class expression, but doing so can make stack traces more helpful, and it enables the use of static methods, which we'll discuss in the next clip.
ES6 classes support static methods and properties. Static methods and properties can be used without creating an instance of the class. Static methods are often used as utility functions, and static properties are often used for things like caching or tracking class-related metadata. One common example is keeping track of how many instances of a class have been created. I'll create a new class called TestStatic and define a method called doubleNumber. This method will take a number and double it. I'll tag the method with the static keyword. Since doubleNumber is static it exists on the class itself, not on instances of the class. So to call it I don't have to set up a new TestStatic first, I'll just call it directly. Console.log TestStatic.doubleNumber 10. When I run my code this returns 20. Let's pretend that I added some instance members to TestStatic, and now I'd like to keep track of how many TestStatic instances have been created. I can add a static member count here, and then if I add a constructor I can call TestStatic.count += 1. Notice again, I called count using TestStatic, not using the this keyword. This is for members on the instance. Since count is static it's not on the instance, it exists on the class itself. Now if I create two variables as new instances of TestStatic, and then console.log TestStatic.count, I'll see 2. I recommend using static methods whenever your code doesn't require accessing member variables, which with ES6 classes generally means methods that don't contain the this keyword. One more thing I should note is that creating a static property inside the class body with an initializer like this is only valid in TypeScript. Similar to how ES6 doesn't support ambient class property initializers, ES6 also doesn't support ambient static initializer statements like this. The valid ES6 syntax would be to put a line just outside the body of the class, TestStatic.count = 0, and that's how TypeScript will transpile this code when emitting to an ES6 target.
Abstract Classes and Interfaces
I'd like to wrap things up by talking about some TypeScript-specific features of ES6 classes. In this clip we'll discuss how to write a class that implements an interface, and we'll also discuss the new abstract keyword. TypeScript 1.6 added support for abstract classes and methods. Abstract classes are intended to be used as base classes for other classes. To enforce this TypeScript will provide a compile-time error if it sees an attempt to instantiate an abstract class. Let's say that we're writing an arcade game. We might create a class called Sprite that represents an object on screen. Sprite might have an x and a y property that stores the Sprites location, and an image URL property that holds the source, the graphic. I could then create some classes that extends Sprite. Let's call one Player and the other Monster. I'll instantiate Player, const p = new Player. We can see that p has the x property and TypeScript knows it's a number, nothing unusual here. As the designer of this system I might not intend for developers to be able to create instances of Sprite directly, but that's currently possible. Const s = new Sprite. There's no complier error from doing that because it's just a regular class. To ensure that creating a Sprite is not possible, in TypeScript I can mark Sprite as abstract. Now I get the error, Cannot create an instance of the abstract class Sprite. In our game we'll call an update method on the Player and Monster objects on every frame. This'll allow the Player object to check for input and will allow the Monster objects to run AI. It'd be nice if we could have TypeScript force any class that implements Sprite to have an update method. We can do that by declaring the update method on Sprite and marking it abstract. Abstract methods can't have an implementation, in fact they can't even have curly braces, or it'll be an error. Now we get two errors, one for each of our classes, Non-abstract class doesn't implement inherited abstract member update. I'll implement update in Player and check for input, and we'll also implement update in Monster and have our Monsters run their AI routine. Another way to standardize structure between classes, or really any kind of object in TypeScript, is with interfaces. Interfaces are very flexible because they're not tied to an inheritance chain. Any object can be marked to implement any interface, and on my class hierarchies interfaces are purely an artifact of the type system, which are erased at compile time. I'll define an interface to represent a Sprite, interface ISprite, x: number, y: number, imageURL string, update function that returns void. Now I can mark Sprite with implements ISprite. So far so good. Now let's say I add a new string property called name to ISprite. TypeScript informs me that Sprite incorrectly implements the interface. I can add name = blank string to Sprite, and TypeScript is happy again.
Steve Ognibene is an application developer in New York City specializing in SQL Server, C#, VB.NET and TypeScript. Steve is an active contributor to open source projects and is the author of the...
Released25 Feb 2016