Dirk Harriman Banner Image

 

TypeScript Notes

TypeScript is a strongly typed programming language that builds on JavaScript, giving you better tooling at any scale. It is a superset of JavaScript. It adds additional syntax to JavaScript to support a tighter integration with your editor. Catch errors early in your editor. TypeScript code converts to JavaScript, which runs anywhere JavaScript runs: In a browser, on Node.js or Deno and in your apps. It understands JavaScript and uses type inference to give you great tooling without additional code.


Types by Inference

TypeScript knows the JavaScript language and will generate types for you in many cases. For example in creating a variable and assigning it to a particular value, TypeScript will use the value as its type. By understanding how JavaScript works, TypeScript can build a type-system that accepts JavaScript code but has types. This offers a type-system without needing to add extra characters to make types explicit in your code. That's how TypeScript knows that helloWorld is a string in the example below.

let helloWorld = "Hello World";

Defining Types

You can use a wide variety of design patterns in JavaScript. However, some design patterns make it difficult for types to be inferred automatically (for example, patterns that use dynamic programming). To cover these cases, TypeScript supports an extension of the JavaScript language, which offers places for you to tell TypeScript what the types should be.

For example, to create an object with an inferred type which includes name: string and id: number, you can write:

const user = { name: "Hayes", id: 0, };

Interface Declaration

You can explicitly describe this object's shape using an interface declaration:

interface User { name: string; id: number; }

You can then declare that a JavaScript object conforms to the shape of your new interface by using syntax like : TypeName after a variable declaration:

const user: User = { name: "Hayes", id: 0, };

If you provide an object that doesn't match the interface you have provided, TypeScript will warn you:

interface User { name: string; id: number; } const user: User = { username: "Hayes", Type '{ username: string; id: number; }' is not assignable to type 'User'. Object literal may only specify known properties, and 'username' does not exist in type 'User'. id: 0, };

Since JavaScript supports classes and object-oriented programming, so does TypeScript. You can use an interface declaration with classes:

interface User { name: string; id: number; } class UserAccount { name: string; id: number; constructor(name: string, id: number) { this.name = name; this.id = id; } } const user: User = new UserAccount("Murphy", 1);

You can use interfaces to annotate parameters and return values to functions:

function deleteUser(user: User) { // ... } function getAdminUser(): User { //... }

There is already a small set of primitive types available in JavaScript:

which you can use in an interface. TypeScript extends this list with a few more, such as:

You'll see that there are two syntaxes for building types: Interfaces and Types. You should prefer interface. Use type when you need specific features.

Composing Types

With TypeScript, you can create complex types by combining simple ones. There are two popular ways to do so: with unions, and with generics.

Unions

With a union, you can declare that a type could be one of many types. For example, you can describe a boolean type as being either true or false:

type MyBool = true | false;

A popular use-case for union types is to describe the set of string or number literals that a value is allowed to be:

type WindowStates = "open" | "closed" | "minimized"; type LockStates = "locked" | "unlocked"; type PositiveOddNumbersUnderTen = 1 | 3 | 5 | 7 | 9;

Unions provide a way to handle different types too. For example, you may have a function that takes an array or a string:

function getLength(obj: string | string[]) { return obj.length; }

To learn the type of a variable in your code, use typeof:

typeof
Type Predicate
stringtypeof s === "string"
numbertypeof n === "number"
booleantypeof b === "boolean"
undefinedtypeof undefined === "undefined"
functiontypeof f === "function"

For example, you can make a function return different values depending on whether it is passed a string or an array:

function wrapInArray(obj: string | string[]) { if (typeof obj === "string") { return [obj]; } return obj; }

Generics

Generics provide variables to types. A common example is an array. An array without generics could contain anything. An array with generics can describe the values that the array contains.

type StringArray = Array<string>; type NumberArray = Array<number>; type ObjectWithNameArray = Array<{ name: string }>;

You can declare your own types that use generics:

interface Backpack<Type> { add: (obj: Type) => void; get: () => Type; } // This line is a shortcut to tell TypeScript there is a // constant called 'backpack', and to not worry about where it came from. declare const backpack: Backpack<string>; // object is a string, because we declared it above as the variable part of Backpack. const object = backpack.get(); // Since the backpack variable is a string, you can't pass a number to the add function. backpack.add(23); Argument of type 'number' is not assignable to parameter of type 'string'.

Structural Type System

One of TypeScript's core principles is that type checking focuses on the shape that values have. This is sometimes called duck typing or structural typing.

In a structural type system, if two objects have the same shape, they are considered to be of the same type.

interface Point { x: number; y: number; } function logPoint(p: Point) { console.log('${p.x}, ${p.y}'); } // logs "12, 26" const point = { x: 12, y: 26 }; logPoint(point);

The point variable is never declared to be a Point type. However, TypeScript compares the shape of point to the shape of Point in the type-check. They have the same shape, so the code passes.

The shape-matching only requires a subset of the object's fields to match.

const point3 = { x: 12, y: 26, z: 89 }; logPoint(point3); // logs "12, 26" const rect = { x: 33, y: 3, width: 30, height: 80 }; logPoint(rect); // logs "33, 3" const color = { hex: "#187ABF" }; logPoint(color); Argument of type '{ hex: string; }' is not assignable to parameter of type 'Point'. Type '{ hex: string; }' is missing the following properties from type 'Point': x, y

There is no difference between how classes and objects conform to shapes:

class VirtualPoint { x: number; y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } } const newVPoint = new VirtualPoint(13, 56); logPoint(newVPoint); // logs "13, 56"

If the object or class has all the required properties, TypeScript will say they match, regardless of the implementation details.


The TypeScript Compiler

The compiler is run on the command line. If no errors are found, there will be no feedback. If you look in the folder, there should be a new javascript file hello.js

// GREETS THE WORLD. console.log("Hello world!");
tsc hello.ts

In this case, there was very little for TypeScript to transform, so it looks identical to what we wrote. The compiler tries to emit clean readable code that looks like something a person would write. While that's not always so easy, TypeScript indents consistently, is mindful of when our code spans across different lines of code, and tries to keep comments around.

What about if we did introduce a type-checking error? Let's rewrite hello.ts:

function greet(person, date) { console.log(`Hello ${person}, today is ${date}!`); } greet("Brendan");

If we run tsc hello.ts again, notice that we get an error on the command line!

Expected 2 arguments, but got 1.

TypeScript is telling us we forgot to pass an argument to the greet function, and rightfully so. So far we've only written standard JavaScript, and yet type-checking was still able to find problems with our code.

Emitting with Errors

One thing you might not have noticed from the last example was that our hello.js file changed again. If we open that file up then we'll see that the contents still basically look the same as our input file. That might be a bit surprising given the fact that tsc reported an error about our code, but this is based on one of TypeScript's core values: much of the time, you will know better than TypeScript.

To reiterate from earlier, type-checking code limits the sorts of programs you can run, and so there's a tradeoff on what sorts of things a type-checker finds acceptable. Most of the time that's okay, but there are scenarios where those checks get in the way. For example, imagine yourself migrating JavaScript code over to TypeScript and introducing type-checking errors. Eventually you'll get around to cleaning things up for the type-checker, but that original JavaScript code was already working! Why should converting it over to TypeScript stop you from running it?

So TypeScript doesn't get in your way. Of course, over time, you may want to be a bit more defensive against mistakes, and make TypeScript act a bit more strictly. In that case, you can use the noEmitOnError compiler option. Try changing your hello.ts file and running tsc with that flag:

tsc --noEmitOnError hello.ts

You'll notice that hello.js never gets updated.


Explicit Types

In the following code note that person and date are defined as string and Date.

function greet(person: string, date: Date) { console.log(`Hello ${person}, today is ${date.toDateString()}!`); }

If the function is called like the following code:

greet("Jesse", Date());

it will produce an error. This is because the Javascript function Date() returns a string and not a Date object. Sending a parameter produced from calling a Date object constructor, such as

greet("Jesse", new Date());

will work without error.

The Compiled javascript from the preceding typescript code

function greet(person, date) { console.log("Hello ".concat(person, ", today is ").concat(date.toDateString(), "!")); }
Notice two things here:

Downleveling

Note that the TypeScript from the above template, the string was rewritten from:

`Hello ${person}, today is ${date.toDateString()}!`;

to

"Hello ".concat(person, ", today is ").concat(date.toDateString(), "!");

The reason for this, something that is called downleveling, is to rewrite script into a version that the most browsers will interpret without error.

noImplicitAny

Recall that in some places, TypeScript doesn't try to infer types for us and instead falls back to the most lenient type: any. This isn't the worst thing that can happen - after all, falling back to any is just the plain JavaScript experience anyway.

However, using any often defeats the purpose of using TypeScript in the first place. The more typed your program is, the more validation and tooling you'll get, meaning you'll run into fewer bugs as you code. Turning on the noImplicitAny flag will issue an error on any variables whose type is implicitly inferred as any.

strictNullChecks

By default, values like null and undefined are assignable to any other type. This can make writing some code easier, but forgetting to handle null and undefined is the cause of countless bugs in the world - some consider it a billion dollar mistake! The strictNullChecks flag makes handling null and undefined more explicit, and spares us from worrying about whether we forgot to handle null and undefined.