[ui.dev] Learn Typescript - part 3

Generics

Generics are one of the most powerful parts of TypeScript. They make it possible to reuse and transform our types into different types, instead of having to rewrite different definitions for each type. Think of them as functions, but for types. A type goes in, a different type comes out.

Generics can be a little daunting, so we’ll take this lesson relatively slowly.

Generic Functions

Lets look at a simple example: pulling a single item from an array.

function getFirstItem(list: number[]): number {
  return list[0];
}

What if we wanted to grab a string from a list of strings instead? We would have to write another function.

function getFirstStringItem(list: string[]): string {
  return list[0];
}

Our array implementation is exactly the same , but we have to write it twice because the type signatures are different. What if we were to use a union type?

function getFirstItem(list: (number | string)[]): number | string {
  return list[0];
}

This is better - we only have to define our function once - but it doesn’t really describe our API at all. This type signature implies that our array contains a mixture of numbers and strings, which isn’t the case at all. Also, once we have the result of the function, we have to narrow the type to be either number or string .

This is where generics come in. We want the ability to write a type signature for a function that takes in an array of some type, lets call that type T , and returns a single item with the type T . Here’s how we would write that signature.

function getFirstItem<T>(list: T[]): T {
  return list[0];
}

Notice the angle brackets we use right after the function name. This is where we define generic types. Here, we’re defining a generic called T , but we could call it anything we want. Once we’ve defined our generic type, we can use it anywhere in our function, including on the parameters and the return signature. Here, we’re saying that our list parameter is an array of T , and it returns a T value.

Now we can use it anywhere we want, with any type we want! The best part is TypeScript will automatically infer the return type from the usage. It even correctly infers complex types, like classes.

class Fruit {
  constructor(public name: string) {}
}

const fruit = getFirstItem([
  new Fruit("banana"),
  new Fruit("apple"),
  new Fruit("pear"),
]);

fruit; // const fruit: Fruit

So, to recap: Generics represent a type that won’t be defined until the type is used in our code. We can use generics with functions. When we originally write the generic function, we might not know the type the generics represent, but when we use our function elsewhere in our code, the generics’ types can be inferred from the usage. This makes it possible to write functions that accept different kinds of types but have the same implementation for each type.

Generic Types

Generics aren’t just for functions. In fact, we can create generic Interfaces, Classes, and type aliases.

Here’s an example that we looked at earlier in the course. This type represents a tree of string values.

type StringTree = {
  value: string;
  left?: StringTree;
  right?: StringTree;
};

This is a great type which could be really handy when working with trees of string s. But what if we have a tree of something other than string s? Either I would have to make a separate type for each of them, or I could make a generic type.

type Tree<T> = {
  value: T;
  left?: Tree<T>;
  right?: Tree<T>;
};

You can see that we accept a generic type that we call T . We then use that as the type of the value on our tree.

When we create our left and right properties, we use the same type recursively, but we need to tell that type what generic type it should use. We can just pass T back into our recursive type, which will let us use T for all of the values of our Tree . We pass types to our generics by putting angle brackets after the generic’s name, like so:

type StringTree = Tree<string>;

Let’s see how this looks when actually creating an object with this type. We’ll use this type to represent a literal tree that grows in the ground that has grafted branches of fruit on it.

interface Fruit {
  name: string;
  color: string;
}

const graftedFruitTree: Tree<Fruit> = {
  value: { name: "trunk", color: "brown" },
  left: {
    value: { name: "apple", color: "red" },
    right: {
      value: { name: "orange", color: "orange" },
    },
  },
  right: {
    value: { name: "pear", color: "yellow" },
  },
};

Generic Classes

Lets take a look at a more complicated example. Suppose we want to create fruit baskets. These baskets will be classes, and each basket will only be able to hold a single type of fruit - no mixed baskets - and each fruit will be its own class, extended from a base Fruit class. We also want to make sure our baskets are actually holding fruit, and not vegetables or rocks or something else.

Let’s start by making a fruit basket class.

class Fruit {
  isFruit: true;
  constructor(public name: string) {}
}
class FruitBasket {
  constructor(public fruits: Fruit[] = []) {}
  add(fruit: Fruit) {
    this.fruits.push(fruit);
  }
  eat() {
    this.fruits.pop();
  }
}

This works well for holding any kind of fruit, but now we need to be able to hold different kinds of fruit. Let’s create our fruit classes. We’ll give each of our fruit classes a type property, to make it clear what type of fruit it is. (This works in a similar way to discriminating unions.)

class Fruit {
  isFruit:true;
  constructor(public name:string) {}
}
class Apple extends Fruit {
  type:"Apple",
  constructor() {
    super("Apple")
  }
}
class Banana extends Fruit {
  type:"Banana",
  constructor() {
    super("Banana")
  }
}

Now, we don’t want to go through the effort of making a separate fruit basket class for each kind of fruit. Instead, we’ll make a generic class . We do it the same way as the generic function, by defining the generic name with angle brackets in front of the class’s name. Then we can use that generic type anywhere we need to inside our class definition.

class FruitBasket<T> {
  constructor(public fruits: T[] = []) {}
  add(fruit: T) {
    this.fruits.push(fruit);
  }
  eat() {
    this.fruits.pop();
  }
}

Let’s look closer at what’s going on here. We are defining a generic type T at the top of our class, before we get to our constructor. Then, down in the constructor, we are using the property shorthand to create a public property called fruits . We’re giving fruits a type of T[] , which means it is an array of T . We’re setting fruits to an empty array by default.

The add method takes fruit as a parameter, with the type being T . This lets us add a single T - whatever that ends up being - to our array of T .

Now we can instantiate our fruit basket.

const appleBasket = new FruitBasket(); // const appleBasket: FruitBasket<unknown>

Hang on, what’s going on here? Why is our appleBasket a basket of unknown ? With our getFirstItem example, TypeScript was able to infer the type of the generic based on what we passed to it. In this case, we aren’t passing anything to our FruitBasket constructor, so it has no way of knowing what type the generic should be, so it defaults to unknown . We have three options.

Option 1: Pass a parameter to FruitBasket when we call the constructor so TypeScript can infer the type of the generic. We have written our class so we can pass an array of items as the initial list.

const appleBasket = new FruitBasket([new Apple()]); // const appleBasket: FruitBasket<Apple>

Option 2: Provide a default generic type for our FruitBasket class. If a type isn’t provided or inferred for our generic, TypeScript will fall back on the default type instead of unknown .

class FruitBasket<T = Apple> {
  //...
}

Option 3: Provide a type for the generic in the class when we instantiate it. We do this by putting the type we want to use for the generic in angle brackets just after the name of the class when we are instantiating it. This same syntax works to specify a type when calling a generic function.

const appleBasket = new FruitBasket<Apple>(); // const appleBasket: FruitBasket<Apple>

Now that we have our appleBasket , lets put some fruit in it.

appleBasket.add(new Apple());
appleBasket.add(new Banana()); // Type Error: Argument of type 'Banana' is not assignable to parameter of type 'Apple'.

TypeScript has warned us that we can put a Banana in appleBasket , since it’s type is incompatible with Apple . We could, however, create a bananaBasket and put a Banana in that basket instead$$.

Generic Constraints

The final thing we wanted to do was make it impossible to put things like Vegetable s inside of our FruitBasket . We can do that by applying a type constraint . This tells TypeScript that our generic can’t just be any type. It has to match the constraints we supply. To do this, we’ll use the extends keyword in our generic definition.

class FruitBasket<T extends Fruit> {
  // ...
}

In this case, we’re saying that our generic T has to match Fruit . That means any type that we pass in for T has to at least have the same properties as Fruit , including the types of those properties. Since Apple and Banana are extended from Fruit , they automatically qualify. Anything that doesn’t match Fruit will throw a type error.

Now we’ll add our Vegetable class and try to make a FruitBasket of Vegetable s.

class Vegetable {
  isFruit: false;
  constructor(public name: string) {}
}

const appleBasket = new FruitBasket<Apple>(); // This works.

const vegetableBasket = new FruitBasket<Vegetable>();
// Type Error: Type 'Vegetable' does not satisfy the constraint 'Fruit'.
//   Types of property 'isFruit' are incompatible.

Hurray! We can’t make a vegetableBasket ! Vegetable may have a isFruit property, but for Vegetable s, that property is a literal false type, while Fruit has a literal true type. Those two types are incompatible, so Vegetable does not pass the constraint we put on our generic T .

Multiple Type Parameters

We can use multiple type parameters in our generics as well. We just separate our generics with a comma when we define them, much like we do with function parameters. As an example, we’ll create a function which grabs a property from whatever object we pass to it. The object’s type will be captured with the T generic, and the key (which could be either a string or a number, for arrays) will be captured with the K generic.

function getObjectProperty<T, K extends string | number>(object: T, key: K) {
  return object[key]; // Type Error: Type 'K' cannot be used to index type 'T'.
}

It looks like our naïve solution creates a type error. TypeScript has no way of knowing ahead of time whether K is the type which is used to index T .

What we can do to solve this is tell TypeScript right away that K represents all of the keys of T using the keyof operator. Once we’ve defined T , we can use it elsewhere in our generic type definition.

function getObjectProperty<T, K extends keyof T>(object: T, key: K) {
  return object[key];
}

Now TypeScript knows that the key parameter has to be one of the properties of object . This means we’ll get type errors if we accidentally use the wrong property name, such as if we make a typo.

const fruit = {
  name: "Apple",
  sweetness: 70,
};

const fruitName = getObjectProperty(fruit, "name");
const misspelledName = getObjectProperty(fruit, "naem");
// Type Error: Argument of type '"naem"' is not assignable to parameter of type '"name" | "sweetness"'.

TypeScript caught our typo, because it knows what properties are present on fruit and knows the property we are asking for doesn’t exist.

(Bonus) Zustand Implementation

Types exist not only to tell the TypeScript compiler how your program works. They also serve as great documentation for other developers who might want to know how to use your API. Reading the type definitions of other projects can help us understand how an API is supposed to be used and help us better understand how to write good types.

Zustand is an up-and-coming state management library for JavaScript. It has support for React, but can be used with any JavaScript framework, or by itself. It has a simple API which uses Generics to make it really easy to work with your state. We’re going to take a tour of the API and see how it’s implemented.

Here’s a sample of how the API works, from the Zustand documentation.

import create from "zustand/vanilla"; // We won't use any of the React APIs

interface BearStore {
  bears: number;
  increasePopulation: () => void;
  removeAllBears: () => void;
}

// Create our store using the interface we defined earlier.
const store = create<BearStore>((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}));

// Call one of the functions we defined
store.getState().increasePopulation();

// Log the current value of the state
console.log(store.getState().bears);

// Update the state
store.setState({ bears: 10 });

// This subscribes to updates. Whenever `state.bears` changes, it will log it to the console.
const unsubscribeFunction = store.subscribe(
  (store) => console.log(store.bears),
  (state) => state.bears
);

Lets start by taking a look at that create function.

function create<TState extends State>(
  createState: StateCreator<TState>
): StoreApi<TState>;

This is the function signature. It has a single generic, TState which has a type constraint extends State . Before we go any further, lets look at that constraint.

type State = Record<string | number | symbol, any>;

Record is a utility type. It takes in two generic parameters: a string | number | symbol , and any . It uses the first as an index type and the second as the type of the value. Basically, it represents any kind of object or array. Using it as a type constraint means TState has to be an object or array; we can’t use a string or number for TState .

Back to the create function.

function create<TState extends State>(
  createState: StateCreator<TState>
): StoreApi<TState>;

Our function takes a createState parameter which has a type of StateCreator<TState> . It’s passing our generic type TState in as a generic to StateCreator . Lets take a look at that definition.

type StateCreator<TState extends State> = (
  set: SetState<TState>,
  get: GetState<TState>,
  api: StoreApi<TState>
) => T;

StateCreator is a type alias for a function definition. This function has three parameters: set , get , and api , and it passes our TState generic in to all three of them. Finally, the function returns a value of the type TState , and that return value becomes our store object.

This is the function we use when we create our store. In fact, we used the set parameter in the increasePopulation and removeAllBears functions. Let’s dive in one level deeper and see what SetState is doing.

type PartialState<TState extends State> =
  | Partial<TState>
  | ((state: TState) => Partial<TState>)
  | ((state: TState) => void);

type SetState<TState extends State> = (
  partial: PartialState<TState>,
  replace?: boolean
) => void;

SetState once again uses our TState generic, and uses it in a function. The first parameter, partial , uses the PartialState type. This is a Union type which represents three things.

First, it includes Partial<TState> . Partial is another utility type. It makes all of the properties of whatever type you pass in optional. This lets us pass in only a part of our state, instead of needing to pass in a complete copy of our state with only a small change. We use that in the removeAllBears function.

Second, we can pass in a function with our state TState as a parameter that returns Partial<TState> . This lets us modify a part of our state based on the current state. We use that in the increasePopulation function.

Finally, we can pass in a function with our state TState as a parameter that returns void . This is used by Zustand to indicate that we actually don’t want to make any changes to our state.

So, to recap, the SetState function gives us three options which we can use for the first partial parameter. The second parameter is an optional boolean that tells Zustand whether we want to outright replace our state instead of changing a small part. Our SetState function returns void , meaning it doesn’t return anything of value.

We dived pretty deep on this part. Let’s go back to the complete signature of create .

function create<TState extends State>(
  createState: StateCreator<TState>
): StoreApi<TState>;

The final part of this signature is the return value: StoreApi<TState> . This ends up being the type of const store , so it’s pretty important. Let’s take a look at how it works.

interface StoreApi<T extends State> {
  setState: SetState<T>;
  getState: GetState<T>;
  subscribe: Subscribe<T>;
  destroy: Destroy;
}

The TState extends State signature probably looks very familiar at this point. We already know what SetState<TState> does. Let’s look at GetState<TState> next.

type GetState<TState extends State> = () => T;

Well, that’s simple. It’s a type signature for a function that does one thing: Return our store. Remember, in this case, we passed the type of our store in as the generic TState , and that’s all that this returns.

We’ll look at Destroy next.

type Destroy = () => void;

This is a function that takes no parameters and returns nothing. It cleans up the store and removes any subscription listeners, so it doesn’t need a complicated type definition.

subscribe: Subscribe<TState> , on the other hand, is a little bit more involved, so we’ll take it extra slowly. subscribe lets us create a function which is called whenever we make changes to our state. Optionally, we can provide a selector, which is a function that tells the subscription to only fire when a particular bit of state changes.

interface Subscribe<TState extends State> {
  (listener: StateListener<TState>): () => void;
  <StateSlice>(
    listener: StateSliceListener<StateSlice>,
    selector: StateSelector<TState, StateSlice>,
    equalityFn?: EqualityChecker<StateSlice>
  ): () => void;
}

This is an interface that defines a function with two overloads. We’ll go over each of them separately, pretending like each is the only definition of the function.

type StateListener<TState> = (state: TState) => void;

interface Subscribe<TState extends State> {
  (listener: StateListener<TState>): () => void;
}

The first overload accepts a parameter called listener which is a StateListener<TState> . We can see in the definition above that StateListener<TState> is a function with a parameter for our state that returns void . This lets us pass a StateListener function which is called with our state whenever anything in the state changes. The Subscribe function also returns another function, known as the unsubscribe function, which takes no parameters and returns void . Calling this will deactivate our subscription, making it so our StateListener function isn’t called anymore.

The second overload is more complicated.

interface Subscribe<TState extends State> {
  <StateSlice>(
    listener: StateListener<StateSlice>,
    selector: StateSelector<TState, StateSlice>,
    equalityFn?: EqualityChecker<StateSlice>
  ): () => void;
}

The second overload creates a new generic just for this function, called StateSlice . The syntax for this is different than other generic functions, since it is being defined inside an interface. StateSlice represents a small part of the state, as defined by the StateSelector<TState, StateSlice> .

type StateSelector<TState extends State, U> = (state: TState) => U;

This function isn’t too complicated by itself. It takes in our state TState and returns U . Remember, when this function is defined in Subscribe , U is really StateSlice . What’s happening is we’re transforming TState into StateSlice , and then using StateSlice everywhere else in our Subscribe function.

Let’s see what the optional equalityFn?: EqualityChecker is doing with StateSlice .

type EqualityChecker<StateSlice> = (
  state: StateSlice,
  newState: any
) => boolean;

This function takes in our state StateSlice . It also takes in newState, which is any . It then returns a boolean . The purpose of this function is to see if our state actually changed. If the function returns true , then the new state is effectively the same as the old state and the subscription doesn’t fire. If the function returns false , then the new state is different from the old state, and the subscription fires.

Finally, StateListener is the same as the first overload, but is passed StateSlice instead of TState .

That’s the whole implementation. Zustand uses generic to make it possible to store just about any kind of state, while only creating about 15 type definitions. That’s a pretty lean API!

Hopefully we’ve learned a bit more about the power of Generics and how to compose them together to create flexible APIs.

(Practice) Generics

Below is a CodeSandbox link which has some TypeScript code. Your job is to implement a few generic functions and types so all of the TypeScript warnings go away.

Good luck!

Generics CodeSandbox

(Solution) Generics

Solution

Generics CodeSandbox Solution

(Bonus) Thinking In Types

We’ve covered a lot of ground in this course. We’ve learned about the base types that come with JavaScript; a bunch of new types that are part of TypeScript; special ways that we can operate on types with typeof, keyof, Unions, and Intersections; and how we can make our types more generic with… well, generics.

As we know, the TypeScript compiler takes our TypeScript code and turns it into JavaScript code which we can then execute. If I had the following TypeScript code, taken from our Generics lesson:

interface Fruit {
  isFruit: true;
  name: string;
}
class FruitBasket<T extends Fruit> {
  constructor(public fruits: T[] = []) {}
  add(fruit: T) {
    this.fruits.push(fruit);
  }
  eat() {
    this.fruits.pop();
  }
}

TypeScript would compile it into the following JavaScript:

class FruitBasket {
  constructor(fruits = []) {
    this.fruits = fruits;
  }
  add(fruit) {
    this.fruits.push(fruit);
  }
  eat() {
    this.fruits.pop();
  }
}

It looks very similar to our TypeScript code, except all of the type definitions, including the Fruit interface and generic type T , were removed. Instead, we’re just left with runtime code.

TypeScript also lets us compile our code into a format that is just types . This file is often shipped with the compiled JavaScript in case an upstream developer needs access to the type definitions for their IDE or something. We can easily see this by pasting the code into TypeScript’s Playground and choosing the “.d.ts” tab on the output panel.

interface Fruit {
  isFruit: true;
  name: string;
}
declare class FruitBasket<T extends Fruit> {
  fruits: T[];
  constructor(fruits?: T[]);
  add(fruit: T): void;
  eat(): void;
}

It’s like a shadow of our original code. It has the same shape, but it’s missing all of the implementation. Still, you can recognize a lot of what is going on just from the type signatures.

It might not be apparent, but type systems can be thought of in very similar terms to our runtime systems. Granted, it’s like an upside-down version of our runtime code, but a lot of the same principles are there.

  • Type aliases are like variables. They are named buckets that we can put any type in, just like a regular variable.
  • TypeScript operators, like typeof, keyof, | , and & , let us combine, manipulate, and modify our types, like of like +, *, and -.
  • Type narrowing lets us determine an exact type from several possibilities.
  • Generics work like functions. Generic types accept a type as a parameter and operate on it to transform it into a different type.

All of these things work together to allow you to compose simple types, like strings and numbers, into more complicated types, like video game entities and business units.

Sometimes it’s going to be a little difficult to figure out what’s going on with our types. Strings and numbers are a little more concrete - every programmer is familiar with how they work. Generic types are more abstract, which makes them a little more complicated to reason about if you don’t have much experience with type systems.

In this section of the course, we’ll be talking about two more constructs which take principles from runtime programming and let us use them as we work in the type system.

  • Mapped types are like loops, which let us take a union of types and perform a transformation on each of the types in the union.
  • Conditional types are like if statements. They let us modify a type based on some condition which must be met.

These two features give us even more control over our types, making it easier for us to reuse and compose our types. As we talk about them, pay attention to how we use the generic types. Especially pay attention to where our generic types are defined.

At the end of this section there will be an activity where we apply what we’ve learned about generics, type operators, mapped types, and conditional types to create a series of utility types that can be used to transform other types. This might be one of the more challenging activities, but don’t skip it. Mastering how these utility types are composed will be very helpful, both for crating complicated type definitions and for understanding simpler type definitions.

Mapped Types

Suppose we had an interface and we wanted to make a readonly version of it, to make sure that we aren’t making any changes to the object. One way we could do this is by creating two interfaces.

interface Fruit {
  name: string;
  color: string;
  sweetness: number;
}

interface ReadonlyFruit {
  readonly name: string;
  readonly color: string;
  readonly sweetness: string;
}

This is fine, except the duplication is a little verbose, especially since we know ReadonlyFruit will have the same properties as Fruit , just readonly . If these two type definitions were in separate files, we might accidentally forget to update one when we change the other.

Fortunately, TypeScript provides us with a special type signature which we can use to perform transforms on interfaces and object types. Mapped types allow us to take an existing type, pull out each property individually, and perform a transformation on that property’s type. You can think of is an an array .map() function, but for the properties on an object type.

These special type transformers are generic types that work like functions. We pass in the object type that we want to transform, and it returns a new type with the transformations.

Let’s use our example above to create a type transformer that makes all properties on an object readonly . Because the syntax is a little complicated, we’ll go over it step by step.

The first thing we need to do is get all of the properties out of our fruit. We’ll create a generic type which uses the keyof operator to do that.

type Properties<T> = keyof T;

type FruitProperties = Properties<Fruit>; // type FruitProperties = "name" | "color" | "sweetness"

TypeScript has determined the properties of whatever we pass in to our generic Properties type and given it to us as a union of string literals. We could use this to get a list of values for an object too. We’ll do that by using an indexed access to get the type of the values using the properties which we already determined.

type Values<T> = T[Properties<T>];
type FruitValues = Values<Fruit>; // type FruitValues = string | number

This is handy if we want to get the type of every value in our interface, but what we really want to do is loop over each property and get the value for that property, and perform the transform we want to do at the same time. We can perform that loop using the in keyword. We’ll use the in keyword to pull individual property literal types out of our Properties type into a new generic P . We can then use P to access the type of that property’s value in T .

type ObjectIdentity<T> = {
  [P in Properties<T>]: T[P];
};

type FruitCopy = ObjectIdentity<Fruit>;
// type FruitCopy = {
//     name: string;
//     color: string;
//     sweetness: number;
// }

The syntax P in Properties<T> probably looks familiar. It very closely resembles the syntax for a for .. in loop in JavaScript. Just like for .. in creates a new variable for each iteration of the loop, we’re creating a new type for each property in our list.

This is called ObjectIdentity because it doesn’t perform any transformation. It will return exactly the same object type that you pass in. We now have a template which we can use to create our transformations. For example, by prepending readonly to our property, we can make each property readonly .

type Readonly<T> = {
  readonly [P in Properties<T>]: T[P];
};

type ReadonlyFruit = Readonly<Fruit>;
// type ReadonlyFruit = {
//     readonly name: string;
//     readonly color: string;
//     readonly sweetness: number;
// }

If you were to paste this into the TypeScript playground or a TypeScript file, you might get an error: Duplicate identifier 'Readonly'. . That’s because TypeScript actually ships with a number of transformer types, which it calls Utility Types . These utility types are so helpful that TypeScript bundles a number of them which we can wherever we want. We’ll look closer at utility types, and create several more ourselves, later in this section.

The final thing we might want to do is remove a modifier from a type signature. For example, if we wanted to change a type so its properties are not readonly . We do this by prepending a minus ( - ) to the modifier when we are adding it to the type. This tells TypeScript “Please remove this modifier from this property if it has it”.

type UnReadonly<T> = {
  -readonly [P in Properties<T>]: T[P];
};

type WritableFruit = UnReadonly<ReadonlyFruit>;
// type WritableFruit = = {
//     name: string;
//     color: string;
//     sweetness: number;
// }

We can even do this with the optional modifier, such as in this Required utility type, which makes all of the properties required.

type Required<T> = {
  [P in Properties<T>]-?: T[P];
};

interface OptionalFruit {
  name: string;
  color?: string;
  sweetness?: number;
}
type RequiredFruit = Required<OptionalFruit>;
// type RequiredFruit = = {
//     name: string;
//     color: string;
//     sweetness: number;
// }

We’ll see a lot more ways to work with mapped types when we take a closer look at utility types later.

Conditional Types

We’ve already learned that we can use literals to represent types that can only be assigned to that literal. For example, the following type can only be assigned to the value “Apple”.

type AppleLiteral = "Apple";

let name: AppleLiteral = "Apple";
let wrongName: AppleLiteral = "Banana"; // Type Error: Type '"Banana"' is not assignable to type '"Apple"'.
let nameString: string = name; // This works

We know that the AppleLiteral type can be assigned to a string , but TypeScript doesn’t tell us that outright. What would be nice if there were some way we could use a type constraint to determine what type a literal type represents.

Fortunately, TypeScript provides us with a construct to do that. Conditional types let us provide a type constraint. If the constraint passes, we get one type; otherwise, we get a different type. If this sounds like an if statement or ternary for types, that’s exactly what it is.

We can create a conditional type by using the ternary syntax, with a question mark after the type constraint, followed by the “true” result, followed by a colon ( : ), and then the “false” result.

type LiteralIsStringType<T> = T extends string ? string : never;

type AppleLiteralType = LiteralIsStringType<AppleLiteral>; // type AppleLiteralType = string;
type NeverLiteralType = LiteralIsStringType<0>; // type NeverLiteralType = never;

This type definition tells us "If the generic T is assignable to a string, then give us a string type; if we pass anything but a string or string literal, give us never ".

We could extend this to include more literal types by chaining our conditional type with more type constraints and results. For example, this will tell us whether a literal is a string , number , or boolean .

type LiteralType<T> = T extends string
  ? string
  : T extends number
  ? number
  : T extends boolean
  ? boolean
  : never;

There are other exciting things we can do as well! For example, suppose we have a type which is “nullable”, which means it is included in Union with null | undefined . If we wanted to transform our type so it does not include null | undefined , we could use a conditional type to check if our type is assignable to null | undefined . If it is, we get never ; if it isn’t, we get our type.

type NonNullable<T> = T extends null | undefined ? never : T;

type NonNullString = NonNullable<string | null | undefined>; // type NonNullString = string;

This demonstrates an important property of conditional types. Conditional types are distributive, which means that when we pass in a Union type, the type constraint isn’t checked against the entire Union that is passed in; rather the constraint is checked against each member of the Union individually.

When checking the string part of the type we pass in, the T in our NonNullable type represents a string type. Since a string type doesn’t extend null | undefined , our conditional fails, and it returns T , which is actually string . When T represents null or undefined , however, both of those pass the type condition, so it returns never . Any time you have a Union of a type and never , the never is removed from the Union automatically, which leaves you with string .

This exclusionary quality of the never type really comes in handy when working with conditional types and type Unions. We can use this to make a utility type which takes two Unions; if any of the types in the first Union are assignable to the second Union, it will remove them from the first union. To create this utility type, we’ll use a generic type that takes two generic parameters.

type Exclude<T, U> = T extends U ? never : T;

type FavoriteLetters = "a" | "l" | "e" | "x";
type Vowels = "a" | "e" | "i" | "o" | "u" | "y";

type NonFavoriteVowels = Exclude<Vowels, FavoriteLetters>;
// type NonFavoriteVowels = "i" | "o" | "u" | "y"

We can switch our conditional around so instead of excluding items from the first Union that match the second, it extracts items from the first Union that match the second.

type Extract<T, U> = T extends U ? T : never;

type FavoriteVowels = Extract<Vowels, FavoriteLetters>;
// type FavoriteVowels = "a" | "e"

Conditional Type Inference

Let’s take a look at one more conditional utility type which could really come in handy. Often, we need to grab the return type of a function. This might be useful if we pass the function’s result to other functions and need an easy way to give those functions the appropriate type signature.

We can’t use index access to get the type from a function’s return signature - that doesn’t even make sense. However, TypeScript is able to infer the type of functions easily enough. What we need is a way to hook into TypeScript’s inference system to tell it we need it to infer the value of a particular part of a type signature.

We’ll create a new utility type called ReturnType . It will have a generic T which is constrained to any function definition. That will keep us from passing string s and Interfaces and such to it. We’ll then use a conditional type to see if T extends a function definition, and add the keyword infer to the return type. This lets us create a new generic, which we’ll call R which represents the function’s return type. If TypeScript is able to infer the type of R , then that’s what we’ll return; otherwise we’ll return any .

type AnyFunction = (...args: any) => any;
type ReturnType<T extends AnyFunction> = T extends (...args: any) => infer R
  ? R
  : any;

// We have to use `typeof` to extract the type signature of this function.
type ParseIntReturn = ReturnType<typeof Number.parseInt>; // type ParseIntReturn = number

// ParseIntReturn is already a type, so we can directly access its properties without using `typeof`
type ToStringReturn = ReturnType<ParseIntReturn["toString"]>; // type ToStringReturn = string

We can think of the infer keyword as unwrapping whatever thing we use it with in our conditional type. In this case, we are unwrapping a function’s type to get the return type. We could also unwrap a function’s parameters by infer ing the parameters type instead of the return type.

type Parameters<T extends AnyFunction> = T extends (...args: infer P) => any
  ? P
  : never;

type ParseIntParams = Parameters<typeof Number.parseInt>; // type ParseIntParams = [s: string, radix?: number]

We can use the infer keyword in conditional types to unwrap many other things too. For example, here’s a conditional type that either unwraps an array T to return the type of the array’s values. If T is not an array, it returns T .

type UnwrapArray<T> = T extends (infer R)[] ? R : T;

In this case, it is infer ing the type of the array T 's values, and putting that type into R .

For good measure, let’s unwrap a Promise with infer . This will give us the type of the value that is resolved by the Promise .

type UnwrapPromise<T> = T extends Promise<infer R> ? R : T;

We likely won’t be using conditional type inference, or maybe even conditional types themselves, directly too much as we write TypeScript. Instead, we’ll most likely create a number of reusable utility types which we can then sprinkle throughout our code. TypeScript ships with many utility types as well, which you can review on the TypeScript website.

(Practice) Utility Types

Below is a CodeSandbox link which has some TypeScript code. Your job is to create new types for each of the utility types which is listed in the TypeScript file. These utility types should match the description which is given, and the end-result type should match the comments as well…

Good luck!

Utility Types CodeSandbox

(Solution) Utility Types

Solution

Utility Types CodeSandbox Solution

(Bonus) From IIFEs to CommonJS to ES6 Modules

This is originally part of our Advanced JavaScript course. However, it’s applicable to us here as well.

I’ve taught JavaScript for a long time to a lot of people. Consistently the most commonly under-learned aspect of the language is the module system. There’s a good reason for that. Modules in JavaScript have a strange and erratic history. In this post, we’ll walk through that history and you’ll learn modules of the past to better understand how JavaScript modules work today.

Before we learn how to create modules in JavaScript, we first need to understand what they are and why they exist. Look around you right now. Any marginally complex item that you can see is probably built using individual pieces that when put together, form the item.

Let’s take a watch for example.

A simple wristwatch is made up of hundreds of internal pieces. Each has a specific purpose and clear boundaries for how it interacts with the other pieces. Put together, all of these pieces form the whole of the watch. Now I’m no watch engineer, but I think the benefits of this approach are pretty transparent.

Reusability

Take a look at the diagram above one more time. Notice how many of the same pieces are being used throughout the watch. Through highly intelligent design decisions centered on modularity, they’re able to re-use the same components throughout different aspects of the watch design. This ability to re-use pieces simplifies the manufacturing process and, I’m assuming, increases profit.

Composability

The diagram is a beautiful illustration of composability. By establishing clear boundaries for each individual component, they’re able to compose each piece together to create a fully functioning watch out of tiny, focused pieces.

Leverage

Think about the manufacturing process. This company isn’t making watches, it’s making individual components that together form a watch. They could create those pieces in house, they could outsource them and leverage other manufacturing plants, it doesn’t matter. The most important thing is that each piece comes together in the end to form a watch - where those pieces were created is irrelevant.

Isolation

Understanding the whole system is difficult. Because the watch is composed of small, focused pieces, each of those pieces can be thought about, built and or repaired in isolation. This isolation allows multiple people to work individually on the watch while not bottle-necking each other. Also if one of the pieces breaks, instead of replacing the whole watch, you just have to replace the individual piece that broke.

Organization

Organization is a byproduct of each individual piece having a clear boundary for how it interacts with other pieces. With this modularity, organization naturally occurs without much thought.

We’ve seen the obvious benefits of modularity when it comes to everyday items like a watch, but what about software? Turns out, it’s the same idea with the same benefits. Just how the watch was designed, we should design our software separated into different pieces where each piece has a specific purpose and clear boundaries for how it interacts with other pieces. In software, these pieces are called modules . At this point, a module might not sound too different from something like a function or a React component. So what exactly would a module encompass?

Each module has three parts - dependencies (also called imports), code, and exports.

imports

code

exports
Dependencies (Imports)

When one module needs another module, it can import that module as a dependency. For example, whenever you want to create a React component, you need to import the react module. If you want to use a library like lodash , you’d need import the lodash module.

Code

After you’ve established what dependencies your module needs, the next part is the actual code of the module.

Exports

Exports are the “interface” to the module. Whatever you export from a module will be available to whoever imports that module.

Enough with the high-level stuff, let’s dive into some real examples.

First, let’s look at React Router. Conveniently, they have a modules folder. This folder is filled with… modules, naturally. So in React Router, what makes a “module”. Turns out, for the most part, they map their React components directly to modules. That makes sense and in general, is how you separate components in a React project. This works because if you re-read the watch above but swap out “module” with “component”, the metaphors still make sense.

Let’s look at the code from the MemoryRouter module. Don’t worry about the actual code for now, but focus on more of the structure of the module.

// imports
import React from "react";
import { createMemoryHistory } from "history";
import Router from "./Router";

// code
class MemoryRouter extends React.Component {
  history = createMemoryHistory(this.props);
  render() {
    return (
      <Router
        history={this.history}
        children={this.props.children}
      />;
    )
  }
}

// exports
export default MemoryRouter;

You’ll notice at the top of the module they define their imports, or what other modules they need to make the MemoryRouter module work properly. Next, they have their code. In this case, they create a new React component called MemoryRouter . Then at the very bottom, they define their export, MemoryRouter . This means that whenever someone imports the MemoryRouter module, they’ll get the MemoryRouter component.

Now that we understand what a module is, let’s look back at the benefits of the watch design and see how, by following a similar modular architecture, those same benefits can apply to software design.

Reusability

Modules maximize reusability since a module can be imported and used in any other module that needs it. Beyond this, if a module would be beneficial in another program, you can create a package out of it. A package can contain one or more modules and can be uploaded to NPM to be downloaded by anyone. react , lodash , and jquery are all examples of NPM packages since they can be installed from the NPM directory.

Composability

Because modules explicitly define their imports and exports, they can be easily composed. More than that, a sign of good software is that is can be easily deleted. Modules increase the “delete-ability” of your code.

Leverage

The NPM registry hosts the world’s largest collection of free, reusable modules (over 700,000 to be exact). Odds are if you need a specific package, NPM has it.

Isolation

The text we used to describe the isolation of the watch fits perfectly here as well. “Understanding the whole system is difficult. Because (your software) is composed of small, focused (modules), each of those (modules) can be thought about, built and or repaired in isolation. This isolation allows multiple people to work individually on the (app) while not bottle-necking each other. Also if one of the (modules) breaks, instead of replacing the whole (app), you just have to replace the individual (module) that broke.”

Organization

Perhaps the biggest benefit in regards to modular software is organization. Modules provide a natural separation point. Along with that, as we’ll see soon, modules prevent you from polluting the global namespace and allow you to avoid naming collisions.

At this point, you know the benefits and understand the structure of modules. Now it’s time to actually start building them. Our approach to this will be pretty methodical. The reason for that is because, as mentioned earlier, modules in JavaScript have a strange history. Even though there are “newer” ways to create modules in JavaScript, some of the older flavors still exist and you’ll see them from time to time. If we jump straight to modules in 2018, I’d be doing you a disservice. With that said, we’re going to take it back to late 2010. AngularJS was just released and jQuery is all the rage. Companies are finally using JavaScript to build complex web applications and with that complexity comes a need to manage it - via modules.

Your first intuition for creating modules may be to separate code by files.

// users.js
var users = ["Tyler", "Sarah", "Dan"]

function getUsers() {
  return users
}
// dom.js

function addUserToDOM(name) {
  const node = document.createElement("li")
  const text = document.createTextNode(name)
  node.appendChild(text)

  document.getElementById("users")
    .appendChild(node)
}

document.getElementById("submit")
  .addEventListener("click", function() {
    var input = document.getElementById("input")
    addUserToDOM(input.value)

    input.value = ""
})

var users = window.getUsers()
for (var i = 0; i < users.length; i++) {
  addUserToDOM(users[i])
}
<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <title>Users</title>
  </head>

  <body>
    <h1>Users</h1>
    <ul id="users"></ul>
    <input
      id="input"
      type="text"
      placeholder="New User">
    </input>
    <button id="submit">Submit</button>

    <script src="users.js"></script>
    <script src="dom.js"></script>
  </body>
</html>

The full code can be found here .

OK. We’ve successfully separated our app into its own files. Does that mean we’ve successfully implemented modules? No. Absolutely not. Literally, all we’ve done is separate where the code lives. The only way to create a new scope in JavaScript is with a function. All the variables we declared that aren’t in a function are just living on the global object. You can see this by logging the window object in the console. You’ll notice we can access, and worse, change addUsers , users , getUsers , addUserToDOM . That’s essentially our entire app. We’ve done nothing to separate our code into modules, all we’ve done is separate it by physical location. If you’re new to JavaScript, this may be a surprise to you but it was probably your first intuition for how to implement modules in JavaScript.

So if file separation doesn’t give us modules, what does? Remember the advantages to modules - reusability, composability, leverage, isolation, organization. Is there a native feature of JavaScript we could use to create our own “modules” that would give us the same benefits? What about a regular old function? When you think of the benefits of a function, they align nicely to the benefits of modules. So how would this work? What if instead of having our entire app live in the global namespace, we instead expose a single object, we’ll call it APP . We can then put all the methods our app needs to run under the APP , which will prevent us from polluting the global namespace. We could then wrap everything else in a function to keep it enclosed from the rest of the app.

// App.js
var APP = {}
// users.js
function usersWrapper () {
  var users = ["Tyler", "Sarah", "Dan"]

  function getUsers() {
    return users
  }

  APP.getUsers = getUsers
}

usersWrapper()
// dom.js

function domWrapper() {
  function addUserToDOM(name) {
    const node = document.createElement("li")
    const text = document.createTextNode(name)
    node.appendChild(text)

    document.getElementById("users")
      .appendChild(node)
  }

  document.getElementById("submit")
    .addEventListener("click", function() {
      var input = document.getElementById("input")
      addUserToDOM(input.value)

      input.value = ""
  })

  var users = APP.getUsers()
  for (var i = 0; i < users.length; i++) {
    addUserToDOM(users[i])
  }
}

domWrapper()
<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <title>Users</title>
  </head>

  <body>
    <h1>Users</h1>
    <ul id="users"></ul>
    <input
      id="input"
      type="text"
      placeholder="New User">
    </input>
    <button id="submit">Submit</button>

    <script src="app.js"></script>    <script src="users.js"></script>
    <script src="dom.js"></script>
  </body>
</html>

The full code can be found here .

Now if you look at the window object, instead of it having all the important pieces of our app, it just has APP and our wrapper functions, usersWrapper and domWrapper . More important, none of our important code (like users ) can be modified since they’re no longer on the global namespace.

Let’s see if we can take this a step further. Is there a way to get rid of our wrapper functions? Notice that we’re defining and then immediately invoking them. The only reason we gave them a name was so we could immediately invoke them. Is there a way to immediately invoke an anonymous function so we wouldn’t have to give them a name? Turns out there is and it even has a fancy name - Immediately Invoked Function Expression or IIFE for short.

IIFE

Here’s what it looks like.

(function () {
  console.log('Pronounced IF-EE')
})()

Notice it’s just an anonymous function expression that we’ve wrapped in parens ().

(function () {
  console.log('Pronounced IF-EE')
})

Then, just like any other function, in order to invoke it, we add another pair of parens to the end of it.

(function () {
  console.log('Pronounced IF-EE')
})()

Now let’s use our knowledge of IIFEs to get rid of our ugly wrapper functions and clean up the global namespace even more.

// users.js

(function () {
  var users = ["Tyler", "Sarah", "Dan"]

  function getUsers() {
    return users
  }

  APP.getUsers = getUsers
})()
// dom.js

(function () {
  function addUserToDOM(name) {
    const node = document.createElement("li")
    const text = document.createTextNode(name)
    node.appendChild(text)

    document.getElementById("users")
      .appendChild(node)
  }

  document.getElementById("submit")
    .addEventListener("click", function() {
      var input = document.getElementById("input")
      addUserToDOM(input.value)

      input.value = ""
  })

  var users = APP.getUsers()
  for (var i = 0; i < users.length; i++) {
    addUserToDOM(users[i])
  }
})()

The full code can be found here .

chef’s kiss . Now if you look at the window object, you’ll notice the only thing we’ve added to it is APP , which we use as a namespace for all the methods our app needs to properly run.

Let’s call this pattern the IIFE Module Pattern .

What are the benefits to the IIFE Module Pattern? First and foremost, we avoid dumping everything onto the global namespace. This will help with variable collisions and keeps our code more private. Does it have any downsides? It sure does. We still have 1 item on the global namespace, APP . If by chance another library uses that same namespace, we’re in trouble. Second, you’ll notice the order of the <script> tags in our index.html file matter. If you don’t have the scripts in the exact order they are now, the app will break.

Even though our solution isn’t perfect, we’re making progress. Now that we understand the pros and cons to the IIFE module pattern, if we were to make our own standard for creating and managing modules, what features would it have?

Earlier our first instinct for the separation of modules was to have a new module for each file. Even though that doesn’t work out of the box with JavaScript, I think that’s an obvious separation point for our modules. Each file is its own module. Then from there, the only other feature we’d need is to have each file define explicit imports (or dependencies) and explicit exports which will be available to any other file that imports the module.

Our Module Standard

1) File based
2) Explicit imports
3) Explicit exports

Now that we know the features our module standard will need, let’s dive into the API. The only real API we need to define is what imports and exports look like. Let’s start with exports. To keep things simple, any information regarding the module can go on the module object. Then, anything we want to export from a module we can stick on module.exports . Something like this

var users = ["Tyler", "Sarah", "Dan"]

function getUsers() {
  return users
}

module.exports.getUsers = getUsers

This means another way we can write it is like this

var users = ["Tyler", "Sarah", "Dan"]

function getUsers() {
  return users
}

module.exports = {
  getUsers: getUsers
}

Regardless of how many methods we had, we could just add them to the exports object.

// users.js

var users = ["Tyler", "Sarah", "Dan"]

module.exports = {
  getUsers: function () {
    return users
  },
  sortUsers: function () {
    return users.sort()
  },
  firstUser: function () {
    return users[0]
  }
}

Now that we’ve figured out what exporting from a module looks like, we need to figure out what the API for importing modules looks like. To keep this one simple as well, let’s pretend we had a function called require . It’ll take a string path as its first argument and will return whatever is being exported from that path. Going along with our users.js file above, to import that module would look something like this

var users = require('./users')

users.getUsers() // ["Tyler", "Sarah", "Dan"]
users.sortUsers() // ["Dan", "Sarah", "Tyler"]
users.firstUser() // ["Tyler"]

Pretty slick. With our hypothetical module.exports and require syntax, we’ve kept all of the benefits of modules while getting rid of the two downsides to our IIFE Modules pattern.

As you probably guessed by now, this isn’t a made up standard. It’s real and it’s called CommonJS.

The CommonJS group defined a module format to solve JavaScript scope issues by making sure each module is executed in its own namespace. This is achieved by forcing modules to explicitly export those variables it wants to expose to the “universe”, and also by defining those other modules required to properly work.

  • Webpack docs

If you’ve used Node before, CommonJS should look familiar. The reason for that is because Node uses (for the most part) the CommonJS specification in order to implement modules. So with Node, you get modules out of the box using the CommonJS require and module.exports syntax you saw earlier. However, unlike Node, browsers don’t support CommonJS. In fact, not only do browsers not support CommonJS, but out of the box, CommonJS isn’t a great solution for browsers since it loads modules synchronously. In the land of the browser, the asynchronous loader is king.

So in summary, there are two problems with CommonJS. First, the browser doesn’t understand it. Second, it loads modules synchronously which in the browser would be a terrible user experience. If we can fix those two problems, we’re in good shape. So what’s the point of spending all this time talking about CommonJS if it’s not even good for browsers? Well, there is a solution and it’s called a module bundler.

Module Bundlers

What a JavaScript module bundler does is it examines your codebase, looks at all the imports and exports, then intelligently bundles all of your modules together into a single file that the browser can understand. Then instead of including all the scripts in your index.html file and worrying about what order they go in, you include the single bundle.js file the bundler creates for you.

app.js ---> |         |
users.js -> | Bundler | -> bundle.js
dom.js ---> |         |

So how does a bundler actually work? That’s a really big question and one I don’t fully understand myself, but here’s the output after running our simple code through Webpack, a popular module bundler.

The full code can with CommonJS and Webpack can be found here . You’ll need to download the code, run “npm install”, then run “webpack”.

(function(modules) { // webpackBootstrap
  // The module cache
  var installedModules = {};
  // The require function
  function __webpack_require__(moduleId) {
    // Check if module is in cache
    if(installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // Create a new module (and put it into the cache)
    var module = installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    };
    // Execute the module function
    modules[moduleId].call(
      module.exports,
      module,
      module.exports,
      __webpack_require__
    );
    // Flag the module as loaded
    module.l = true;
    // Return the exports of the module
    return module.exports;
  }
  // expose the modules object (__webpack_modules__)
  __webpack_require__.m = modules;
  // expose the module cache
  __webpack_require__.c = installedModules;
  // define getter function for harmony exports
  __webpack_require__.d = function(exports, name, getter) {
    if(!__webpack_require__.o(exports, name)) {
      Object.defineProperty(
        exports,
        name,
        { enumerable: true, get: getter }
      );
    }
  };
  // define __esModule on exports
  __webpack_require__.r = function(exports) {
    if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
      Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
    }
    Object.defineProperty(exports, '__esModule', { value: true });
  };
  // create a fake namespace object
  // mode & 1: value is a module id, require it
  // mode & 2: merge all properties of value into the ns
  // mode & 4: return value when already ns object
  // mode & 8|1: behave like require
  __webpack_require__.t = function(value, mode) {
    if(mode & 1) value = __webpack_require__(value);
    if(mode & 8) return value;
    if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
    var ns = Object.create(null);
    __webpack_require__.r(ns);
    Object.defineProperty(ns, 'default', { enumerable: true, value: value });
    if(mode & 2 && typeof value != 'string')
      for(var key in value)
        __webpack_require__.d(ns, key, function(key) {
          return value[key];
        }.bind(null, key));
    return ns;
  };
  // getDefaultExport function for compatibility with non-harmony modules
  __webpack_require__.n = function(module) {
    var getter = module && module.__esModule ?
      function getDefault() { return module['default']; } :
      function getModuleExports() { return module; };
    __webpack_require__.d(getter, 'a', getter);
    return getter;
  };
  // Object.prototype.hasOwnProperty.call
  __webpack_require__.o = function(object, property) {
      return Object.prototype.hasOwnProperty.call(object, property);
  };
  // __webpack_public_path__
  __webpack_require__.p = "";
  // Load entry module and return exports
  return __webpack_require__(__webpack_require__.s = "./dom.js");
})
/************************************************************************/
({

/***/ "./dom.js":
/*!****************!*\
  !*** ./dom.js ***!
  \****************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {

eval(`
  var getUsers = __webpack_require__(/*! ./users */ \"./users.js\").getUsers\n\n
  function addUserToDOM(name) {\n
    const node = document.createElement(\"li\")\n
    const text = document.createTextNode(name)\n
    node.appendChild(text)\n\n
    document.getElementById(\"users\")\n
      .appendChild(node)\n}\n\n
    document.getElementById(\"submit\")\n
      .addEventListener(\"click\", function() {\n
        var input = document.getElementById(\"input\")\n
        addUserToDOM(input.value)\n\n
        input.value = \"\"\n})\n\n
        var users = getUsers()\n
        for (var i = 0; i < users.length; i++) {\n
          addUserToDOM(users[i])\n
        }\n\n\n//# sourceURL=webpack:///./dom.js?`
);}),

/***/ "./users.js":
/*!******************!*\
  !*** ./users.js ***!
  \******************/
/*! no static exports found */
/***/ (function(module, exports) {

eval(`
  var users = [\"Tyler\", \"Sarah\", \"Dan\"]\n\n
  function getUsers() {\n
    return users\n}\n\nmodule.exports = {\n
      getUsers: getUsers\n
    }\n\n//# sourceURL=webpack:///./users.js?`);})
});

You’ll notice that there’s a lot of magic going on there (you can read the comments if you want to know exactly what’s happening), but one thing that’s interesting is they wrap all the code inside of a big IIFE. So they’ve figured out a way to get all of the benefits of a nice module system without the downsides, simply by utilizing our old IIFE Module Pattern.

What really future proofs JavaScript is that it’s a living language. TC-39, the standards committee around JavaScript, meets a few times a year to discuss potential improvements to the language. At this point, it should be pretty clear that modules are a critical feature for writing scalable, maintainable JavaScript. In ~2013 (and probably long before) it was dead obvious that JavaScript needed a standardized, built in solution for handling modules. This kicked off the process for implementing modules natively into JavaScript.

Knowing what you know now, if you were tasked with creating a module system for JavaScript, what would it look like? CommonJS got it mostly right. Like CommonJS, each file could be a new module with a clear way to define imports and exports - obviously, that’s the whole point. A problem we ran into with CommonJS is it loads modules synchronously. That’s great for the server but not for the browser. One change we could make would be to support asynchronous loading. Another change we could make is rather than a require function call, since we’re talking about adding to the language itself, we could define new keywords. Let’s go with import and export .

Without going too far down the “hypothetical, made up standard” road again, the TC-39 committee came up with these exact same design decisions when they created “ES Modules”, now the standardized way to create modules in JavaScript. Let’s take a look at the syntax.

ES Modules

As mentioned above, to specify what should be exported from a module you use the export keyword.

// utils.js

// Not exported
function once(fn, context) {
  var result
  return function() {
    if(fn) {
      result = fn.apply(context || this, arguments)
      fn = null
    }
    return result
  }
}

// Exported
export function first (arr) {
  return arr[0]
}

// Exported
export function last (arr) {
  return arr[arr.length - 1]
}

Now to import first and last , you have a few different options. One is to import everything that is being exported from utils.js .

import * as utils from './utils'

utils.first([1,2,3]) // 1
utils.last([1,2,3]) // 3

But what if we didn’t want to import everything the module is exporting? In this example, what if we wanted to import first but not last ? This is where you can use what’s called named imports (it looks like destructuring but it’s not).

import { first } from './utils'

first([1,2,3]) // 1

What’s cool about ES Modules is not only can you specify multiple exports, but you can also specify a default export.

// leftpad.js

export default function leftpad (str, len, ch) {
  var pad = '';
  while (true) {
    if (len & 1) pad += ch;
    len >>= 1;
    else break;
  }
  return pad + str;
}

When you use a default export, that changes how you import that module. Instead of using the * syntax or using named imports, you just use import name from './path' .

import leftpad from './leftpad'

Now, what if you had a module that was exporting a default export but also other regular exports as well? Well, you’d do it how you’d expect.

// utils.js

function once(fn, context) {
  var result
  return function() {
    if(fn) {
      result = fn.apply(context || this, arguments)
      fn = null
    }
    return result
  }
}

// regular export
export function first (arr) {
  return arr[0]
}

// regular export
export function last (arr) {
  return arr[arr.length - 1]
}

// default export
export default function leftpad (str, len, ch) {
  var pad = '';
  while (true) {
    if (len & 1) pad += ch;
    len >>= 1;
    else break;
  }
  return pad + str;
}

Now, what would the import syntax look like? In this case, again, it should be what you expect.

import leftpad, { first, last } from './utils'

Pretty slick, yeah? leftpad is the default export and first and last are just the regular exports.

What’s interesting about ES Modules is, because they’re now native to JavaScript, modern browsers support them without using a bundler. Let’s look back at our simple Users example from the beginning of this tutorial and see what it would look like with ES Modules.

The full code can be found here .

// users.js

var users = ["Tyler", "Sarah", "Dan"]

export default function getUsers() {
  return users
}
// dom.js

import getUsers from './users.js'

function addUserToDOM(name) {
  const node = document.createElement("li")
  const text = document.createTextNode(name)
  node.appendChild(text)

  document.getElementById("users")
    .appendChild(node)
}

document.getElementById("submit")
  .addEventListener("click", function() {
    var input = document.getElementById("input")
    addUserToDOM(input.value)

    input.value = ""
})

var users = getUsers()
for (var i = 0; i < users.length; i++) {
  addUserToDOM(users[i])
}

Now here’s the cool part. With our IIFE pattern, we still needed to include a script to every JS file (and in order, none the less). With CommonJS we needed to use a bundler like Webpack and then include a script to the bundle.js file. With ES Modules, in modern browsers, all we need to do is include our main file (in this case dom.js ) and add a type='module' attribute to the script tab.

<!DOCTYPE html>
<html>
  <head>
    <title>Users</title>
  </head>

  <body>
    <h1>Users</h1>
    <ul id="users">
    </ul>
    <input id="input" type="text" placeholder="New User"></input>
    <button id="submit">Submit</button>

    <script type=module src='dom.js'></script>  </body>
</html>

Tree Shaking

There’s one more difference between CommonJS modules and ES Modules that we didn’t cover above.

With CommonJS, you can require a module anywhere, even conditionally.

if (pastTheFold === true) {
  require('./parallax')
}

Because ES Modules are static, import statements must always be at the top level of a module. You can’t conditionally import them.

if (pastTheFold === true) {
  import './parallax' // "import' and 'export' may only appear at the top level"
}

The reason this design decision was made was because by forcing modules to be static, the loader can statically analyze the module tree, figure out which code is actually being used, and drop the unused code from your bundle. That was a lot of big words. Said differently, because ES Modules force you to declare your import statements at the top of your module, the bundler can quickly understand your dependency tree. When it understands your dependency tree, it can see what code isn’t being used and drop it from the bundle. This is called Tree Shaking or Dead Code Elimination.

There is a stage 4 proposal for dynamic imports which will allow you to conditionally load modules via import().

I hope diving into the history of JavaScript modules has helped you gain not only a better appreciation for ES Modules, but also a better understanding of their design decisions. For a deeper dive into ES Modules specifically, visit ES Modules in Depth.

(Bonus) ES Modules In Depth

Modules in JavaScript are much more straightforward since ES Modules were added to the specification. Modules are separated by file and loaded asynchronously. Exports are defined using the export keyword; values can be imported with the import keyword.

While the basics of importing and exporting individual values is pretty easy to grasp and use, there are many other ways to work with ES Modules to make your imports and exports work the way you need them to. In this lesson, we’ll go over all of the ways you can export and import within your modules.

One thing to remember is that exports and static imports can only happen at the top level of the module. You cannot export or statically import from within a function, if statement, or any other block. Dynamic imports, on the other hand, can be done from within a function; we’ll talk about those at the end of the lesson.

Exports

Default Export

Every module has a single “default” export, which represents the main value which is exported from the module. There might be more things exported, but the default export is what defines the module. You can only have one default export in a module.

const fruitBasket = new FruitBasket();

export default fruitBasket;

Notice that I have to first define the value before adding it to my default export. If I wanted to, I could export my value immediately, without assigning it to a variable. But I cannot assign it to a variable at the same time as exporting it.

We can export a function declaration and a class declaration by default without first assigning it to a variable.

export default function addToFruitBasket(fruit) {
  // ... implementation goes here
}

We can even export literal values as the default export.

export default 123;

Named Export

Any variable declaration can be exported when it is created. This creates a “Named Export” using the variable name as the export name.

export const fruitBasket = new FruitBasket();

We can also immediately export function and class declarations.

export function addToFruitBasket(fruit) {
  // ... implementation goes here
}
export class FruitBasket {
  // ... implementation goes here
}

If we wanted to export a variable which was already defined, we could do that by wrapping the variable in curly brackets around our variable name.

const fruitBasket = new FruitBasket();

export { fruitBasket };

We can even use the as keyword to rename our export to be different from the variable name. We can export other variables at the same time, if we wanted.

const fruitBasket = new FruitBasket();
class Apple {}

export { fruitBasket as basketOfFruit, Apple };

Aggregate Exports

One thing that is common is importing modules from one module and then immediately exporting those values. It looks something like this.

import fruitBasket from "./fruitBasket.js";

export { fruitBasket };

This can get tedious when you are importing and exporting lots of things at the same time. ES Modules allows us to import and export multiple values at the same time.

export * from "./fruitBasket.js";

This will take all of the named exports of ./fruitBasket.js and re-export them. It won’t re-export default exports though, since a module can only have one default export. If we were to import and export multiple modules with default exports, which value would become the default export for the exporting module?

We can specifically export default modules from other files, or name the default export when we re-export it.

export { default } from "./fruitBasket.js";

// or

export { default as fruitBasket } from "./fruitBasket.js";

We can selectively export different items from another module as well, instead of re-exporting everything. We use curly brackets in this case as well.

export { fruitBasket as basketOfFruit, Apple } from "./fruitBasket.js";

Finally, we can wrap up an entire module into a single named export using the as keyword. Suppose we have the following file.

// fruits.js
export class Apple {}
export class Banana {}

We can now pack this into a single export which is an object containing all of the named and default exports.

export * as fruits from "./fruits.js"; // { Apple: class Apple, Banana: class Banana }

Imports

Default Imports

When importing a default value, we need to assign a name to it. Since it is the default, it doesn’t matter what we name it.

import fruitBasketList from "./fruitBasket.js";

We can also import all of the exports, including named and default exports, at the same time. This will put all of them exports into an object, and the default export will be given the property name “default”.

import * as fruitBasket from "./fruitBasket.js"; // { default: fruitBasket }

Named Imports

We can import any named export by wrapping the exported name in curly brackets.

import { fruitBasket, Apple } from "./fruitBasket.js";

We can also rename the import as we import it using the as keyword.

import {fruitBasket as basketOfFruit, Apple} from './fruitBasket.js`

We can also mix named and default exports in the same import statement. The default export is listed first, followed by the named exports in curly brackets.

import fruitBasket, { Apple } from "./fruitBasket.js";

Finally, we can import a module without listing any of the exports we want to use in our file. This is called a ‘side-effect’ import, and will execute the code in the module without providing us any exported values.

import "./fruitBasket.js";

Dynamic Imports

Sometimes we don’t know the name of a file before we import it. Or we don’t need to import a file until we are partway through executing code. We can use a dynamic import to import modules anywhere in our code. It’s called “dynamic” because we could use any string value as the path to the import, not just a string literal.

Since ES Modules are asynchronous, the module won’t immediately be available. We have to wait for it to be loaded before we can do anything with it. Because of this, dynamic imports return a promise which resolves to our module.

If our module can’t be found, the dynamic import will throw an error.

async function createFruit(fruitName) {
  try {
    const FruitClass = await import(`./${fruitName}.js`);
  } catch {
    console.error("Error getting fruit class module:", fruitName);
  }
  return new FruitClass();
}

Modules in TypeScript

Exporting Types

Sometimes we define a type in one file that we need to reference in another file. We can export and import them like any other value, even alongside normal values.

// ./fruitBasket.ts
export class Fruit {}
export type FruitBasket = Fruit[];

export const fruit: FruitBasket = [];

We can then import and use them in other files.

// ./main.ts
import { FruitBasket, Fruit } from "./fruitBasket.ts";

export function addToBasket(basket: FruitBasket) {
  basket.push(new Fruit());
}

Remember, type definitions are removed entirely from our code when we compile TypeScript to JavaScript. These types aren’t actually being imported at runtime; they only exist before the code is compiled.

Sometimes we need to import a type, but we don’t want to execute any code from the file we are importing them from. One solution would be to move the types out into a separate file. Another solution is indicating to TypeScript that the only things we are importing from a file are types. We can do that with the type keyword.

// ./main.ts
import type { FruitBasket, Fruit } from "./fruitBasket.ts";

This indicates that FruitBasket and Fruit only represent types, not values. Classes are an interesting case; they represent both types and values. If we were to try to instantiate Fruit after only importing the type, TypeScript would warn us that we can’t do that.

// ./main.ts
import type { FruitBasket, Fruit } from "./fruitBasket.ts";

new Fruit(); // Type Error: 'Fruit' cannot be used as a value because it was imported using 'import type'.

ES Modules vs CommonJS

TypeScript is a compiler in addition to being a type checker. That means you can feed it any valid JavaScript code and it can transform it into other JavaScript code.

One place where this becomes important is in the module system. There are significant differences between CommonJS, module loaders for AMD modules, and ES Modules, and those differences are most obvious when we have the esModuleInterop tsconfig.json flag disabled.

Take a look at the following example using both ES Modules and CommonJS. All of these examples assume the esModuleInterop flag is turned off.

// fruitBasket.js
export default new FruitBasket();

// main.js
import * as fruitBasketModule from "./fruitBasket.js";
import fruitBasketDefault from "./fruitBasket.js";
const fruitBasketRequire = require("./fruitBasket.js");

console.log(fruitBasketModule); // { default: [Function: FruitBasket] }
console.log(fruitBasketDefault); // [Function: FruitBasket]
console.log(fruitBasketRequire); // { default: [Function: FruitBasket] }

We can see that when the export is using ES Modules, using a require statement behaves more like the import * as type of import statement. If we wanted to access the default export using require , we would have to use property access to get the default property off of the fruitBasketRequire variable.

What happens when we export using CommonJS?

// fruitBasket.js
module.exports = new FruitBasket();

// main.js
import * as fruitBasketModule from "./fruitBasket.js"; // Error: File './fruitBasket.ts' is not a module.
import fruitBasketDefault from "./fruitBasket.js"; // Error: File './fruitBasket.ts' is not a module.
const fruitBasketRequire = require("./fruitBasket.js");

console.log(fruitBasketRequire); // [Function: FruitBasket]

The TypeScript compiler won’t even let us try to import these modules using ES Modules syntax. It a file doesn’t use an import or export statement, it doesn’t count as a module, and you have to use require to pull in the file’s contents. Fortunately, it behaves the way you would expect for CommonJS - instead of putting the export onto the default property, it provides it to us directly.

TypeScript has it’s own module syntax which is unique to TypeScript. It’s designed to model the traditional CommonJS workflow, while being compatible with ES Module syntax.

Exporting modules is similar to CommonJS, except instead of using the special module.exports object, we assign our exported values to a special export object.

// fruitBasket.js
export = new FruitBasket()

We can then import it using a special import ... = require(...) syntax. It looks kind of like CommonJS mixed with ES Modules.

// main.js
import FruitBasket = require('./fruitBasket.js')

console.log(FruitBasket) // [Function: FruitBasket]

Of course, TypeScript helps us with this incompatibility when we turn on the esModuleInterop tsconfig.json flag. Be aware that the export = and import = syntax is TypeScript specific. It isn’t supported if you are using TypeScript with many third-party tools, like Babel. I would recommend avoiding it if possible. Using the esModuleInterop flag will cover most cases. Try to only use ES Modules for all of the files you write in your program. If you have to import CommonJS code, use CommonJS syntax or TypeScript’s special syntax.

(Bonus) TypeScript Namespaces

Before ES Modules was standardized and included in JavaScript, TypeScript had its own form of code organization called namespaces. Each namespace that you create exist in the global environment, similar to IIFE modules. That makes it so you can access anything in any namespace from any other file in your project, but makes it difficult to examine what dependencies a specific file has.

You might find some situations where using namespaces could be valuable, such as creating type definitions for a third-party module. Or you might be working with a project that already uses namespaces. In most circumstances, though, you should use modules instead of namespaces. Modules are more compatible with tools like Babel and Node.js. ES Modules are well supported and provide for better code reuse and stronger isolation. They can also be statically analyzed, making it easier to work with bundlers like Webpack.

In short, namespaces are supported by TypeScript and have a few uses, but you should avoid using them in favor of modules for any new projects. This section on namespaces is included in case you encounter them in any TypeScript codebases.

Using Namespaces

Namespaces are blocks which we create with the namespace keyword. Anything inside the block is considered part of the namespace. If we want something inside our namespace to be accessible outside our namespace, we use the export keyword to its declaration. We can export types and values from within namespaces.

namespace FruitBasket {
  export abstract class Fruit {
    name!: string;
  }
  export type FruitBasket = Fruit[];

  const fruitBasket: FruitBasket = [];

  export function addToBasket(fruit: Fruit) {
    fruitBasket.push(fruit);
  }
  export function eat() {
    return fruitBasket.pop();
  }
  export function nextFruit() {
    return fruitBasket[fruitBasket.length - 1];
  }
}

Here, we’re exporting the abstract Fruit class which we can extend, the FruitBasket type, and a few functions. Those are the only things which can be accessed from outside of our namespace.

If this sounds familiar, you might be interested to know that namespaces are implemented as IIFEs when they are compiled to JavaScript.

var FruitBasket;
(function (FruitBasket) {
  class Fruit {}
  FruitBasket.Fruit = Fruit;
  const fruitBasket = [];
  function addToBasket(fruit) {
    fruitBasket.push(fruit);
  }
  FruitBasket.addToBasket = addToBasket;
  function eat() {
    return fruitBasket.pop();
  }
  FruitBasket.eat = eat;
  function nextFruit() {
    return fruitBasket[fruitBasket.length - 1];
  }
  FruitBasket.nextFruit = nextFruit;
})(FruitBasket || (FruitBasket = {}));

The var FruitBasket; declaration assigns our FruitBasket namespace to the global scope, and the function closure makes it so nothing is exposed globally except the things we want. Anything with an export keyword is added to the FruitBasket global object, such as FruitBasket.nextFruit = nextFruit; , making it accessible from outside.

Multiple Files

While we can access namespaces globally, the order that these files are loaded and executed makes a differences. Fortunately, we can create a dependency graph of our files using a reference tag. This is a comment added to the top of our TypeScript file that tells TypeScript that we’re referencing another file. Notice below that when making the comment, we use three slashes ( / ) instead of two.

In this example, we extend our namespace with additional properties in a separate file. We use the reference tag to tell the TypeScript compiler that the fruitBasket.ts file needs to be loaded first.

// ./fruitBasket.ts
namespace FruitBasket {
  export abstract class Fruit {
    name!: string;
  }
  export type FruitBasket = Fruit[];

  const fruitBasket: FruitBasket = [];
}

// ./fruitBasketFunctions.ts

/// <reference path="fruitBasket.ts">
namespace FruitBasket {
  export function addToBasket(fruit: Fruit) {
    fruitBasket.push(fruit);
  }
  export function eat() {
    return fruitBasket.pop();
  }
  export function nextFruit() {
    return fruitBasket[fruitBasket.length - 1];
  }
}

By default, the TypeScript compiler will emit a single file for each file in our project. That means we need to add a separate <script> tag for each file, and make sure they are imported in the correct order.

// index.html
<html>
  <head>
    <!-- ... -->
  </head>
  <body>
    <!-- ... -->
    <script src="fruitBasket.js" type="text/javascript"></script>
    <script src="fruitBasketFunctions.js" type="text/javascript"></script>
  </body>
</html>

Depending on which module system we are using, we can configure TypeScript to emit a single file with the namespace declarations in the correct order using the outFile tsconfig.json setting. We’ll talk more about that when we talk about advanced TypeScript configuration.

Built-In Type Definitions

It would be really tedious to have to write type definitions for every built-in JavaScript object. Fortunately, TypeScript comes with its own set of definitions for every feature in JavaScript, called the standard library. We can use any of the functions in JavaScript’s standard library without worrying about doing something unsafe. TypeScript version updates include updates to these definitions, so you can confidently use new features too.

Math.max; // (method) Math.max(...values: number[]): number
isNaN; // function isNaN(number: number): boolean

new Date().toLocaleString; // (method) Date.toLocaleString(): string (+1 overload)

You can use type definitions from the standard library too. For example, all of TypeScript’s utility types are included in the standard library, so we don’t have to define them ourselves.

type ReadonlyFruit = Readonly<{ name: string; color: string }>;
// type ReadonlyFruit {
//   readonly: name: string;
//   readonly: color: string;
// }

TSConfig.json lib property

The default configuration for TypeScript includes definitions for ES5 and DOM APIs, such as those that exist in web browser like document.getElementById and HTMLElement . We can configure which standard library packages we want to include by modifying the lib property of our tsconfig.json file. This property takes an array of strings representing different features or packs of features.

For example, if we wanted to use ES2020 features, we could use put lib: ["ES2020"] in our tsconfig.json. This would automatically include all of the previous versions, so we don’t have to list them all. In fact, we can put any version of JavaScript, from ES2015 to the current version, or use ESNext to get a continually updated set of new JavaScript features.

If we manually change the lib property of tsconfig.json, we’ll have to include the DOM API standard library too. We just need to include it in the array, like so: `lib: [“ESNext”,“DOM”].

We can also choose specific standard library components, such as ES2015.Proxy to get support for proxy syntax. However, it’s usually best to choose the specific version of JavaScript we want to have the type definitions for.

TypeScript doesn’t ship with support for Node.js APIs, but we’ll talk about how we can add support in a future lesson.

(Bonus) Outputting TypeScript Definitions

When you are building an application with TypeScript, you likely don’t care about the type definitions after you’ve compiled your project to JavaScript. At that point, all you want to do is run your code.

If you are building a JavaScript library that others consume, though, you might want to hang on to those type definitions. The only problem is the TypeScript compiler strips out all of the type signatures, leaving you with JavaScript. Fortunately, there is an option which you can use to have TypeScript output separate files, called declaration files, which you can include with your JavaScript library. Then, any TypeScript users who use your library will still be able take advantage of the types.

If we set the declaration property in our tsconfig.json file to true , TypeScript will output a type declaration file for all of our files alongside the files themselves. Let’s use a slightly modified version of the TypeScript program we used earlier.

// index.ts
class Fruit {
  constructor(public name: string, public color?: string) {}
}
export class FruitBasket {
  static maxFruit: number = 5;
  #fruitList: Fruit[] = [];
  addFruit(fruit: Fruit) {
    // Throw away green fruit
    if (fruit?.color === "green") return;
    if (this.#fruitList.length < FruitBasket.maxFruit) {
      this.#fruitList.push(fruit);
    }
  }
  eat() {
    this.#fruitList.pop();
  }
}

Running this through the TypeScript compiler will create an index.js file and an index.d.ts file. The “d” in the extension stands for “definition”. .d.ts files only include type definitions and declarations, but no values or executable code. Here’s what the TypeScript definition file looks like for the example above.

// index.d.ts
declare class Fruit {
  name: string;
  color?: string;
  constructor(name: string, color?: string);
}
export declare class FruitBasket {
  #private;
  static maxFruit: number;
  addFruit(fruit: Fruit): void;
  eat(): void;
}
export {};

Looking closely at this, it almost seems like the opposite of our JavaScript output. Instead of stripping out the types, TypeScript removed the JavaScript, leaving us with just type definitions. If we were to try to import and instantiate these classes, TypeScript would complain and tell us that these classes only represent types, not values.

There are two interesting things to notice here. The first is the absence of the #fruitList property. Since that’s marked as an ES Private field, we won’t be able to access that property anyway. TypeScript doesn’t even include it in the type definition.

The second is the new declare keyword. This only exists in TypeScript, and is used any time we are creating a type definition for something that exists in a JavaScript file. This is covered in depth in the bonus section on typing third-party modules.

Once we’ve generated our declaration files, we can include them in our library bundle by adding a “types” field to our package.json file. It should include a path to the type definition file relative to where package.json is located.

// package.json
{
  //...
  "types": "./build/index.d.ts"
}

One of the best ways to understand how TypeScript interprets type declaration files is by looking at the output of compiled TypeScript. You can easily do this by pasting TypeScript code into the TypeScript Playground and choosing the ‘.D.TS’ tab on the output window.

Definitely Typed and @types/ packages

Third-party packages are a big part of why working with JavaScript is so great. NPM has a huge ecosystem of packages which we can use in Node.js projects or on the web. While some of these packages are written in TypeScript and include TypeScript definitions, most of them are still written in JavaScript. That means we can’t use them in our project without decreasing our type safety, since we might accidentally use an API wrong, or might use a function return value in the wrong way. In addition, we won’t be able to take advantage of the IDE enhancements which TypeScript provides, which can be really helpful for figuring out how to use an API.

Fortunately, there is a huge repository of user-submitted types for over 7,000 NPM packages. It’s called Definitely Typed, and it publishes its type definitions on NPM using the @types/ namespace. Adding types to our project is as easy as running npm install .

One of the most used type definitions is the one for Node.js. If you want to use any of Node.js’s APIs, like fs or path , you’ll want to install these type definitions.

npm install --save-dev @types/node

That’s all you have to do! You can now take advantage of all of Node.js’s features without needing to worry if you are using the types correctly - TypeScript will pick up the definition you installed and warn you if there is a problem.

If we have strict mode on, or if we are using the noImplicitAny flag in tsconfig.json, TypeScript will warn you if a package doesn’t have type definitions when you try to import it.

import React from "react";
// Type Error: Could not find a declaration file for module 'react'. 'node_modules/react/index.js' implicitly has an 'any' type.
//   Try `npm install @types/react` if it exists or add a new declaration (.d.ts) file containing `declare module 'react';`

Installing the @types/react package with NPM resolves the issue.

Some packages don’t have definitions on DefinitelyTyped, which means running npm install @types/somePackage will fail. In that case, you will have to create the type definition yourself. That’s fairly uncommon, since the most popular libraries on NPM have type definitions on DefinitelyTyped. If you still need to create a new definition file for a third-party module, check out the bonus lesson.

In the event that you need to use a package with no type definitions, you can create a simple shim which will let TypeScript recognize the module, but make the entire module any . This works by creating a .d.ts file inside your project folder. The file needs to have the same name as the package you are importing. Then, inside that file, use the declare keyword to tell typescript about your module:

//fruit-basket.d.ts
declare module "fruit-basket";

Now, anywhere that we import the fruit-basket module, we will get a value with the any type. As always, when we use any , we lose all of the type safety which comes with TypeScript, but when you’ve got deadlines to meet and don’t have the time or attention to add types to a third-party package, this will work in a pinch.

Overriding Built in Definitions

Sometimes we might need to override a built in type definition, such as adding something to the window global. We can do this with a special type declaration that extends the global namespace. This declaration should happen inside our application code, not in a .d.ts file.

declare global {
  interface Window {
    fruitList: Fruit[];
  }
}

The interface that we define on the Window object (note the capital W ) will be combined with the built in definition, and then anywhere in our code that we reference window.fruitList , we’ll have the correct type definitions.