Use 'declare global' to allow types to cross module boundaries

Let's talk about globals in TypeScript. Globals in TypeScript can be manipulated and used in really interesting ways.

Here we have a global reducer.

export const todosReducer: GlobalReducer<{ todos: { id: string }[] }> = (
  state,
  event
) => {
  return state
}

And this global reducer has representation of state. We take in a state and have an event, and then we return the state, which is a typical pattern for a reducer.

Problem is, our event is typed as never. Let's fix that. We can access TypeScript's global scope using the declare global syntax. We'll use that then create a GlobalReducerEvent interface.

declare global { interface GlobalReducerEvent {} } 

In it, we'll create some events

Let's say we have an add to-do event, where we can pass text with the string type.

declare global {
  interface GlobalReducerEvent {
    ADD_TODO: { text: string }
  }
}

Now we can access that event in our global reducer.

export const todosReducer: GlobalReducer<{ todos: { id: string }[] }> = (
  state,
  event
) => {
  // event.type = 'ADD_TODO'
  return state
}

So we've also got another reducer called the user reducer, which is another global reducer. This reducer also now receives the add to-do event type since we used declare global.

If we want to do the same thing here, then we would just copy and paste the declare global code over. and we would add some more events to the global scope.

For instance, we might have a log in function and need to support a log-in event, which all reducers can receive. That's usually how it works in something like Redux.

declare global {
  interface GlobalReducerEvent {
    LOG_IN: { ... }
  }
}

export const userReducer: GlobalReducer<{ id: string }> = (
  state,
  event
) => {
  // event.type === 'LOG_IN'
  // event.type === 'ADD_TODO'
  return state
}

The way that this works is we have a types file. And in it, we declare an empty interface, GlobalReducerEvent. And then, there's a bit of a clever mapping type, which maps over all of the log in stuff, and then turns it into a union type.

declare global {
  interface GlobalReducerEvent {
    LOG_IN: {}
  }
}

export type GlobalReducer<TState> = (
  state: TState,
  event: {
    [EventType in keyof GlobalReducerEvent]: {
      type: EventType
    } & GlobalReducerEvent[EventType]
  }[keyof GlobalReducerEvent]
) => TState

So here we're just saying EventType in keyof GlobalReducerEvent. And in that mapping, set the type to EventType. And then, we grab anything the user puts in their reducer events, and we append it to or intersect it with the event as well.

And this means that you can use globals. in really cool ways across your application.

Transcript

0:00 Let's talk about globals in TypeScript, because globals in TypeScript can be manipulated and used in really interesting ways. Here, we have a global reducer, and this global reducer, it's got its state here, or its representation of state.

0:15 In fact, this is supposed to be an array of to-dos, because it's to-dos, ID, string, array. Here, we take in a state, we have an event, and then we return the state, which is a typical pattern for a reducer, except our event is typed as never, so let's fix that.

0:31 We can say declare global, which is a way of accessing TypeScript's global scope, and we're going to say interface global reducer event. We're going to pass in some events here. Let's say we have an add to-do event, where we can pass in an ID of string, for instance, to-do, for instance, or text string. That maybe makes more sense.

0:53 Now, our event here has add to-do in it, and we can actually access it saying event.type equals add to-do. We've also got another reducer called the user reducer, which is another global reducer. This also receives now add to-do. If we wanted to do the same here, then we would just copy and paste this over, and we would add some more events to the global scope.

1:19 Now, for instance, if we have a used Lexa or something like that -- so in here, we might have a login function -- and now, we have a login event, which all reducers can receive. That's usually how works in something like Redux. Now, the way that this works is we have a types file in here.

1:38 In here, we just declare an empty interface, global reducer event here. Here, there's a bit of a clever mapping type, which maps over all of the login stuff here, and then turns it not a union type. Here, we're just saying key of global reducer event, so here are the keys.

1:56 We grab the type in there, and then we grab anything the user puts in here, and we append it to or intersect it with the event as well. This means that you can use globals in really cool ways across your application.

Globals in TypeScript?! 🤯

declare global is a super useful tool for when you want to allow types to cross module boundaries. Here, we create a GlobalReducer type, where you can add new event types as you create new reducers.

Discuss on Twitter

More Tips

Play Type Predicates

Type Predicates

1 min

Play TypeScript 5.1 Beta is OUT!

TypeScript 5.1 Beta is OUT!

2 mins

Play How to Name your Types

How to Name your Types

4 mins

Play Don't use return types, unless...

Don't use return types, unless...

4 mins

Play TypeScript 5.0 Beta Deep Dive

TypeScript 5.0 Beta Deep Dive

6 mins

Play Conform a Derived Type Without Losing Its Literal Values

Conform a Derived Type Without Losing Its Literal Values

1 min

Play Avoid unexpected behavior of React’s useState

Avoid unexpected behavior of React’s useState

1 min

Play Understand assignability in TypeScript

Understand assignability in TypeScript

2 mins

Play Compare function overloads and generics

Compare function overloads and generics

1 min

Play Use infer in combination with string literals to manipulate keys of objects

Use infer in combination with string literals to manipulate keys of objects

1 min

Play Access deeper parts of objects and arrays

Access deeper parts of objects and arrays

1 min

Play Ensure that all call sites must be given value

Ensure that all call sites must be given value

1 min

Play Understand how TypeScript infers literal types

Understand how TypeScript infers literal types

1 min

Play Get a TypeScript package ready for release to NPM in under 2 minutes

Get a TypeScript package ready for release to NPM in under 2 minutes

1 min

Play Use assertion functions inside classes

Use assertion functions inside classes

1 min

Play Assign local variables to default generic slots to dry up your code and improve performance

Assign local variables to default generic slots to dry up your code and improve performance

2 mins

Play Know when to use generics

Know when to use generics

2 mins

Play Map over a union type

Map over a union type

1 min

Play Make accessing objects safer by enabling 'noUncheckedIndexedAccess' in tsconfig

Make accessing objects safer by enabling 'noUncheckedIndexedAccess' in tsconfig

1 min

Play Use generics to dynamically specify the number, and type, of arguments to functions

Use generics to dynamically specify the number, and type, of arguments to functions

1 min

Play Turn a module into a type

Turn a module into a type

2 mins

Play Create autocomplete helper which allows for arbitrary values

Create autocomplete helper which allows for arbitrary values

2 mins

Play Use deep partials to help with mocking an entity

Use deep partials to help with mocking an entity

1 min

Play Throw detailed error messages for type checks

Throw detailed error messages for type checks

1 min

Play Create a 'key remover' function which can process any generic object

Create a 'key remover' function which can process any generic object

1 min

Play Use generics in React to make dynamic and flexible components

Use generics in React to make dynamic and flexible components

1 min

Play Create your own 'objectKeys' function using generics and the 'keyof' operator

Create your own 'objectKeys' function using generics and the 'keyof' operator

1 min

Play Write your own 'PropsFrom' helper to extract props from any React component

Write your own 'PropsFrom' helper to extract props from any React component

1 min

Play Use 'extends' keyword to narrow the value of a generic

Use 'extends' keyword to narrow the value of a generic

1 min

Play Use function overloads and generics to type a compose function

Use function overloads and generics to type a compose function

2 mins

Play Decode URL search params at the type level with ts-toolbelt

Decode URL search params at the type level with ts-toolbelt

2 mins

Play Use 'in' operator to transform a union to another union

Use 'in' operator to transform a union to another union

2 mins

Play Derive a union type from an object

Derive a union type from an object

2 mins