The relationship between TypeScript and JavaScript is rather unique among modern programming languages. TypeScript sits as a layer on-top of JavaScript, offering the features of JavaScript and then adds its own layer on top of that. This layer is the TypeScript type system.

JavaScript already has a set of language primitives like string, number, object, undefined etc, however there are no ahead-of-time checks that these are consistently assigned across your whole codebase. TypeScript acts as that layer.

This means that your existing working JavaScript code is also TypeScript code, however TypeScript’s type-checker might highlight discrepancies between what you thought was happening and what the JavaScript language does.

This tutorial tries to give you a 5 minute overview of the type-system, with a focus on understanding the type-system language extensions which TypeScript adds.

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.

  1. ts
    let helloWorld = "Hello World";

By understanding how JavaScript works, TypeScript can build a type-system which accepts JavaScript code but has types. This offers a type-system without needing to add extra characters to make types explicit in your code. Which is how TypeScript knows that helloWorld is a string in the above example.

It’s quite possible that you have used VS Code with JavaScript, and had editor auto-completion as you worked. That is because the understanding of JavaScript baked into TypeScript has been used under-the-hood to improve working with JavaScript.

Defining Types

JavaScript is a dynamic language which allows for a lot of design patterns. Some design patterns can be hard to provide types for automatically (because they might use dynamic programming) in those cases TypeScript supports an extension of the JavaScript language which offers places for you to tell TypeScript what the types should be.

Here is an example of creating an object which has an inferred type which includes name: string and id: number:

  1. ts
    const user = {
  2. name: "Hayes",
  3. id: 0,
  4. };

An explicit way to describe this object’s shape is via an interface declaration:

  1. ts
    interface User {
  2. name: string;
  3. id: number;
  4. }

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

  1. ts
    const user: User = {
  2. name: "Hayes",
  3. id: 0,
  4. };

TypeScript will warn you if you provide an object which doesn’t match the interface you have provided:

  1. ts
    interface User {
  2. name: string;
  3. id: number;
  4. }
  5. const user: User = {
  6. username: "Hayes",
  7. Type '{ username: string; id: number; }' is not assignable to type 'User'.
  8. Object literal may only specify known properties, and 'username' does not exist in type 'User'.2322Type '{ username: string; id: number; }' is not assignable to type 'User'.
  9. Object literal may only specify known properties, and 'username' does not exist in type 'User'. id: 0,
  10. };

Because JavaScript supports classes and object-oriented programming, so does TypeScript - an interface declaration can also be used with classes:

  1. ts
    interface User {
  2. name: string;
  3. id: number;
  4. }
  5. class UserAccount {
  6. name: string;
  7. id: number;
  8. constructor(name: string, id: number) {
  9. this.name = name;
  10. this.id = id;
  11. }
  12. }
  13. const user: User = new UserAccount("Murphy", 1);

Interfaces can be used to annotate parameters and return values to functions:

  1. ts
    function getAdminUser(): User {
  2. //...
  3. }
  4. function deleteUser(user: User) {
  5. // ...
  6. }

There are already a small set of primitive types available in JavaScript: boolean, bigint, null, number, string, symbol, object and undefined, which you can use in an interface. TypeScript extends this list with a few more. for example: any (allow anything), unknown (ensure someone using this type declares what the type is), never (it’s not possible that this type could happen) void (a function which returns undefined or has no return value).

You’ll see quite quickly that there are two syntaxes for building types: Interfaces and Types - you should prefer interface, and use type when you need specific features.

Composing Types

Similar to how you would create larger complex objects by composing them together TypeScript has tools for doing this with types. The two most popular techniques you would use in everyday code to create new types by working with many smaller types are Unions and Generics.

Unions

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

  1. ts
    type MyBool = true | false;

Note: If you hover over MyBool above, you’ll see that it is classed as boolean - that’s a property of the Structural Type System, which we’ll get to later.

One of the most popular use-cases for union types is to describe a set of strings or numbers literal which a value is allowed to be:

  1. ts
    type WindowStates = "open" | "closed" | "minimized";
  2. type LockStates = "locked" | "unlocked";
  3. type OddNumbersUnderTen = 1 | 3 | 5 | 7 | 9;

Unions provide a way to handle different types too, for example you may have a function which accepts an array or a string.

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

TypeScript understands how code changes what the variable could be with time, you can use these checks to narrow the type down.

TypePredicate
stringtypeof s === “string”
numbertypeof n === “number”
booleantypeof b === “boolean”
undefinedtypeof undefined === “undefined”
functiontypeof f === “function”
arrayArray.isArray(a)

For example, you could differentiate between a string and an array, using typeof obj === "string" and TypeScript will know what the object is down different code paths.

  1. ts
    function wrapInArray(obj: string | string[]) {
  2. if (typeof obj === "string") {
  3. // ^ = (parameter) obj: string
  4. return [obj];
  5. } else {
  6. return obj;
  7. }
  8. }

Generics

You can get very deep into the TypeScript generic system, but at a 1 minute high-level explanation, generics are a way to provide variables to types.

A common example is an array, an array without generics could contain anything. An array with generics can describe what values are inside in the array.

  1. ts
    type StringArray = Array<string>;
  2. type NumberArray = Array<number>;
  3. type ObjectWithNameArray = Array<{ name: string }>;

You can declare your own types which use generics:

  1. ts
    interface Backpack<Type> {
  2. add: (obj: Type) => void;
  3. get: () => Type;
  4. }
  5. // This line is a shortcut to tell TypeScript there is a
  6. // constant called `backpack`, and to not worry about where it came from
  7. declare const backpack: Backpack<string>;
  8. // object is a string, because we declared it above as the variable part of Backpack
  9. const object = backpack.get();
  10. // Due to backpack variable being a string, you cannot pass a number to the add function
  11. backpack.add(23);
  12. Argument of type '23' is not assignable to parameter of type 'string'.2345Argument of type '23' 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 which 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 the same.

  1. ts
    interface Point {
  2. x: number;
  3. y: number;
  4. }
  5. function printPoint(p: Point) {
  6. console.log(`${p.x}, ${p.y}`);
  7. }
  8. // prints "12, 26"
  9. const point = { x: 12, y: 26 };
  10. printPoint(point);

The point variable is never declared to be a Point type, but TypeScript compares the shape of point to the shape of Point in the type-check. Because they both have the same shape, then it passes.

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

  1. ts
    const point3 = { x: 12, y: 26, z: 89 };
  2. printPoint(point3); // prints "12, 26"
  3. const rect = { x: 33, y: 3, width: 30, height: 80 };
  4. printPoint(rect); // prints "33, 3"
  5. const color = { hex: "#187ABF" };
  6. printPoint(color);
  7. Argument of type '{ hex: string; }' is not assignable to parameter of type 'Point'.
  8. Type '{ hex: string; }' is missing the following properties from type 'Point': x, y2345Argument of type '{ hex: string; }' is not assignable to parameter of type 'Point'.
  9. Type '{ hex: string; }' is missing the following properties from type 'Point': x, y

Finally, to really nail this point down, structurally there is no difference between how classes and objects conform to shapes:

  1. ts
    class VirtualPoint {
  2. x: number;
  3. y: number;
  4. constructor(x: number, y: number) {
  5. this.x = x;
  6. this.y = y;
  7. }
  8. }
  9. const newVPoint = new VirtualPoint(13, 56);
  10. printPoint(newVPoint); // prints "13, 56"

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

Next Steps

This doc is a high level 5 minute overview of the sort of syntax and tools you would use in everyday code. From here you should: