Since its inception, JavaScript has experienced monumental growth - especially in recent years.
The language has expanded its application domain far beyond the browser. It is now used to power backends, create hybrid mobile applications, architect cloud solutions, design neural networks and even control robots. The versatility of JavaScript paired with its high adoption rate has created an increasing demand for scalable, secure, performant and feature-rich JavaScript applications. This in turn has created a need for tooling, libraries and frameworks that make it easier and faster to write applications that meet those expectations.
Table of contents
This demand for high performance, maintainable and robust JavaScript led to the introduction of TypeScript.
Let’s explore in detail what the design goals are, and benefits that using TypeScript brings to any codebase - from a small side project to a large enterprise one.
If you’d like to learn more about TypeScript after this intro, check out our TypeScript courses.
TypeScript is a Superset of JavaScript
TC39, the committee that regulates the development of JavaScript, has introduced new features to the language with each release version. Recently added features such as classes and block-scoped variables make standard JavaScript more robust. However, the language can be further enhanced and hardened to handle highly complex architectural demands in a reliable and predictable way. TC39 oftentimes has features in its pipeline that would help achieve that architectural goal but it takes time for them to become part of the standard language and to be supported by all major browsers.
As each new version of JavaScript extends the previous one, we could think of “Future JavaScript” as a superset of the current standard one. With that model in mind, TypeScript was created to act as that superset of JavaScript that puts the future of the language in the hands of today’s developers. Moreover, TypeScript integrates features outside of the scope of TC39, such as type-checking, generics and interfaces, that mitigate many points of failure present in JavaScript and rev up development - all provided through abstractions written in JavaScript. All that TypeScript gives you is convenient syntactic sugar that eventually gets all converted to cross-platform JavaScript.
Let’s explore in detail the architecture and components of TypeScript to understand its benefits deeply.
TypeScript Architecture: Design Goals
Microsoft designed TypeScript with specific architectural parameters in mind that allow TypeScript to integrate fully and easily with existing JavaScript code while providing robust features external to JavaScript.
JavaScript Compatibility with TypeScript
As we’ve established, TypeScript is a superset of JavaScript. This relationship permits TypeScript to understand and work with any code that is valid JavaScript. Any valid JavaScript code is valid TypeScript code with only a few exceptions: handling option function parameters and assigning a value to an object literal.
Do take note that valid TypeScript is not valid JavaScript code. TypeScript contains syntax and abstractions that do not exist in JavaScript and using them with JavaScript would generate JavaScript runtime errors. However, in an effort to promote compatibility, TypeScript developers align the language with the pipeline of ECMAScript. Current and future ECMAScript proposals are considered when designing new TypeScript features.
Giving Type Check to JavaScript
JavaScript being a loosely typed language is extremely lenient on the value assigned to its variables and it creates no structural contracts of any kind between those variables and the constructs that use them. Passing a number argument to a function that expects a string parameter generates no errors in JavaScript during development but will create havoc during runtime when the body of the function is not able to use that argument correctly.
To prevent these runtime issues, TypeScript was designed as a strongly typed language that performs static type-checking during its compilation time to JavaScript. For flexibility, the type-checking capabilities of TypeScript are optional; however, most of TypeScript key benefits revolve around type-checking - it’s the main reason to use TypeScript! For example, type-checking lets the language service layer of the language to be used for creating better tools that maximize your productivity while reducing the instance of errors.
More Powerful JavaScript Object Oriented Programming
The syntactic sugar provided by TypeScript will allow us to reduce the footprint of our code significantly while increasing its expressiveness. TypeScript makes writing class object-oriented code a breeze. It provides us with classes, interfaces and modules that allow us to properly structure our code in encapsulated reusable structures that makes it easy to maintain and scale. Within classes, we are also able to specify the visibility level of class properties and methods by using TypeScript provided modifiers - public
, private
and protected
. There are many other abstractions that will make us happy and productive developers!
Zero Overhead
As TypeScript developers, we work in two different contexts - design and execution. In the design context, we use TypeScript directly to write our application. Now, since TypeScript is not supported by any browser, in order to make our design code work, it has to become JavaScript code. In the execution context, all of our TypeScript code is compiled into JavaScript code and is then executed by its target platform - the browser for example. The browser has no clue that this code is compiled - it looks just like the plain JavaScript it knows how to execute. Therefore, TypeScript imposes no runtime overhead on any application.
Free eBook
Directives, simple right? Wrong! On the outside they look simple, but even skilled Angular devs haven’t grasped every concept in this eBook.
- Observables and Async Pipe
- Identity Checking and Performance
- Web Components <ng-template> syntax
- <ng-container> and Observable Composition
- Advanced Rendering Patterns
- Setters and Getters for Styles and Class Bindings
For the browser to receive valid execution code, the TypeScript compiler takes TypeScript features and implements them in whatever JavaScript compile target of our choice - we can go as early as ES3! As we know, there are some TypeScript features that simply do not exist in JavaScript and cannot be implemented, such as type-checking and interfaces. These unsupported features are simply removed from the compiled code - this is known as type erasure. Their removal has no impact on the functionality of your code because these unique features are there only to help TypeScript enhance your developer experience and they don’t overrun or override anything core to the JavaScript language.
TypeScript Architecture: Components
The architecture of TypeScript is neatly organized in different layers.
Language
Core TypeScript Compiler
Sometimes called the TypeScript transpiler, the TypeScript compiler has the core task of managing the low-level mechanics of type-checking our code and converting it into valid JavaScript code. The compiler uses static code analysis to mitigate the occurrence of runtime errors. Typos in our code or passing the wrong type of argument to a function will make the compiler throw compile-time errors to warn us that something is wrong before we even execute the code. This is extremely valuable as, even with the most comprehensive suite of tests, logic errors and edge cases can crash our application at runtime. TypeScript ensures that type definitions that we create within our code are used consistently throughout it.
The compiler itself is made up of different parts that work together fast to make our code predictable and to compile it:
Parser
A quiet complex yet critical component that takes input data, our TypeScript source files, and builds a data structure from it - in this case, an Abstract Syntax Tree. Parsing our code creates a structural representation of the sources that allows us to check that they are following the language grammar - that is, that the sources are built using the correct syntax.
Binder
When we have, for example, a function and a module with the same name, the binder links these named declarations using a Symbol, allowing the type system to make sense of them.
Type Resolver or Type Checker
This component resolves types for each construct, checks semantic operations and generates type diagnostics.
Emitter
Generates output from .ts
and d.ts
files. The output can be either a JavaScript file (.js
), a TypeScript definition file, (d.ts
), or a source map file (.js.map
).
Pre-processor
Resolves and manages references amongst files using import or /// \
.
We will learn in an upcoming section how to setup and configure the TypeScript compiler.
TypeScript Standalone Compiler, tsc
We’ll shortly explore the installation and usage of the standalone TypeScript compiler. Referred to often as tsc
, it is a high-level compiler that takes a TypeScript file, .ts
, and outputs a JavaScript file, .js
.
Language Service
This component layer sits on top of the core TypeScript compiler and provides features that are needed for IDEs and text editors to do their job, such as statement completions, signature help, code formatting and outlining, syntax highlighting and many more. The language service also powers code refactoring such as renaming variables, debugging and incremental compilation.
Tool Integration
TypeScript offers type annotations that allow IDEs and text editors to perform comprehensive static analysis on our code. These annotations allow these tools to make smart suggestions by making our code far more predictable. In return, IDEs and text editors can offer better auto completion and refactoring of TypeScript code.
Setting Up and Using TypeScript
How to Install TypeScript
The easiest way to get TypeScript up and running is by installing its standalone compiler (tsc) globally via a Node.js package manager such as npm or yarn.
npm install -g typescript
or
yarn global add typescript
Once this global installation of the TypeScript compiler is complete, we have access to the tsc
command from our terminal that allow us to compile .ts
files into .js
ones. We can verify the success of our compiler installation by running the following command to check its version:
tsc -v
The TypeScript compiler comes with many options that we’ll be exploring as we move forward. For now, create a folder anywhere in your file system called ts-intro
. We are going to use that folder to store our TypeScript source files and take it for a spin!
Create a TypeScript File
Using a text editor, IDE or terminal - whatever option you prefer - create a file named barista.ts
. Within our file, we are going to create a barista
function that takes name
and outputs an order call using that name
:
// barista.ts
function barista(name) {
console.log('Peppermint Mocha Frappuccino for ' + name);
}
let customer = {
name: 'Todd',
};
barista(customer.name);
We’ve created a valid TypeScript file but how do we run it? Let’s do that next.
Compile TypeScript
With our folder ts-intro
as our current directory, let’s execute the following command in our terminal window:
tsc barista.ts
We get barista.js
being added to our folder ts-intro
- this is the output of the compilation. Open barista.js
and notice that it’s almost exactly the same as barista.ts
:
// barista.js
function barista(name) {
console.log('Peppermint Mocha Frappuccino for ' + name);
}
var customer = {
name: 'Todd',
};
barista(customer.name);
One way to quickly spot what changed through compilation is by running a difference on the content of both files:
OSX / Linux: diff barista.ts barista.js
Windows: FC barista.ts barista.js
The file difference is nothing major. tsc
changed the scoped variable let
to var
. This happened because the default target JavaScript for compilation is ES3 - which doesn’t support let
. We’ll learn soon on how to modify the compiler configuration. We can now run barista.js
through node by executing the following command:
node barista.js
As it is, barista.ts
has no TypeScript on it, hence, there’s not much to compile. Let’s add more TypeScript features to it to see a more dramatic file change.
Let’s drastically modify the code by creating a Barista
class that has a static method that calls the order and uses type annotations to enforce type checking of our variables:
// barista.ts
class Barista {
static callOrder(name: string) {
console.log('Peppermint Mocha Frappuccino for ' + name);
}
}
let customer = {
name: 'Todd',
};
Barista.callOrder(customer.name);
Because callOrder
is static, we do not need to create an instance of the class to be able to use the method. Much like Array.from
, we call the method from the class name itself. Compile the code running tsc barista.ts
again and note how this time we get a quite different barista.js
:
// barista.js
var Barista = /** @class */ (function() {
function Barista() {}
Barista.callOrder = function(name) {
console.log('Peppermint Mocha Frappuccino for ' + name);
};
return Barista;
})();
var customer = {
name: 'Todd',
};
Barista.callOrder(customer.name);
ES3 supports no class
construct, therefore, this TypeScript construct has to be implemented in plain JavaScript. Notice, however, how nice and readable the compiled code is! The compiler created a comment annotation, @class
, in the JavaScript file to denote Barista
as intended to be a class - increasing our code readability.
Configure TypeScript using tsconfig
Much like package.json
is added to give npm
instructions on what packages to install as project dependencies, we can use a tsconfig.json
file to provide instructions on how our TypeScript project should be configured. Adding tsconfig.json
to ts-intro
marks the folder as the root directory of our TypeScript project. In this file, we can specify compiler options to compile our .ts
files as well as root files for our project.
Create a tsconfig.json
file within ts-intro
with the following configuration to tell the compiler to use ES6 as the JavaScript compilation target instead:
{
"compilerOptions": {
"target": "es6"
}
}
From now on, whenever we run the tsc
command, the compiler will check this file first for special instructions and then proceed with compilation based on those instructions. It’s important to know that to make use of tsconfig.json
, we do not specify any file inputs to tsc
. To compile barista.ts
once again under this new configuration simply run the command tsc
in your terminal.
Since we are using ES6, which supports class
, as the JavaScript compilation target, barista.js
doesn’t look very different than barista.ts
! The only difference is that the compiler removed the code related to static type checking. The name
argument doesn’t have any annotations to indicate its type since this feature is not part of ES6.
There’s a much easier way to initialise a TypeScript project and create its tsconfig.json
file. We can use a handy shortcut similar to what’s done to kickstart a Node.js project. Let’s go ahead and delete the tsconfig.json
file that we created and then run the following initialisation command:
tsc --init
The output of running this command is a newly created tsconfig.json
file that is packed with a lot of default options to configure our TypeScript project compiler - most of them are not enabled by default. The configuration options are accompanied by comments that explain what each one configures in our compiler!
{
"compilerOptions": {
/* Basic Options */
"target":
"es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */,
"module":
"commonjs" /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
// "lib": [], /* Specify library files to be included in the compilation: */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
// "outDir": "./", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
/* Source Map Options */
// "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
}
}
The best part of this new tsconfig.json
is definitely how well documented the options are - they are pretty self-explanatory! You don’t have to use all of these options though. For most of my Angular applications that use TypeScript, I use the following configuration:
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"outDir": "dist",
"sourceMap": true,
"experimentalDecorators": true
},
"files": [
"./node_modules/@types/mocha/index.d.ts",
"./node_modules/@types/node/index.d.ts"
],
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}
Note that
es6
andES2015
can be used interchangeably.
I added three additional properties to tsconfig.json
(files
, include
and exclude
) that allow us to specify which files in the containing directory and subdirectories should be compiled:
- When
files
orinclude
properties are specified, the compiler will only process a union of the files listed as the value of these properties. files
takes a list of relative or absolute file paths, whereasinclude
andexclude
take a list of glob-like file patterns.exclude
is used to filter the files listed underinclude
; however, any files that have been explicitly listed underfiles
are always included regardless of them matching any pattern defined underexclude
.
I presented my tsconfig.json
as an example to showcase the file filtering capabilities of the compiler. Running tsc
with it in our ts-intro
folder will give us an error saying that the paths specified in files
are not found. Again, I use this for my Angular projects that makes use of node_modules
- if you wish, give it a try on your projects.
We’ve covered a lot so far. We’ve learned how to create a TypeScript file, how to compile a TypeScript file, and how to configure the compiler to render different compilations of a TypeScript file. There’s much, much more that can be configured through tsconfig.json
but that’s a story for another post!
TypeScript Playground
Whenever you need to perform some quick experimentation with how TypeScript code would compile to JavaScript it’s not necessary to go through all this setup. The TypeScript Team created an online tool that allows us to compile TypeScript code and compare it side by side with its JavaScript output online. This tool is called TypeScript Playground and we can access it at typescriptlang.org/play.
TypeScript Playground allows you to share the code snippets you create there with others.
The Playground also has built-in examples that showcase TypeScript code snippets of different complexities and categories, such as using generics. Use it at your leisure to create deep mapping knowledge between TypeScript and JavaScript easily.
Conclusion
TypeScript brings a lot of benefits to our productivity and developer experience. We’ve seen that integrating it with an existing JavaScript project is easy and carries little to no overhead. TypeScript is not unique to Angular, other powerful frontend frameworks such as React and Vue are starting to be used with TypeScript to allow developer teams to create applications that are reliable, sustainable and scalable. JavaScript and TypeScript are continually evolving but not competing against each other. TypeScript was created to complement and enhance JavaScript - not replace it. The future may see them becoming very similar in features but with TypeScript remaining the statically typed alternative.
With this TypeScript introduction, we’ve just scratched the surface of all the amazing things that we can do with TypeScript. I hope you enjoyed this post!
If you’re interested in taking your TypeScript skills to an ultimate level, I invite you to come and learn TypeScript basic and advanced concepts with me at Ultimate Courses - don’t worry if you are not an Angular developer, the course is designed completely around TypeScript!