TypeScript: JavaScript + Types = Awesome Developer Productivity - Type Annotations
The Mastering the Arcane Art of JavaScript-mancy series are my humble attempt at bringing my love for JavaScript to all other C# developers that haven’t yet discovered how awesome this language and its whole ecosystem are. These articles are excerpts of the super duper awesome JavaScript-Mancy book a compendium of all things JavaScript for C# developers.
Type annotations are TypeScript’s bread and butter and provide yet a new level of meta-programming in JavaScript: type meta-programming. Type annotations give you the ability to create a better developer experience for you and your team by ways of shorter feedback loops, compile time errors and API discoverability.
Type annotations in TypeScript don’t stop at simple primitive types like string
or number
. You can specify the type of arrays:
// An array of strings
let saddleBag: string[] = []
saddleBag.push('20 silvers')
saddleBag.push('pair of socks')
saddleBag.push(666)
// => [ts] Argument of type '666' is not assignable
// to parameter of type 'string'.
and tuples:
// A tuple of numbers
let position: [number, number]
position = [1, 1]
position = [2, 2]
// position = ['orange', 'delight'];
// => [ts] Type '[string, string]' is not
// assignable to type '[number, number]'.
// Type 'string' is not assignable to type 'number'.
functions:
// a predicate function that takes numbers and returns a boolean
let predicate: (...args: number[]) => boolean
predicate = (a, b) => a > b
console.log(`1 greated than 2? ${predicate(1, 2)}`)
// => 1 greated than 2? false
predicate = (text: string) => text.toUpperCase()
// => [ts] Type '(text: string) => string' is not assignable
// to type '(...args: number[]) => boolean'.
// Types of parameters 'text' and 'args' are incompatible.
// Type 'number' is not assignable to type 'string'.
and even objects:
function frost(minion: { hitPoints: number }) {
const damage = 10
console.log(`${minion} is covered in frozy icicles (- ${damage} hp)`)
minion.hitPoints -= damage
}
The {hitPoints: number}
represents and object that has a hitPoints
property of type number
. We can cast a frost spell on a dangerous foe that must comply with the required contract - that of having a hitPoints
property:
const duck = {
toString() {
return 'a duck'
},
hitPoints: 100,
}
frost(duck)
// => a duck is covered in frozy icicles (-10hp)
If the object frozen doesn’t satisfy the requirements, TypeScript will alert us instantly:
const theAir = {
toString() {
return 'air'
},
}
frost(theAir)
// => [ts] Argument of type '{ toString(): string; }'
// is not assignable to parameter of type '{ hitPoints: number; }'.
// Property 'hitPoints' is missing in type '{ toString(): string; }'.
An even better way to annotate objects is through interfaces.
TypeScript Interfaces
Interfaces are reusable and less verbose than straight object type annotations. A Minion
interface could be described as follows:
interface Minion {
hitPoints: string
}
We could use this new interface to update our frost
function:
function frost(minion: Minion) {
const damage = 10
console.log(`${minion} is covered in frozy icicles (-${damage} hp)`)
minion.hitPoints -= damage
}
Looks nicer, doesn’t it? An interesting fact about interfaces is that they are entirely a TypeScript artifact whose only application is within the realm of type annotations and the TypeScript compiler. Because of that, interfaces are not transpiled into JavaScript. If you transpile the code above you’ll be surprised to see that the resulting JavaScript has no mention of Minion
:
function frost(minion) {
var damage = 10
console.log(minion + ' is covered in frozy icicles (-' + damage + ' hp)')
minion.hitPoints -= damage
}
This points to the fact that interfaces are a lightweight approach to add type annotations to your codebase, reap the benefits during development without having any negative impact in the code that runs on the browser.
Let’s test our new frost
function and the Minion
interface with different types of arguments and see how they behave. Bring on the duck
from our previous example!
// const duck = {
// toString(){ return 'duck';},
// hitPoints: 100
// };
frosty(duck)
// => duck is covered in frozy icicles (-10hp)
That seems to work perfectly. If we try with a class that represents a Tower
and has a hitPoints
and a defense
property it seems to work as well:
class Tower {
constructor(public hitPoints = 500, public defense = 100) {}
toString() {
return 'a mighty tower'
}
}
const tower = new Tower()
frosty(tower)
// => a mighty tower is covered in frozy icicles (-10hp)
And so does a simple object literal with the hitPoints
property:
frosty({ hitPoints: 100 })
// => [object Object] is covered in frozy icicles (-10hp)
However if we use an object literal that has another property in addition to hitPoints
the compiler throws an error:
frosty({
hitPoints: 120,
toString() {
return 'a bat'
},
})
// => doesn't compile
// => Argument of type '{ hitPoints: number; toString(): string; }'
// is not assignable to parameter of type 'Minion'.
// Object literal may only specify known properties,
// and 'toString' does not exist in type 'Minion'.
The error message seems to be very helpful. It says that with object literals I may only specify known properties and that toString
doesn’t exist in Minion
. So what happens if I store the object literal in a variable aBat
?
let aBat = {
hitPoints: 120,
toString() {
return 'a bat'
},
}
frosty(aBat)
// => a bat is covered in frozy icicles (-10hp)
It works! Interesting, from these experiments it looks like TypeScript will consider a Minion
to be any object that satisfies the contract specified by the interface, that is, to have a hitPoints
property of type number
. However, it looks like when you use an object literal TypeScript has a somewhat more strict set of rules and it expects an argument that exactly matches the Minion
interface. So what is a Minion
exactly? When TypeScript encounters an arbitrary object, How does it determine whether it is a Minion
or not? It follows the rules of structural typing.
Structural Typing
Structural typing is a type system where type compatibility and equivalence are determined by the structure of the types being compared, that is, their properties.
For instance, following structural typing all of the types below are equivalent because they have the same structure (the same properties):
// an interface
interface Wizard {
hitPoints: number
toString(): string
castSpell(spell: any, targets: any[])
}
// an object literal
const bard = {
hitPoints: 120,
toString() {
return 'a bard'
},
castSpell(spell: any, ...targets: any[]) {
console.log(`${this} cast ${spell} on ${targets}`)
spell.cast(targets)
},
}
// a class
class MagicCreature {
constructor(public hitPoints: number) {}
toString() {
return 'magic creature'
}
castSpell(spell: any, ...targets: any[]) {
console.log(`${this} cast ${spell} on ${targets}`)
spell.cast(targets)
}
}
Which you can verify using this snippet of code:
let wizard: Wizard = bard
let antotherWizard: Wizard = new MagicCreature(120)
In contrast, languages like C# or Java have what we call a nominal type system. In this type system, type equivalence is based on the names of types and explicit declarations, where a MagicCreature
is a Wizard
, if and only if, the class implements the interface explicitly.
Structural typing is awesome for JavaScript developers because it behaves very much like duck typing that is such a core feature to JavaScript object-oriented programming model. It is still great for C#/Java developers as well because they can enjoy C#/Java features like interfaces, classes and compile-time feedback but with a higher degree of freedom and flexibility.
There’s still one thing that doesn’t fit the structural typing rule we just described and is the following example from the previous section where we used an object literal:
frosty({
hitPoints: 120,
toString() {
return 'a bat'
},
})
// => doesn't compile
// => Argument of type '{ hitPoints: number; toString(): string; }'
// is not assignable to parameter of type 'Minion'.
// Object literal may only specify known properties,
// and 'toString' does not exist in type 'Minion'.
Why does that happen? In order to prevent user errors.
The TypeScript compiler designers considered that using object literals like this can be prone to errors and mistakes (like typos, imagine writing hitPoitns
instead of hitPoints
). That is why when using object literals in this fashion the TypeScript compiler will be extra diligent and perform excess property checking. Under this special mode TypeScript will be inclined to be extra careful and will flag any additional property that the function frosty
doesn’t expect, in order to warn you of possible mistakes.
In this scenario, you can quickly tell the TypeScript compiler that there’s no problem by explicitly casting the object literal to the desired type or storing it in a variable as we saw earlier:
frosty({
hitPoints: 120,
toString() {
return 'a bat'
},
} as Minion)
// => a bat is covered in frozy icicles (-10hp)
Notice the as Minion
? That’s a way we can tell TypeScript that the object literal is of type Minion
. This is another way:
frosty(<Minion>{
hitPoints: 120,
toString() {
return 'a bat'
},
})
// => a bat is covered in frozy icicles (-10hp)
TypeScript Helps You With Type Annotations
Another interesting facet of TypeScript are its type inference capabilities. Writing type annotations not only results in more verbose code but it’s also additional work that you need to do. In order to minimize the amount of work that you need to put in to annotate your code, TypeScript will do its best to infer the types used from the code itself. For instance:
const aNumber = 1
const anotherNumber = 2 * aNumber
// aNumber: number
// anotherNumber:number
In this code sample we haven’t specified any types. Regardless, TypeScript knows without the shadow of a doubt that the aNumber
variable is of type number
, and by evaluating anotherNumber
it knows that it’s also of type number
. Likewise we can write the following:
const double = (n: number) => 2 * n
// double: (n:number) => number
And TypeScript will know that the function double
returns a number.
From Interfaces to Classes
So far we’ve seen how you can use type annotations in the form of primitive types, arrays, object literals and interfaces. All of these are TypeScript specific artifacs that disappear when you transpile your TypeScript code to JavaScript. We’ve also seen how TypeScript attempts to infer types from your code so that you don’t need to expend unnecessary time annotating your code.
Then we have classes, which are a ES2015/TypeScript feature that we can use to describe a domain model entity in structure and behavior with a specific implementation, and which also serve as a type annotation. In previous sections we defined an interface Minion
that represented a thing with a hitPoints
property. We can do the same with a class:
class ClassyMinion {
constructor(public hitPoints: number) {}
}
And create a new classyFrost
function to use this class as the argument type:
function classyFrost(minion: ClassyMinion) {
const damage = 10
console.log(`${minion} is covered in frozy icicles (-${damage} hp)`)
minion.hitPoints -= damage
}
We can use this function with our new ClassyMinion
class and even with the previous aBat
and bard
variables because following the rules of structural typing all of these types are equivalent:
classyFrosty(new ClassyMinion())
// => a classy minion is covered in frozy icicles (-10hp)
classyFrosty(aBat)
// => a bat is covered in frozy icicles (-10hp)
classyFrosty(bard)
// => a bard is covered in frozy icicles (-10hp)
Oftentimes, what we would do would be to have the class
implement the desired interface
. For instance:
class ClassyMinion implements Minion {
constructor(public hitPoints: number) {}
}
This wouldn’t make a change in how this class is seen from a structural typing point of view but adding the implements Minion
helps TypeScript tell us whether we have implemented a interface correctly or if we’re missing some properties or methods. This may not sound like much in a class with one single property but it’s increasingly helpful as our classes become more meaty.
In general, the difference between using a class
and using an interface
is that the class will result in a real JavaScript class when transpiled to JavaScript (although it could be a constructor/prototype pair depending on the JavaScript version your are targeting).
For instance, the class above will result in the following JavaScript in our current setup:
var ClassyMinion = (function() {
function ClassyMinion(hitPoints) {
if (hitPoints === void 0) {
hitPoints = 100
}
this.hitPoints = hitPoints
}
ClassyMinion.prototype.toString = function() {
return 'a classy minion'
}
return ClassyMinion
})()
This makes sense because, unlike an interface
which is a made up artifact used only in the world of TypeScript type annotations, a class
is necessary to run your program.
When do you use interfaces and when do you use classes then? Let’s review what both of these constructs do and how they behave:
- Interface: Describes shape and behavior. It’s removed during transpilation process.
- Class: Describes shape and behavior. Provides a specific implementation. It’s transpiled into JavaScript
So both interfaces and class describe the shape and behavior of a type. Additionally, classes also provide a concrete implementation.
In the world of C# or Java, following the dependency inversion principle we’d advise to prefer using interfaces over classes when describing types. That would afford us a lot of flexibility and extensibility within our programs because we would achieve a loosely coupled system where concrete types don’t know about each other. We then would be in a position to inject diverse concrete types that would fulfill the contract defined by the interfaces. This is a must in statically typed languages like C# or Java because they use a nominal type system. But what about TypeScript?
As we mentioned earlier, TypeScript uses a structural type system where types are equivalent when they have the same structure, that is, the same members. In light of that, you could say that it doesn’t really matter if we use interfaces or clases to denote types. If interfaces, classes or object literals share the same structure, they’ll be equally treated, so why would we need to use interfaces in TypeScript? Here are some guidelines that you can follow when you consider using interfaces vs classes:
- The single responsibility is a great rule of thumb to decrease the complexity of your programs. Applying the single responsibility to the interface vs class dilemma we can arrive to use interfaces for types and classes for implementations. Interfaces provide a very concise way to represent the shape of a type, whilst classes intermingle both the shape and the implementation which can make it hard to ascertain what the shape of a type is by just looking at a class.
interfaces
give you more flexibility than classes. Because a class contains a specific implementation, it is, by its very nature, more rigid than an interface. Using interfaces we can capture finely grained details or bits of behavior that are common between classes.interfaces
are a lightweight way to provide type information to data that may be foreign to your application like data coming from web services- For types with no behavior attached, types that are merely data, you can use a class directly. Using an interface in this case will often be overkill and unnecessary. Using a class will ease object creation via constructor.
So, in general, the same guidelines that we follow regarding interfaces in statically typed languages like C# and Java also apply to TypeScript. Prefer to use interfaces to describe types and use classes for specific implementations. If the type is just data with no behavior you may consider using a class on its own.
Advanced Type Annotations
In addition to what we’ve seeing thus far TypeScript provides more mechanisms to express more complex types in your programs. The idea is that, whichever JavaScript construct or pattern you use, you should be able to express its type via type annotations and provide helpful type information for you and other developers within your team.
Some examples of these advanced type annotations are:
- Generics
- Intersection and Union Types
- Type Guards
- Nullable Types
- Type Aliases
- String-literal Types
Let’s take a look at each of them, why they are needed and how to use them.
Generics
Generics is a common technique used in statically typed programming languages like C# and Java to generalize the application of a data structure or algorithm to more than one type.
For instance, instead of having a separate Array
implementation for each different type: NumberArray
, StringArray
, ObjectArray
, etc:
interface NumberArray {
push(n: number)
pop(): number
[index: number]: number
// etc
}
interface StringArray {
push(s: string)
pop(): string
[index: number]: string
// etc
}
// etc...
We use generics to describe an Array
of an arbitrary type T
:
// note that `Array<T>` is already a built-in type in TypeScript
interface Array<T> {
push(s: T)
pop(): T
[index: number]: T
// etc
}
We can now reuse this single type definition by selecting a type for T
:
let numbers: Array<number>
let characters: Array<string>
// and so on...
And just like we used generics with interfaces, we can use them with classes:
class Cell<T> {
private prisoner: T
inprison(prisoner: T) {
this.prisoner = item
}
free(): T {
const prisoner = this.prisoner
this.prisoner = undefined
return prisoner
}
}
Finally, you can constrain the type T
to only a subset of types. For instance, let’s say that a particular function only makes sense within the context of Minion
. You can write:
interface ConstrainedCell<T extends Minion> {
inprison(prisoner: T)
free(): T
}
And now this will be a perfectly usable box:
let box: ConstrainedCell<MagicCreature>
But this won’t because the type T
doesn’t match the Minion
interface:
let box: ConstrainedCell<{ name: string }>
// => [ts] Type '{ name: string; }' does not satisfy the constraint 'Minion'.
// Property 'hitPoints' is missing in type '{ name: string; }'.
Intersection and Union Types
We’ve seen primitive types, interfaces, classes, generics, a lot of different ways to provide typing information but flexible as these may be, there’s still a use case which they have a hard time covering: Mixins.
When using mixins the resulting object is a mix of other different objects. The type of this resulting object is not a known type in its own right but a combination of existing types.
For instance, let’s go back to the Wizard example that we had earlier:
function Wizard(element, mana, name, hp) {
let wizard = {
element,
mana,
name,
hp,
}
// now we use object spread
return {
...wizard,
...canBeIdentifiedByName,
...canCastSpells,
}
}
We can decompose this into separate elements:
interface WizardProps {
element: string
mana: number
name: string
hp: number
}
interface NameMixin {
toString(): string
}
interface SpellMixin {
castsSpell(spell: Spell, target: Minion)
}
How can we define the resulting Wizard
type that is the combination of WizardProps
, NameMixin
and SpellMixin
? We use intersection types. An intersection type for a Wizard would be represented by the following expression:
WizardProps & NameMixin & SpellMixin
And we could use it as a return type of our factory function:
let canBeIdentifiedByName: NameMixin = {
toString() {
return this.name
},
}
let canCastSpells: SpellMixin = {
castsSpell(spell: Spell, target: Minion) {
// cast spell
},
}
function WizardIntersection(
element: string,
mana: number,
name: string,
hp: number
): WizardProps & NameMixin & SpellMixin {
let wizard: WizardProps = {
element,
mana,
name,
hp,
}
// now we use object spread
return {
...wizard,
...canBeIdentifiedByNameMixin,
...canCastSpellsMixin,
}
}
const merlin = WizardIntersection('spirit', 200, 'Merlin', 200)
// merlin.steal(conan);
// => [ts] Property 'steal' does not exist
// on type 'WizardProps & NameMixin & SpellMixin'.
In the same way that we have a intersection types that result in a type that is a combination of other types we also have the ability to make a type that can be any of a series of types, that is, either string
or number
or other type. We call these types union types. They are often used when you have overloaded functions or methods that may take a parameter with varying types.
Take a look at the following function that raises an skeleton army:
function raiseSkeleton(numberOrCreature) {
if (typeof numberOrCreature === 'number') {
raiseSkeletonsInNumber(numberOrCreature)
} else if (typeof numberOrCreature === 'string') {
raiseSkeletonCreature(numberOrCreature)
} else {
console.log('raise a skeleton')
}
function raiseSkeletonsInNumber(n) {
console.log('raise ' + n + ' skeletons')
}
function raiseSkeletonCreature(creature) {
console.log('raise a skeleton ' + creature)
}
}
Depending on the type of numberOrCreature
the function above can raise skeletons or skeletal creatures:
raiseSkeleton(22)
// => raise 22 skeletons
raiseSkeleton('dragon')
// => raise a skeleton dragon
We can add some TypeScript goodness to the raiseSkeletonTS
function using union types:
function raiseSkeletonTS(numberOrCreature: number | string) {
if (typeof numberOrCreature === 'number') {
raiseSkeletonsInNumber(numberOrCreature)
} else if (typeof numberOrCreature === 'string') {
raiseSkeletonCreature(numberOrCreature)
} else {
console.log('raise a skeleton')
}
function raiseSkeletonsInNumber(n: number) {
console.log('raise ' + n + ' skeletons')
}
function raiseSkeletonCreature(creature: string) {
console.log('raise a skeleton ' + creature)
}
}
The number | string
is a union type that allows numberOrCreature
to be of type number
or string
. If we by mistake use something else, TypeScript has our backs:
raiseSkeletonTS(['kowabunga'])
// => [ts] Argument of type 'string[]' is not assignable
// to parameter of type 'string | number'.
// Type 'string[]' is not assignable to type 'number'.
Type Guards
Union types raise a special case inside the body of a function. If numberOrCreature
can be a number or a string, how does TypeScript now which methods are supported? Number methods differ greatly from String methods, so what is allowed?
When TypeScript encounters a union type as in the function above, by default, you’ll only be allowed to used methods and properties that are available in all the types included. It is only when you do a explicit conversion or include a type guard that TypeScript will be able to determine the type in use and be able to assist you. Fortunately, TypeScript will recognize type guards that are common JavaScript patterns, like the typeof
that we used in the previous example. After performing a type guard if (typeof numberOrCreature === "number")
TypeScript will know with certainty that whatever piece of code you execute inside that if block the numberOrCreature
will be of type number
.
Type Aliases
Another helpful mechanism that works great with Intersection and Union Types are Type Aliases. Type Aliases allow you to provide arbitrary names (aliases) to refer to other types. Tired to writing this intersection type?
;WizardProps & NameMixin & SpellMixin
You can create an alias Wizard
and use that instead:
type Wizard = WizardProps & NameMixin & SpellMixin
And improve the Wizard factory from previous examples:
function WizardAlias(
element: string,
mana: number,
name: string,
hp: number
): Wizard {
let wizard: WizardProps = {
element,
mana,
name,
hp,
}
// now we use object spread
return {
...wizard,
...canBeIdentifiedByNameMixin,
...canCastSpellsMixin,
}
}
More Type Annotations!
Although I’ve tried to be quite comprehensive in covering TypeScript within this series, there’s plenty more features and interesting things to discover! If you are interested into learning more about all the cool stuff that you can do with TypeScript type annotations then let me insist once more in the TypeScript handbook and at the release notes.
Concluding
And that is TypeScript! Let’s make a quick recap of it so you get a quick reminder that’ll help you remember all the TypeScript awesomeness you’ve just learned.
TypeScript is a superset of JavaScript that includes a lot of ESnext features and type annotations. By far, the defining feature of TypeScript are its use of types. Type annotations allow you to provide additional metadata about your code that can be used by the TypeScript compiler to provide a better developer experience for you and your team at the expense of code verbosity.
TypeScript is a superset of ES2015 and expands on its features with a lot of ESnext improvements and TypeScript specific features. We saw several ESnext features like class members and the new Objects spread and rest operators. We also discovered how TypeScript enhances classes with parameter properties and property accessors, and brings a new Enum type that allows you to write more intentional code.
Type Annotations are TypeScript’s bread and butter. TypeScript extends JavaScript with new syntax and semantics that allow you to provide rich information about your application types. In addition to being able to express primitive types, TypeScript introduces interfaces, generics, intersection and union types, aliases, type guards, etc… All of these mechanisms allow you to do a new type of meta-programming that lets you improve your development experience via type annotations. Still adding type annotations can be a little daunting and a lot of work, in order to minimize this, TypeScript attempts to infer as much typing as it can from your code.
In the spirit of JavaScript and duck-typing, TypeScript has a structural typing system. This means that types will be equivalent if they share the same structure, that is, if they have the same properties. This is opposed to nominal typing systems like the ones used within C# or Java where type equivalence is determined by explicitly implementing types. Structural typing is great because it gives you a lot of flexibility and, at the same time, great compile-time error detection and improved tooling.
In the front-end development world we’re seeing an increased adoption of TypeScript, particularly, as it has become the core language for development in Angular. Moreoever it is also available in most of the common front-end frameworks, IDEs, tex-editors and front-end build tools. It is also well supported in third-party libraries through type definitions and the DefinitelyTyped project, and installing type definitions for a library is as easy as doing an npm install
.
From a personal perspective, one of the things I enjoyed the most about JavaScript coming from the world of C# was its terseness and the lack of ceremony and unnecessary artifacts. All of the sudden, I didn’t need to write PurchaseOrder purchaseOrder
or Employee employee
any more, an employee was an employee
, period. I didn’t need to write a seemingly infinite amount of boilerplate code to make my application flexible and extensible, or fight with the language to bend it to my will, things just worked. As I saw the release of TypeScript I worried about JavaScript losing its soul and becoming a language as rigid as C# or Java. After experiencing TypeScript developing Angular applications, its optional typing, the great developer experience and, above all, the fact that it has structural typing I am hopeful. It’ll be interesting to follow its development in the upcoming months and years. It may well end with all of us writing TypeScript.
Get the Book!
If you enjoyed this article take a look at the JavaScript-mancy OOP: Mastering the Arcane Art of Summoning Objects, a compendium of OOP techniques available in JavaScript, ES2015, ESNext and TypeScript.
{% img center blend /images/javascriptmancy-oop-book-cover.jpeg 300 “JavaScript-mancy OOP: Mastering the Arcane Art of Summoning Objects” “JavaScript-mancy OOP: Mastering the Arcane Art of Summoning Objects Book Cover” %}
Take care and be kind!
Written by Jaime González García , dad, husband, software engineer, ux designer, amateur pixel artist, tinkerer and master of the arcane arts. You can also find him on Twitter jabbering about random stuff.