Turn a module into a type

Let's imagine that we've got a union type that looks like this.

export type Action = "ADD_TODO" | "REMOVE_TODO" | "EDIT_TODO" 

This is a common pattern in older Redux applications. Now, we've actually got a constants.ts file which has all of the elements of the union. They're even inferred by TypeScript.

The exports are not actually inferred as strings. They're inferred as the literals "ADD_TODO", "REMOVE_TODO", and "EDIT_TODO".

export const ADD_TODO = "ADD_TODO"
export const REMOVE_TODO = "REMOVE_TODO"
export const EDIT_TODO = "EDIT_TODO"

So there must be a way that we can extract this information and create the union type dynamically. Because otherwise, whenever we add a new element in constants.ts, we've also got to add it to the new place. It's just not very DRY.

So if we go to types, what we can do is we can say export type ActionModule. And we're going to use this funky little bits of TypeScript syntax.

export type ActionModule = typeof import("./constants") 

You can use any kind of normal import that you would here, and it just grabs the information from constants and creates a new type out of it. So set something to the type of ActionModule, you would see that it has all of the elements in constants.ts.

in there. So if we remove one of the exports from constants, it won't be a part of these types anymore, which is quite clever.

Then we can use the trick we learned before to create a union type out of the keys of ActionModule. So we can say this:

export type ActionModule = typeof import("./constants")

export type Action = ActionModule[keyof ActionModule]

What that does is it takes the exported keys of the ActionModule, and it sort of iterates over them. So we end up with ADD_TODO, REMOVE_TODO, and EDIT_TODO which stays in sync with our actual code.

Transcript

0:00 Hello, folks. Let's imagine that we've got a union type that looks like this, where we have add to-do, remove to-do, and edit to-do as part of the union. This is a common pattern in old Redux applications. Now, this action type here, we've actually got a constants.ts file right next to it which has all of the elements of the union.

0:23 They're even inferred by TypeScript, because we have add to-do is actually not inferred as a string, it's inferred as the literal add to-do, same as remove to-do, edit to-do. There must be a way that we can extract this information and create the union type dynamically.

0:38 Otherwise, whenever we add a new element here, we've also got to add it to the new place. It's just not very dry. If we go to types, what we can do is we can say export type, action module, and we're going to use this funky little bit of TypeScript syntax, which is type of import constants.

0:57 You can use any kind of normal import that you would here. It just grabs the information from constants and creates a new type out of it. We can say export constant action module is action module, like this, and you can see that it has all of the elements in there.

1:14 If we removed one of the exports from constants, for instance, like if we just take this off, then it won't be a part of this types anymore, which is quite clever. Now, what we want to do, then, is we want to take this action module, and we want to turn it into the action below, if I just re-add this export.

1:33 The way that we can do that is we can use a trick that we've done before, which is the take the values of an object and turn them into a union, which we just do like this. We say action module, and then key of action module. What that does is it takes the keys of the action module, the exported keys, and it iterates over them, so we end up with add to-do, remove to-do, edit to-do, that will stay in sync with our actual code.

Want to turn a module into a type? You can use typeof import('./') to grab the type of any module, even third-party ones.

Here, we create a type from a constants.ts file, then map over the values to create a union.

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 Use 'declare global' to allow types to cross module boundaries

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

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