From JavaScript to TypeScript Pt. I: Types & Variables
Get started with TypeScript and learn about types and function annotations.
JavaScript is the language on fire. Build an app for any platform you want including website, server, mobile, and desktop.
Object-oriented programming (OOP) has been one of the most influential paradigms in the development of modern programming practices. If you've ever thought in terms of classes or inheritance, you prove the point: Those ideas have their origins in classical OOP.
TypeScript is a language that adds object-oriented syntactical sugar to JavaScript
Object-oriented programming has been so influential that many can't imagine doing things any other way. TypeScript's very existence is testament to this fact: It provides classically object-oriented syntax for a language that supports almost no classical object-oriented features.
Make no mistake: TypeScript does not add true classes, interfaces, or generics. Just some sugar to make it easier if you think that way. Regardless, it's a good introduction to the concepts of OOP for people whose background is mostly in JavaScript.
This series has two goals:
- Cover the essentials of TypeScript, for JavaScripters with an OOP background; and
- Cover the essentials of OOP, in the context of TypeScript, for JavaScripters with no formal exposure to it.
Those in the first group should find it easy to map the concepts we discuss to the syntax we'll see. Those in the latter group should find it easy to focus on the concepts without getting distracted by substantially different syntax.
We'll ease into it, and start in familiar territory. After reading this article, you'll be able to:
- Define type, as it relates to the JavaScript environment;
- List TypeScript's built-in types, and identify where they differ from JavaScript's;
- Declare variables with the proper types and access modifiers; and
- Annotate functions with the proper argument and return types.
Setting Up
To get started, you'll need to install TypeScript (TS). In this article, we'll be using Node.js, v5.11.0, and NPM, v3.8.6. You can also get TS bundled with Visual Studio 2015, if you prefer.
To install TypeScript, run:
npm install --global typescript
This installs a command-line tool,
tsc, which you use to transpile TS to JavaScript.
Create a new directory somewhere: We'll save and compile our TS examples here.
# This creates a new directory in your home folder. # Feel free to do this somewhere else. cd && mkdir ts_examples
While simple, I've collected all of the examples in a GitHub repo. If you don't want to type, run:
git clone git checkout Part_1-Types
. . . And you're good to go.
Types
The word type means different things in different contexts. In general, types are a way of classifying data structures based on the kind of information they can contain, and what you can do with it.
Types in JavaScript
In JavaScript, clause 6 of the ECMAScript specification defines the following types:
- Undefined;
- Null;
- Boolean;
- String;
- Symbol;
- Number; and
- Object.
To say a variable is of a certain type is to say two things:
- That it supports all the operations of that type; and
- That its internal representation is distinct from that of the other types.
At the level of applications programming, we're only concerned with the first point.
"use strict"; const name = "Peleke"; // name has type string const nothing = null; // nothing has type null -- kind of* const nemo = undefined; // nemo has type undefined const obj = { }; // obj has type object // * Brendan Eich made a mistake in the 10-day whirlwind of putting together JavaScript. // typeof null returns "object", but it's not an object, and this is a bug. // Under the hood, null /is/ its own primitive type -- Null. You just can't tell. // For more:
Of course, if you've spent more than a few minutes writing JavaScript, you'll know that this is mostly trivia. We don't annotate types in JavaScript, and we can assign values of different types to the same variables through the lifetime of our program.
As far as we're concerned, if an object has the properties and methods we need it to, we don't care what the underlying type is. If it behaves properly, it's good enough. The rest is an implementation detail.
This is sometimes called duck-typing: If it walks like a duck, and it quacks like a duck, it's probably okay if we
eat it treat it like a duck.
The Purpose of Types
Languages like Java and Haskell are statically typed. That means that, before they run your program, they'll check to make sure that you get strings where you expect strings, numbers where you expect numbers, and mongeese where you expect mongeese. If you pass a mongoose to a function that expects a string, it'll complain, and you'll have to fix it before your program will compile.
The point of all that trouble is to let you know when you've written code that might break before you run it. After all, if you write a function that expects a string, and you give it a mongoose, chances are good it's not going to know what to do with it.
private boolean screamName(String name) { return name.toUpperCase(); } // Mongeese don't like to be capitalized. // Fortunately, the compiler prevents this from building. tameMongoose(new Mongoose());
In JavaScript, we'd just go ahead and pass the mongoose along and cross our fingers. If it's got a
toUpperCase, it works. If not, our program dies. Devil's gambit.
"use strict"; const sweet_mongoose = { }; const savage_mongoose = {}; sweet_mongoose.name = "sweetheart"; savage_mongoose.name = "savage"; sweet_mongoose.toUpperCase = function () { return this.name.toUpperCase(); } function screamName (name) { return name.toUpperCase(); } screamName(sweet_mongoose); // What a SWEETHEART. screamName(savage_mongoose); // Kills program with a TypeError. Cold-blooded.
The idea behind all this is that a value's type can be conceived of as a sort of interface. In other words, a object's type reveals what you can and cannot do with it. Attempting to do something with an object that doesn't make sense for its type -- like capitalizing a number -- is a programmer error that a type-aware compiler can catch ahead of time.
Therein lies one of the benefits: We trade runtime errors for compile-time warnings. By contrast, dynamic languages eschew compile-time safety checks in favor of greater flexibility and programmer productivity.
Obviously, neither system is perfect:
- In our Java function, what if the mongoose did have a
toUpperCasemethod that behaved as desired? Then we'd have to do some finaggling just to get our program to compile.
- In Javascript, what if we get an object with the function we expect, but different behavior? An Observable and an RSS feed have very different
subscribemethods. We'll obviously notice an error, but there's a substantial chance the program would execute -- just not correctly.
That said:
- Types are a form of self-documenting code.
- Certain type errors are easy to catch at compile time, but nonobvious at runtime.
- Types help with the design of function compositions and complex reactive streams.
Do we need static typing? Obviously not. JavaScript doesn't have it, and the Internet works fine.
Do we benefit from it? Maybe. It's powerful in languages like Java. It's exceedingly powerful in certain statically-typed functional languages, like ML, Haskell, and Scala.
TypeScript takes the stance that we unequivocally do benefit from static typing, allowing you to declare your variables with any of a handful of built-in types.
TypeScript's Basic Types
TypeScript exposes nine types for us to work with. Whether you use them or not is up to you: Type annotations are always optional in TypeScript.
- Boolean;
- Number;
- String;
- Array;
- Tuple;
- Enum;
- Any;
- Void;
- Function.
We'll start with the first eight, and circle back to the last one a bit later.
Boolean
In TypeScript, you declare variables as in JavaScript, with a colon and type after the variable name. Booleans behave as usual.
Type Keyword :
boolean
"use strict"; const lie : boolean = false, truth : boolean = true;
Save that in a file called
boolean.ts, then run
tsc boolean.ts. It'll create a file called
boolean.js, Open it up to see some sweet, sweet vanilla JavaScript:
"use strict"; var lie = false, truth = true;
Number
TypeScript's Number type is synonymous with JavaScript's Number type. If youre curious, JavaScript numbers are an IEEE 64-bit binary format double precision floating point values. Or, you know, just numbers.
Type Keyword :
number
"use strict"; const pi : number = 3.14159;
String
As with Number, TypeScript's String is synonymous with JavaScript's underlying String type.
Type Keyword :
string
"use strict"; const tree_of_knowledge : string = "Yggdrasil";
Array
TypeScript treats arrays as their own type, and requires that you declare the type of what's inside of them, as well.
Type Keywords
- Array<[ Item Type]>
[]
There are two syntaxes for arrays.
"use strict"; // The item type, T, followed by brackets means, "an array whose items are of type T." const divine_lovers : string[] = ["Zeus", "Aphrodite"]; // Writing Array<[Item Type]> means the same thing. const digits : Array<number> = [143219876, 112347890]; // But this doesn't work. Hm. . . const only_strings : string[] = []; only_strings.push("This Works!") only_strings.push(42); // This doesn't.
Well. That's annoying.
We can't put
42 into
only_strings because we declared that it can only contain items of type
string. This is an example of TypeScript's compile-time complaints in action.
We'll see how to fix this shortly. But first -- tuples!
Tuples
Tuples are another collection data type. They're useful for collecting a known number of items into an array-like structure. You can declare each item to be of a specific type.
Type Keyword : [(Item Type), . . . , (Item Type)]
"use strict"; // [Date, Month, Year] :: Triplet of numbers let date_triplet : [number, number, number]; date_triplet = [31, 6, 2016]; // [Name, Age] let athena : [string, number]; athena = ['Athena', 9386]; // You retrieve items with indexes, as with arrays. // TS remembers the type for you. var name : string = athena[0]; const age : number = athena[1]; // But: name = athena[1]; // No dice. We're writing TypeScript now.
So . . . What are tuples useful for?
Good question. JavaScript is the only language I write without native tuples, but I rarely use them elsewhere. Occasionally, I use them to group related data that isn't liable to change:
"use strict"; //The Big Three of Powerlifting let big_three : [string, string, string]; big_three = ['Squat', 'Deadlift', 'Bench Press']
. . . But in general, I use a Map or Set instead.
Enum
Enums allow you to associate names with intever values. The name comes from the fact that they're enumerable.
Creating a enum effectively creates a new type keyword, which you use like any other type.
"use strict"; // This is an example from the TypeScript docs. // enum Color { Red, Green, Blue }; const red : Color = Color.Red; // Enums are like associative arrays. Each enum constant is associated with an index, starting at 0. console.log(Color[0]); // 'Red' // You can start from any number instead of zero, if you want. // Note that indexing a non-existent enum constant returns undefined, // but doesn't throw an error. enum RomanceLanguages { Spanish = 1, French, Italian, Romanian, Portuguese }; console.log(RomanceLanguages[4]); // 'Romanian' console.log(RomanceLanguages[0]); // undefined
Symbols provide an interesting alternative to enums.
Any
Any is more or less what it sounds like: A type that accepts any value. In some sense, it's a way to opt out of type checking. It also allows us to declare mixed collections.
Type Keyword :
any
"use strict"; let mystery : any = 4; // number mystery = "four"; // string -- no error const not_only_strings : any[] = []; not_only_strings.push("This Works!") not_only_strings.push(42); // This does too.
Void
Finally, we have Void. This is the typeclass associated with the values
undefined or
null. Unlike the other types, you won't use this to declare variables.
It's not that you can't. Just that, if you do, you can only set that varible to
null or
undefined.
"use strict"; let the_void : void = undefined; the_void = null; the_void = "nothing"; // Error.
The main reason
void exists is to mark functions without return statements.
Speaking of which, it's about time we talk about functions.
Return Types
TypeScript allows you to mark the type of the value that a function returns.
Just as a variable's type indicates what sort of data the variable contains, a function's return type indicates what sort of data it returns.
All the types we've seen so far are valid return types. In particuar, you'll use
void almost exclusively as the return type of a function with no
return statement.
You denote a return type the same way you denote a variable type: With a colon and a type keyword, this time after the arguments list.
"use strict"; function capitalizeName (name : string) : string { return name.toUpperCase(); } // Shoutout to @achm4m0 (Alberto) for notifying me of an error here! console.log(capitalizeName('geronimo')); // 'GERONIMO' // But: console.log(capitalizeName(42)); // Error; 42 isn't a string. // TypeScript checks at compile-time if you're returning the right type. function even_broken (num : number) : boolean { return (num % 2); // WRONG. This will cause a compile-time error. } function even (num : number) : boolean { return (num % 2 == 0); // Much better; this works. }
Note that you can also mark argument types as you would that of normal variables.
Notifications like the one we get for
even_broken are pretty helpful during development.
The Last Type: Functions
One of JavaScript's greatest strengths is that it treats functions as first class citizens. This means we can pass them as arguments and return them from functions ike any other value. How does TypeScript handle that?
Simple. Function name; parenthetical list of typed arguments; fat arrow; return type.
"use strict"; let multiply : (first : number, second : number) => number; multiply = function (first, second) { return first * second; }
This example illustrates that you can annotate your functions types before defining them.
The same pattern applies if you're returning a function. In that case, though, you wrap the type of the function you return -- that is, everything after the fat arrow -- in parentheses.
"use strict"; let multiplyFirst : ( first : number) => ((second : number) => number); multiplyFirst = function (first) { return function (num) { return first * num; } } console.log(multiply(5, 2)); // '10' console.log(multiplyFirst(9)(2)); // '18'
Newcomers to static typing often find variable types annoying. Function types are where they start to get nauseated.
This is actually the typing feature I use most in TypeScript. Somewhat ironically, that's because of my background in functional programming, not OOP, and that remains the context in which I find it most useful.
Annotating functions that return
string or
number is easy, and beneficial largely for the purpose of self-documentation.
On the other hand, writing function type annotations is an integral, active part of my design process when I work with Rx streams and complex [function compositions](.
Conclusion
By now, you should feel like you've started getting your hands dirty with TypeScript. You've seen:
- How to transpile TS files on the command line;
- How types in TS compare to JavaScript's;
- All of the types TS has to offer; and
- How to annotate function types.
You should also feel pretty confident that you can article why we might benefit from all this in the first place.
Next time, we'll stray a little farther from familiar territory, and explore classes; interfaces; enums; and how to get the most out of them by adapting classical obect-oriented design to our more dynamic environment.
Until then, feel free to drop questions in the comments below, or shoot them to me on Twitter (@PelekeS).