Introducing TypeScript into your JavaScript app introduces some rigidity to your code – you lose the complete flexibility of JavaScript, but gain a lot of robustness and reliability in your code. As you learn TypeScript however, you’ll find a lot of ways to quickly build dynamic types – the Record utility type is one of those.

The Record Utility Type

TypeScript provides a utility type exactly for the purpose of defining dynamic objects, the Record type. It looks like this:


type MyObject = Record<keys, values>;

It’s generic, and takes two type parameters – one for whatever type your keys might be, and one for whatever type your values might be. For a regular JavaScript object, the keys have to be either strings, symbols, or numbers, so the same constraint applies to your keys here.

So to sum it up, you can put together an object that uses dynamic keys with the Record type, which takes a type parameter for the keys of your object, and a parameter for the possible values.

Example 1

If you’re looking for a generic object, chances are you want this type:

type MyObject = Record<string, any>;

I wouldn’t recommend using this because of the any, but TypeScript will let you do whatever you want with it:

const foo: MyObject = {
    foo: 7,
    bar: { bar: 'hi' },
    7: 'hi',
    [Symbol('foo')]: 'foo',
    baz: () => console.log('You can put whatever you want in this object'),
};

Example 2

We can get a bit stricter by providing a type for our values. For example:

type User = { name: string };
type UserDatabase = Record<string, User>;

const db: UserDatabase = {
    user1: { name: 'Adam' },
    user2: { name: 'Brian' },
    user3: { name: 'Carl' },
};

In this little mockup, we can imagine a mock application storing an object containing users, and only users. Adding a stricter type for the values gives you type inference when you access the values in the object, letting you do something like this:


function printUserName(userId: string) {
    console.log('Name:', db[userId].name);
}

Example 3

We can get even stricter by specifying a stricter type for the keys. A common pattern for this might be providing a type union for the keys.

Let’s extend our imaginary application – imagine we have types of users – admins, regular users and guests, and we want to control what type of user can access a page/resource.

First we’ll define our types of users:

type UserType = 'admin' | 'user' | 'guest';

Then something to store our permissions:

type PageAccessPermissions = Record<UserType, boolean>;

And we can put that all together:

const myPagePermissions: PageAccessPermissions = {
    admin: true,
    user: false,
    guest: false,
};

There’s something important to note here. Giving an object this type doesn’t mean that it has to have all the keys we’ve defined, just that the keys it does have to fit that type. This makes sense if you go back to the less strict type – would “Record<string, any>” mean an object that has every possible string as keys, or just an object where the keys have to be a string?

If you are looking to make sure a variable implements every key, you can use the “satisfies” keyword:

const myPagePermissions = {
    admin: true,
    user: false,
    guest: false,
} satisfies PageAccessPermissions;

If we take away a key:

const myPagePermissions = {
    admin: true,
    user: false,
} satisfies PageAccessPermissions;
//Property 'guest' is missing in type '{ admin: true; user: false; }' but required in type 'PageAccessPermissions'.

This can be quite a useful pattern if you have a lot of keys and want to make sure you’ve implemented them all, for example a colour palette:

type ColourVariant =
    | 'primary'
    | 'secondary'
    | 'tertiary'
    | 'alert'
    | 'error'
    | 'light'
    | 'dark';

const colourPalette = {
    primary: 'green',
    secondary: 'yellow',
    tertiary: 'white',
    alert: 'green',
    error: 'red',
    dark: 'black',
} satisfies Record<ColourVariant, string>;

//Property 'light' is missing in type '{ primary: string; secondary: string; tertiary: string; dark: string; }' but required in type 'Record<ColourVariant, string>'
//Whoops - we missed light

Using the satisfies keyword, we can ensure our object has a colour for every variant we need.

Conclusion

Thanks for reading! TypeScript can be a bit daunting at first, and the added type-checking can feel limiting, but once you get stuck in, tools like this can allow you to create dynamic types at a much faster pace. If you liked this article, feel free to leave a comment below!

Avatar photo
👋 Hey, I'm Omari Thompson-Edwards
Hey, I'm Omari! I'm a full-stack developer from the UK. I'm currently looking for graduate and freelance software engineering roles, so if you liked this article, reach out on Twitter at @marile0n

💬 Leave a comment

Your email address will not be published. Required fields are marked *

We will never share your email with anyone else.