Dirk Harriman Banner Image

 

TypeScript Types


Primatives: string, number & boolean

JavaScript has three very commonly used primitives: string, number, and boolean. Each has a corresponding type in TypeScript. As you might expect, these are the same names you'd see if you used the JavaScript typeof operator on a value of those types.

The type names String, Number, and Boolean (starting with capital letters) are legal, but refer to some special built-in types that will very rarely appear in your code. Always use string, number, or boolean for types.


Arrays

To specify the type of an array like [1, 2, 3], you can use the syntax number[]; this syntax works for any type (e.g. string[] is an array of strings, and so on). You may also see this written as Array<number>, which means the same thing.

that [number] is a different thing; refer to the section on Tuples.


Any

TypeScript also has a special type, any, that you can use whenever you don't want a particular value to cause typechecking errors.

When a value is of type any, you can access any properties of it (which will in turn be of type any), call it like a function, assign it to (or from) a value of any type, or pretty much anything else that's syntactically legal:

None of the following lines of code will throw compiler errors. Using any disables all further type checking, and it is assumed you know the environment better than TypeScript.

let obj: any = { x: 0 }; obj.foo(); obj(); obj.bar = 100; obj = "hello"; const n: number = obj;

The any type is useful when you don't want to write out a long type just to convince TypeScript that a particular line of code is okay.

noImplicitAny

When you don't specify a type, and TypeScript can't infer it from context, the compiler will typically default to any. You usually want to avoid this, though, because any isn't type-checked. Use the compiler flag noImplicitAny to flag any implicit any as an error.


Type Annotations

When you declare a variable using const, var, or let, you can optionally add a type annotation to explicitly specify the type of the variable:

let myName: string = "Alice";

TypeScript doesn't use "types on the left"-style declarations like int x = 0; Type annotations will always go after the thing being typed.

In most cases, though, this isn't needed. Wherever possible, TypeScript tries to automatically infer the types in your code. For example, the type of a variable is inferred based on the type of its initializer. In the following no type annotation are needed. myName is inferred as type string

let myName = "Alice";

Functions

TypeScript allows you to specify the types of both the input and output values of functions.

Parameter Type Annotations

function greet(name: string) { console.log("Hello, " + name.toUpperCase() + "!!"); }

Return Type Annotations

function getFavoriteNumber(): number { return 26; }

Functions Which Return Promises

If you want to annotate the return type of a function which returns a promise, you should use the Promise type:

async function getFavoriteNumber(): Promise<number> { return 26; }

Anonymous Functions

Anonymous functions are a little bit different from function declarations. When a function appears in a place where TypeScript can determine how it's going to be called, the parameters of that function are automatically given types. Here's an example:

const names = ["Alice", "Bob", "Eve"]; // CONTEXTUAL TYPING FOR FUNCTION - PARAMETER s INFERRED TO HAVE TYPE string names.forEach(function (s) { console.log(s.toUpperCase()); }); // CONTEXTUAL TYPING ALSO APPLIES TO ARROW FUNCTIONS names.forEach((s) => { console.log(s.toUpperCase()); });

Even though the parameter s didn't have a type annotation, TypeScript used the types of the forEach function, along with the inferred type of the array, to determine the type s will have. This process is called contextual typing because the context that the function occurred within informs what type it should have. Similar to the inference rules, you don't need to explicitly learn how this happens, but understanding that it does happen can help you notice when type annotations aren't needed.


Object Types

Apart from primitives, the most common sort of type you'll encounter is an object type. This refers to any JavaScript value with properties, which is almost all of them! To define an object type, we simply list its properties and their types. For example, here's a function that takes a point-like object:

// THE PARAMETER'S TYPE ANNOTATION IS AN OBJECT TYPE function printCoord(pt: { x: number; y: number }) { console.log("The coordinate's x value is " + pt.x); console.log("The coordinate's y value is " + pt.y); } printCoord({ x: 3, y: 7 });

Here, we annotated the parameter with a type with two properties - x and y - which are both of type number. You can use , or ; to separate the properties, and the last separator is optional either way. The type part of each property is also optional. If you don't specify a type, it will be assumed to be any.

Optional Properties

Object types can also specify that some or all of their properties are optional. To do this, add a ? after the property name:

function printName(obj: { first: string; last?: string }) { // ... } // Both OK printName({ first: "Bob" }); printName({ first: "Alice", last: "Alisson" });

In JavaScript, if you access a property that doesn't exist, you'll get the value undefined rather than a runtime error. Because of this, when you read from an optional property, you'll have to check for undefined before using it.

function printName(obj: { first: string; last?: string }) { // ERROR - MIGHT CRASH IF 'obj.last' WAS NOT PROVIDED! console.log(obj.last.toUpperCase()); 'obj.last' is possibly 'undefined'. if (obj.last !== undefined) { // OK console.log(obj.last.toUpperCase()); } // A SAFE ALTERNATIVE USING MODERN JavaScript SYNTAX: console.log(obj.last?.toUpperCase()); }

Union Types

TypeScript's type system allows you to build new types out of existing ones using a large variety of operators. Now that we know how to write a few types, it's time to start combining them in interesting ways.

Defining a Union Type

The first way to combine types you might see is a union type. A union type is a type formed from two or more other types, representing values that may be any one of those types. We refer to each of these types as the union's members.
Let's write a function that can operate on strings or numbers:

function printId(id: number | string) { console.log("Your ID is: " + id); } printId(101); // OKAY printId("202"); // OKAY printId({ myID: 22342 }); // ERROR Argument of type '{ myID: number; }' is not assignable to parameter of type 'string | number'.

Working with Union Types

It's easy to provide a value matching a union type - simply provide a type matching any of the union's members. If you have a value of a union type, how do you work with it?

TypeScript will only allow an operation if it is valid for every member of the union. For example, if you have the union string | number, you can't use methods that are only available on string:

function printId(id: number | string) { console.log(id.toUpperCase()); Property 'toUpperCase' does not exist on type 'string | number'. Property 'toUpperCase' does not exist on type 'number'. }

Narrowing

The solution is to narrow the union with code, the same as you would in JavaScript without type annotations. Narrowing occurs when TypeScript can deduce a more specific type for a value based on the structure of the code.
For example, TypeScript knows that only a string value will have a typeof value "string":

function printId(id: number | string) { if (typeof id === "string") { // In this branch, id is of type 'string' console.log(id.toUpperCase()); } else { // Here, id is of type 'number' console.log(id); } }

Dealing With Possible Array

Another example is to use a function like Array.isArray:

function welcomePeople(x: string[] | string) { if (Array.isArray(x)) { // Here: 'x' is 'string[]' console.log("Hello, " + x.join(" and ")); } else { // Here: 'x' is 'string' console.log("Welcome lone traveler " + x); } }

Notice that in the else branch, we don't need to do anything special - if x wasn't a string array, then it must have been a string.

Sometimes you'll have a union where all the members have something in common. For example, both arrays and strings have a slice method. If every member in a union has a property in common, you can use that property without narrowing:

// RETURN TYPE IS INFERRED AS number[] | string function getFirstThree(x: number[] | string) { return x.slice(0, 3); }

Type Aliases


We've been using object types and union types by writing them directly in type annotations. This is convenient, but it's common to want to use the same type more than once and refer to it by a single name.

A type alias is exactly that - a name for any type. The syntax for a type alias is:

type Point = { x: number; y: number; }; function printCoord(pt: Point) { console.log("The coordinate's x value is " + pt.x); console.log("The coordinate's y value is " + pt.y); } printCoord({ x: 100, y: 100 });

You can actually use a type alias to give a name to any type at all, not just an object type. For example, a type alias can name a union type:

type ID = number | string;

Note that aliases are only aliases - you cannot use type aliases to create different/distinct "versions" of the same type. When you use the alias, it's exactly as if you had written the aliased type. In other words, this code might look illegal, but is OK according to TypeScript because both types are aliases for the same type:

type UserInputSanitizedString = string; function sanitizeInput(str: string): UserInputSanitizedString { return sanitize(str); } // Create a sanitized input let userInput = sanitizeInput(getInput()); // Can still be re-assigned with a string though userInput = "new input";

Interfaces

An interface declaration is another way to name an object type:

interface Point { x: number; y: number; } function printCoord(pt: Point) { console.log("The coordinate's x value is " + pt.x); console.log("The coordinate's y value is " + pt.y); } printCoord({ x: 100, y: 100 });

Differences Between Type Aliases and Interfaces

Type aliases and interfaces are very similar, and in many cases you can choose between them freely. Almost all features of an interface are available in type, the key distinction is that a type cannot be re-opened to add new properties vs an interface which is always extendable.

interface Animal { name: string; } interface Bear extends Animal { honey: boolean; } const bear = getBear(); bear.name; bear.honey;
type Animal = { name: string; } // EXTENDS THE TYPE VIA INTERSECTIONS type Bear = Animal & { honey: boolean; } const bear = getBear(); bear.name; bear.honey;
interface Window { title: string; } interface Window { ts: TypeScriptAPI; } const src = 'const a = "Hello World"'; window.ts.transpileModule(src, {});
type Window = { title: string; } type Window = { ts: TypeScriptAPI; } // Error: Duplicate identifier 'Window'.

Type Assertions

Sometimes you will have information about the type of a value that TypeScript can't know about.
For example, if you're using document.getElementById, TypeScript only knows that this will return some kind of HTMLElement, but you might know that your page will always have an HTMLCanvasElement with a given ID. In this situation, you can use a type assertion to specify a more specific type:

const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");

Literal Types

In addition to the general types string and number, we can refer to specific strings and numbers in type positions.

One way to think about this is to consider how JavaScript comes with different ways to declare a variable. Both var and let allow for changing what is held inside the variable, and const does not. This is reflected in how TypeScript creates types for literals.

let changingString = "Hello World"; changingString = "Olá Mundo"; // Because `changingString` can represent any possible string, that // is how TypeScript describes it in the type system changingString; let changingString: string const constantString = "Hello World"; // Because `constantString` can only represent 1 possible string, it // has a literal type representation constantString; const constantString: "Hello World"

By themselves, literal types aren't very valuable:

let x: "hello" = "hello"; // OK x = "hello"; // ... x = "howdy"; Type '"howdy"' is not assignable to type '"hello"'.

It's not much use to have a variable that can only have one value! But by combining literals into unions, you can express a much more useful concept. For example, functions that only accept a certain set of known values:

function printText(s: string, alignment: "left" | "right" | "center") { // ... } printText("Hello, world", "left"); printText("G'day, mate", "centre"); Argument of type '"centre"' is not assignable to parameter of type '"left" | "right" | "center"'.

Numeric literal types work the same way:

function compare(a: string, b: string): -1 | 0 | 1 { return a === b ? 0 : a > b ? 1 : -1; }

Of course, you can combine these with non-literal types:

interface Options { width: number; } function configure(x: Options | "auto") { // ... } configure({ width: 100 }); configure("auto"); configure("automatic"); Argument of type '"automatic"' is not assignable to parameter of type 'Options | "auto"'.

Null & Undefined


Enums