Home > Software design >  Type definition of object literal constant with complex property types
Type definition of object literal constant with complex property types

Time:12-08

I want to have a TypeScript object literal constant with known but long list of properties which all have the same type. Below is the definition of the type and simplified example with only three properties.

type ContainerSize = {
  height: string;
  width: string;
  threshold?: number;
};

const CONTAINER_SIZES = {
  '/': { height: '65px', width: '220px' },
  '/info': { height: '220px', width: '220px' },
  default: { height: '700px', width: '500px', threshold: 480 },
} as const;

The issue with the definition above is that the property types are not ContainerSize. This can be fixed by using Record<string, ContainerSize> as the object type:

const CONTAINER_SIZES: Record<string, ContainerSize> = {
  '/': { height: '65px', width: '220px' },
  '/info': { height: '220px', width: '220px' },
  default: { height: '700px', width: '500px', threshold: 480 },
} as const;

but now any string is a valid key and thus CONTAINER_SIZES['not-existing'] wouldn't show an error.

Is there any other way to define the object literal than writing the properties twice as in the example below?

const CONTAINER_SIZES: Record<'/' | '/info' | 'default', ContainerSize> = {
  '/': { height: '65px', width: '220px' },
  '/info': { height: '220px', width: '220px' },
  default: { height: '700px', width: '500px', threshold: 480 },
} as const;

CodePudding user response:

Two options for you:

  1. Inferring the keys by defining the object separately first

  2. A do-nothing function that returns ContainerSize

Inferring the keys from previous object

It must be possible to do this in a single step, but here's how you can do it with two steps:

const _sizes = {
    '/': { height: '65px', width: '220px' },
    '/info': { height: '220px', width: '220px' },
    default: { height: '700px', width: '500px', threshold: 480 },
} as const;
const CONTAINER_SIZES: Record<keyof typeof _sizes, ContainerSize> = _sizes;

Playground link

As you pointed out (thanks!), that loses the readonly aspect of the property values (both CONTAINER_SIZE's property values and the values of the ContainerSize objects), which we can fix with Readonly in two places:

const CONTAINER_SIZES: Readonly<Record<keyof typeof _sizes, Readonly<ContainerSize>>> = _sizes;

Playground link

A do-nothing function

Another option, which I've used when I wanted ContainerSize enforced when providing the values (developer experience stuff), is to have a do-nothing function:

const cs = (size: ContainerSize) => size;

Then:

const CONTAINER_SIZES = {
    '/':      cs({ height: '65px', width: '220px' }),
    '/info':  cs({ height: '220px', width: '220px' }),
    default:  cs({ height: '700px', width: '500px', threshold: 480 }),
};

That provides autocompletion for property names etc. within the object literals being passed into cs. (On one of the projects where I did this, I quickly found that I wanted to add some cross-property verification logic to the function, so it ended up not being a do-nothing in the end...)

Playground link

  • Related