Home > OS >  Enforce references to only fields that exist for an inline object definition?
Enforce references to only fields that exist for an inline object definition?

Time:03-24

I want to quickly declare an object where the fields are enforced to be of a specific type (CSSProperties in this example, but I want a generic type in the end).

When I use an indexed type (or Record), TS restricts values of the member field values to the correct type and will throw errors if I mis-define their contents. But it loses knowledge about what fields have been defined, so a reference to an undefined field will not give a compile error and the caller just gets undefined.

export const simpleStyle = {
  a: {
    margin: 0,
  },
  b: {
    margin: 0,
    wibble: 'blah', // no compile error: BAD
  },
} 

const simpleA = simpleStyle.a;
// causes a compile error: GOOD
// const simpleB = simpleStyle.wibble;

export type CssStyles = { [index: string]: CSSProperties };

export const typedStyle: CssStyles = {
  a: {
    margin: 0,
  },
  b: {
    margin: 0,
    // causes a compile error : GOOD
    // wibble: 'blah',
  },
}

const typedA = typedStyle.a;
// does not cause a compile error
const typedB = typedStyle.wibble;

How do I define this type such that Typescript considers references to undefined indexes as invalid? Where "defined" is determined by the contents of the object I declare (I don't want to have to redefine all the fields in a union and I don't want to define an explicit type).

CodePudding user response:

If you want to enforce and type and at the same infer a more specific subtype, then you have to use a generic function.

function createStyles<K extends string>(
  styles: Record<K, CSSProperties>
): Record<K, CSSProperties> {
  return styles
}

In your case you want to infer the keys of the object, but you want to the values to be a strict known type. That means you capture the keys as K, and use that as the keys for the object the function accepts.

This also uses the Record utility type to pair keys with values in an object.

Now this works as you expect:

export const typedStyle = createStyles({
  a: {
    margin: 0,
  },
  b: {
    margin: 0,
    wibble: 'blah', // error
  },
})

const typedA = typedStyle.a;
const typedB = typedStyle.wibble; // error

Playground


Lastly, I'll add that the Typescript team is working an upcoming feature to make the function not required via the satisfies operator.

  • Related