TypeScript, the language, is a strict superset of JavaScript ES6+ that adds a type system to JavaScript. It keeps all of the JavaScript syntax and features.
What TypeScript adds is the syntax to describe types of data, objects, classes, and functions.
According to wikipedia
A data type is a classification of data which tells the compiler or interpreter how the programmer intends to use the data.
Type checking, the process of validating matching types in your programs, removes an entire category of bugs from your program by not allowing you to make them. Essentially, a type system is there to help you ensure correct code without even running your program.
JavaScript | TypeScript |
---|---|
|
|
var a: number = "test";
var b: boolean = 3;
var c: string = false;
example.ts|1 col 5 error| Type 'string' is not assignable to type 'number'. example.ts|2 col 5 error| Type 'number' is not assignable to type 'boolean'. example.ts|3 col 5 error| Type 'boolean' is not assignable to type 'string'.
The TypeScript type system supports gradual typing, in other words, it will let you add types as need. The "any" type allows you to put anything inside. It is an escape hatch from the type system. TypeScript also has syntax for type casting if needed. Ideally, they should be used sparingly.
var a: any = 1;
var b: any = true;
var c: any = "test";
var d: any = null;
var e: any = undefined;
var f: any[] = [1,2,3]
var g: any = ["one","two"]
function sum(x, y) {
return x + y;
}
function sum(x: number, y: number): number {
return x + y;
}
var point1 = {
x: 1,
y: 4,
name: "start"
};
var point1: {x: number, y: number, z?: number, name: string} = {
x: 1,
y: 4,
name: "start"
};
The ? after property names indicates the property is optional
type Point = {x: number, y: number, z?: number, name: string};
var point1: Point = {
x: 1,
y: 4,
name: "start"
};
var point2: Point = {
x: 4,
y: 10,
z: -1,
name: "end"
};
You can treat string constants as types.
type PrimaryColor = "red" | "blue" | "green";
function adjustColor(color: PrimaryColor, value: number) { /* */ }
adjustColor("red", 123) //ok
adjustColor("purple", 255) //Error
You can create named numeric contants with enums
enum Directions {
Up=1,
Down,
Left,
Right
}
function Go(d: Directions): string {
return "You went " + Directions[d] + " aka " + d;
}
Go(Directions.Left); //You went left aka 3
type EightBit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
let coolbit: EightBit = 7; //ok
let badbit: EightBit = 8; //error
Type aliases can be used to alias primitives, which can be useful for documentation
type Dollars = number;
type Name = string;
function BankBalance(person: Name): Dollars {
return 3.50
}
Functions can also be described with type aliases
function sum(x: number, y: number): number {
return x + y;
}
type binary_op = (x: number, y: number) => number;
var sum2: (x: number, y: number) => number = sum;
var sum3: binary_op = sum;
Typescript supports the es6 syntax for classes
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
}
class SoftwareEngineer extends Person {
prog_language: string;
constructor(name: string, prog_language: string) {
super(name)
}
}
var joeTester: Person = new SoftwareEngineer("Joe", "TypeScript");
Typescript simplifies the "this.property = property" pattern with the public keyword. It also supports private and protected attributes on classes, but these are compiler enforced only.
class Person {
constructor(public name: string) {}
}
class SoftwareEngineer extends Person {
private prog_language: string;
constructor(public name: string, private prog_language: string) {
super(name)
this.prog_language = prog_language;
}
}
Abstract classes are base classed which are meant to be inherited from. They cannot be instantiated directly. They specify methods which must be implemented in the subclasses. They are allowed to include implementations of methods to share as well.
abstract class Animal {
abstract makeSound(): void;
move(): void {
console.log("roaming the earth...");
}
}
class Mammal extends Animal {
makeSound(): void {
console.log("A noise!");
}
}
Interfaces can be used to describe objects, classes, and functions. They are similar to abstract classes however, they cannot contain an implementation of a function. Interfaces fill a similar role to type aliases, but they can do and describe a few more things.
//object type
interface Point {
x: number;
y: number;
z?: number;
label: string;
}
var origin: Point = {x: 0, y: 0, z:0, label: "origin"};
//function type
interface binary_op {
(x: number, y: number): number
}
var add_one: binary_op = function(x: number, y: number): number {
return x + y + 1
};
Interfaces can ensure that a class must implement specific methods and attributes.
interface ClockInterface {
currentTime: Date;
setTime(d: Date);
}
class Clock implements ClockInterface {
currentTime: Date;
setTime(d: Date) {
this.currentTime = d;
}
constructor(hour: number, minute: number) {}
}
Interfaces can also be used to describe container objects like arrays and objects
interface PhoneBook {
[phoneNumber: string]: Person
}
class Person {
constructor(public name: string) {}
}
var yellowpages: PhoneBook = {
"1112223333": new Person("John"),
"3334445555": new Person("Jane")
}
interface GuestList {
[index: number]: Person
}
var partylist: GuestList = [new Person("John"), new Person("Jane")]
Variables, classes, and interfaces can enforce immutability on variables and properties. TypeScript also comes with a ReadonlyArray< T > type. Only the compiler forbids reassignment.
const example = true;
interface ImmutablePoint {
readonly x: number;
readonly y: number;
}
class Pet {
constructor(public readonly name: string) {};
}
let hex_letters: ReadonlyArray< string > = ['A','B','C','D','E','F'];
Tuples are a way to describe fixed length arrays with types for each index.
let person: [string, number] = ["John Smith", 30];
person = ["Jane Doe", 30]; //ok
person = [20, "Joe Tester"] // Error
We can use union types to describe pieces of data that can be two or more types of data. The | operator lets us create a union of types.
var password: string | number = "test"
password = 1234 //also ok!
function validatePassword(password: number | string): boolean {
if(typeof password == "number") {
return password > 1000;
}else{
return password.length > 4;
}
}
Once you have the ability to mash types together with Union Types, TypeScript also gives us the ability to pull types apart with type guards.
class BinaryPassword {
data: ArrayBuffer;
constructor(public data: ArrayBuffer) {}
}
type EncryptedPassword = {password: string};
function IsEncryptedPassword(obj: Any): obj is Password {
return obj.password !== undefined
}
type Password = BinaryPassword | EncryptedPassword | number | string;
function validatePassword(password: Password): boolean {
if(password instanceof BinaryPassword) {
return true;
}else if(IsEncryptedPassword(password)) {
return true;
}else if(typeof password == "number") {
return number > 1000;
}else if(typeof password == "string" ){
return string.length > 4;
}else{
throw new TypeError("Not a valid password type");
}
}
These allow you describe an object which is one type AND another type. A common JavaScript pattern called the "mixin" pattern can be described with intersection types.
type Fahrenheit = {fahrenheit: number};
type Celcius = {celcius?: number};
function convertToCelcius(temp: Fahrenheit): Fahrenheit & Celcius {
let result: Fahrenheit & Celcius = temp;
let c_temp = (temp.fahrenheit-32)*(5/9);
result.celcius = c_temp;
return result;
}
Most languages traditional languages use a nominal type system, which means that when they compare types, they look at the name of the type to determine if an expression is well typed.
Typescript has a structural type system which means that it compares the shape of types rather than just the name.
type Person = {name: string, age: number};
type Pet = {name: string, age: number, breed: string};
function HappyBirthday(p: Person) {
p.age++;
console.log(`Happy Birthday ${p.name}!`+
`You are now ${p.age} years old!`);
}
let myDoge: Pet = {name: "Spot", age: 4, breed: "pug"};
HappyBirthday(myDoge);
//Happy Birthday Spot! You are now 5 years old!
Typescript has a config file you can generate for your project called "tsconfig.json". Among other things, this file allows you to ramp up or ramp down strictness in typing. Here is an example of a maximally strict config file.
{
"compilerOptions": {
"module": "commonjs",
"sourceMap": true,
"target": "ES6",
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noFallthroughCasesInSwitch": true,
"strictNullChecks": true
}
}
Typescript can also prevent common errors because it understands how data and types flow through JavaScript.
var test: number;
console.log(test); //error unassigned variable
function goTime(isGoTime: boolean) {
if(isGoTime) {
return isGoTime;
}else{
return !isGoTime;
}
return "asdf"; //error unreachable code
}
switch(test) {
case 1:
console.log("Hello world!"); //error switch case fall-through
case 2:
console.log("Goodnight world!");
break;
default:
console.log("foobar");
}
TypeScript has the ability to create complex types called a discriminated unions. Combining literal types or type guard functions TypeScript can narrow a union type into a specific type to define per type behavior.
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
radius: number;
}
type Shape = Square | Rectangle | Circle;
function area(s: Shape) {
switch (s.kind) {
case "square": return s.size * s.size;
case "rectangle": return s.height * s.width;
case "circle": return Math.PI * s.radius ** 2;
}
}
These functions take a type as a parameter. A is a variable. Interfaces, classes, and type aliases all support generics
function head< A >(xs: A[]): A { return xs[0]; }
function tail< A >(xs: A[]): A[] { return xs.slice(1); }
let one = head< number >([1,2,3]);
let letter = head< string >(["a","b"]);
type Container< A > = { value: A };
class LinkedList< A > {
data: A;
next: LinkedList< A >;
constructor(public value: A, public next: LinkedList< A >) { }
}
It is possible to add constraints to generic types.
interface Lengthwise {
length: number;
}
function logLength< T extends Lengthwise >(arg: T): T {
console.log(arg.length);
return arg;
}
A common pattern in JavaScript is to use indirection when working with objects of a certain kind .
inteface Person {
name: string;
age: number;
}
let person = {name: "Joe Tester", age: 30};
let personProp: keyof Person = "name"; //ok
let personProp2: keyof Person = "test"; //error
function getProperty< T, K extends keyof T >(p: T, prop: keyof K): T[K] {
return p[prop];
}
let age: number = getProperty(person, "age");
The goal of a type system is to ensure that our program is free of bugs. However, to actually get this benefit takes a bit of design. The trick is to push business logic into the types. The goal is to make invalid states inexpressible. If you have an expressive type system, it becomes easier to describe your business logic as types and thereby create code that is provably correct.
sudo npm install -g typescript
installs the command tsc
tsc filename.ts
Upon running tsc on a ts file it will generate a .js file of the same name, which is the translation of the typescript code