Skip to content

TypeScript

TypeScript is a statically-typed programming language that is a superset of JavaScript. TypeScript was created to address the challenges of building large-scale JavaScript applications and adds optional type annotations, classes, interfaces, and other features to the language.

TypeScript can be used in pretty much any project that JavaScript can be used. But, saying this TypeScript really shines in larger scale production applications that have multiple developers working on it concurrently.

Why TypeScript?

There are many reasons to use TypeScript but the primary motivator for a lot of developers is the increased error detection and handling capabilities that help us write more robust and maintainable code.

The main benefits of using TypeScript include:

  • Enhanced code quality and maintainability
  • Improved productivity
  • Error detection
  • Improved collaboration

TypeScript Syntax Examples

With our bases and foundational knowledge of TypeScript covered, let’s dive into some actual code examples of TypeScript. Of course, we can’t cover every possible situation here but we’ll cover three of the core things you’ll be using in TypeScript; these are variables, functions, and objects.

Before jumping into the examples though it’s important to understand implicit and explicit types as they’ll come up a lot during your TypeScript journey.

  • Implicit is where TypeScript automatically works out the type based on the values being assigned to it.
  • Explicit is where we manually assign a type specifically to that variable.

Now, we know the difference, let’s jump into some examples.

Variables

Variables are the foundation of any programming language and with TypeScript it’s no different; depending on the type of variable, the methods you can call with be different so it’s important they’re typed correctly.

// Implicit variables
const string = 'hello world'; // string
const number = 4; // number
const myFunction = () => 'hello world'; // () => string (function returning a string)
// Explicit variables
const stringOrUndefined: string | undefined = 'hello world'; // string or undefined
const numberOrNull: number | null = 4; // number or null

Functions

We briefly touched on functions in our variables example but let’s now take a bit of a closer look at them here. You can type both the arguments being passed in and the values being returned. But, while you can if you want to, you don’t always need to type the return value as it’ll be implicitly worked out for you most of the time. The arguments on the other hand will always need an explicit typing assigned to them.

// Implicit return of a string
function toUpperCase(arg: string) {
return arg.toUpperCase(); // This will always be a string, as only a string can be provided
}
// Explicit return of a string
function toUpperCaseExplicit(arg: string): string {
return arg.toUpperCase();
}
// Typing arguments provided as an object
function combineStrings({ arg1, arg2 }: { arg1: string, arg2: string }): string {
return `${arg1} ${arg2}`;
}

Objects

We used an object in our previous example for functions, but let’s dive deeper into them here. Objects are a fundamental part of TypeScript, so it’s important we type them correctly. There are a couple of ways we can go about typing them in TypeScript, and one is the implicit route of using the properties defined on the object (which is more accurate and provides better auto-completion), and the other is using index types.

// Implicit types
const obj = {
prop1: 'hello',
prop2: 45,
prop3: undefined
}
// The above object would have a type of
{
prop1: string;
prop2: number;
prop3: undefined
}

This method of typing is the preferred route as you can now immediately see what properties are available on the object and the data types they’ll return. For example, you could use obj.prop1 and be confident it would be a string type. But, what about if the object is being returned from an API or it’s being programmatically generated so it could have one property or it could have a hundred properties?

You could type out every possible property but that’s both time-consuming and not guaranteed to be accurate so a quicker solution is to use an index type.

// Index types
const obj: { [property: string]: string | number | undefined } = {
prop1: 'hello',
prop2: 45,
prop3: undefined
}

TypeScript Features

With the basics of converting JavaScript syntax to TypeScript syntax covered, let’s move on to some of the more TypeScript-specific features and the syntax for those.

Type Guards

One of the most compelling reasons to use TypeScript is the ability for TypeScript to warn you if you are attempting to do something that the type of variable doesn’t allow. This often leads to times when you must guard your statements to allow them to be performed with certainty. For example, if you have a variable that is either a string or an array of strings, you can’t just call .toUpperCase() on the variable because what if it’s an array? It’ll error.

So, let’s look at an example of how to do this safely in TypeScript.

function uppercase(value: string | string[]) {
if (Array.isArray(value)) {
return value.map(str => str.toUpperCase());
}
return value.toUpperCase();
}
uppercase('hello world'); // HELLO WORLD
uppercase(['hello', 'world']); // ['HELLO', 'WORLD']

In this example, we create a function that takes in a value that can be either a string or an array of strings; then inside the function, we handle both possibilities by using Array.isArray() to check if the value passed in is an array or not.

Some other ways you can type guard is using things like [instanceof][typeof] , or type predicates .

Interfaces

Interfaces are another way we can type objects and their values but instead of doing them inline, we can define them independently and then reuse them in multiple places. Let’s take a look at these by adapting our earlier example of object typing.

// Inline typing
const obj: { prop1: string; prop2: number; prop3: undefined } = {
prop1: 'hello',
prop2: 45,
prop3: undefined
}
// Using an interface
interface ObjType {
prop1: string;
prop2: number;
prop3: undefined;
}
const obj: ObjType = {
prop1: 'hello',
prop2: 45,
prop3: undefined
}
const obj2: ObjType = {
prop1: 'world',
prop2: 90,
prop3: undefined
}

As you can see by using an interface we’re able to cut down on the amount of typing we need to write and can reuse these types across multiple objects to save us having to type things multiple times. You can learn more about interfaces and the differences between them and types here .

Unions and Intersection Types

As an extension of interfaces, what happens if you want to join two of them together to create one larger type or if you want to specify that a value can be one of two types? Well, this is where type unions and intersections come into play.

Unions

By using a type union, we can specify that a value can be one of the multiple types, and it’s up to us to use a type guard to narrow that value down to the correct type and handle all of the possibilities. We’ve used type unions already in this tutorial, earlier when we talked about if a value can be either a string[] | string | undefined , this is a union and is denoted by the |. You can also combine these with interfaces and do something like this.

interface ObjOne {
prop1: string;
prop2: number;
prop3: undefined;
}
interface ObjTwo {
prop1: string;
prop2: number;
prop3: string[];
}
type ObjUnion = ObjOne | ObjTwo

Now, if you used ObjUnion as the object type, you would need to narrow it down when using that object to determine which one of the interfaces in the union is the correct one to use for that object.

Intersections

If unions are what happens when we want to say this value can be this type OR that type, intersections are if we want to say this object is this type AND that type. We can use intersections to combine multiple types into one and they look like this.

interface ObjOne {
prop1: string;
prop2: number;
}
interface ObjTwo {
prop3: string[];
}
type ObjOneAndTwo = ObjOne & ObjTwo;
/*
ObjOneAndTwo is now...
{
prop1: string;
prop2: number;
prop3: string[];
}
*/

Learn more about type unions and intersections here .

Enums

Enums are one of the few features in TypeScript that aren’t a type-level extension of JavaScript, enums allow us to define a set of named constraints and use them throughout our code to ensure consistency. Let’s look at an example to help show this.

enum Direction {
North = "NORTH",
East = "EAST",
South = "SOUTH",
West = "WEST",
}
// ❗️ Notice we're using the enum as the type of the argument
function logDirection(direction: Direction) {
console.log(direction)
}
logDirection(Direction.North); // ✅ This works correctly as we use the enum
logDirection('NORTH'); // ❌ This errors as we're not using the enum

As you can see in this example, we use the enum we defined as the type of the argument in the function which means only the values of that enum will be accepted into the function, any other values (even if they match the enum) will throw an error. This is why enums are great for consistency across a codebase as they force developers to use the same values and prevent them from passing in different ones that might cause an error.

Generics

Generics are a more advanced feature of TypeScript but they are also one of the most helpful features of the language. Generics allow us to pass in a type dynamically to a function which means we can take a function that would otherwise have a single purpose and make it more reusable.

A great example of using Generics is functions that fetch data from an API. This is because TypeScript can’t infer the type of the data being returned from an API request. So, instead, it will default to something like unknown or any which isn’t particularly helpful for us developers, and to some extent, it negates the benefits of using TypeScript in the first place. But, this is where generics come in, we can convert a fetcher function to accept a generic and tell TypeScript that the returned value from the fetch function is the type we passed as the generic.

We realize that is a lot to take in and might be a bit confusing so let’s look at an example for it.

interface ObjOne {
prop1: string;
prop2: number;
}
// Defining our function with a generic type (T)
async function fetchData<T>() {
const response = await fetch('API_URL');
// 👇 Telling TS that the response data is the type of T (our generic)
const data = await response.json() as T;
return data;
}
await fetchData < string > (); // The returned data would be a string
await fetchData < number > (); // The returned data would be a number
await fetchData < ObjOne > (); // The returned data would have a type of ObjOne

TypeScript Tooling

The TypeScript ecosystem is constantly expanding and new tools are always being developed and released for the community to use but to get you started on your journey into TypeScript and its tooling here are four of the most popular and well-known tools available.

  • tsc: A TypeScript compiler to convert TypeScript into JavaScript for browsers and Node.js to execute.
  • ts-node: If you don’t want to go the tsc route and would rather just run your TypeScript code directly then you can use ts-node, instead of normal Node.js to execute your code.
  • ESLint: ESLint is an incredibly popular tool for linting JavaScript code but it also supports TypeScript code (via the typeScript-eslint project) with its own set of TypeScript-specific rules. So, if you have a project that uses both JavaScript and TypeScript, this tool is perfect.
  • ts-jest: Want to add some Jest tests to your TypeScript code? ts-jest is the best option for that.

Useful Resources