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

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