Vincent
Vincent

Reputation: 61

Type inference does not include dynamic Object.fromEntries() properties

I am trying to dynamically generate my styles using material UI's makeStyles() but when I use Object.fromEntries in conjunction, the type is not correctly inferred.

import * as React from "react";
import { makeStyles } from "@material-ui/core/styles";

const paperViewportSizes = {
  sm: '30',
  md: '50',
  lg: '66',
  xl: '80',
} 

const useStyles = makeStyles((theme) => ({
  App: {
    color: "hotpink"
  },
  ...Object.fromEntries(
    ['Sm', 'Md', 'Lg'].map((size, i) => [
      `contentPadding${size}`,
      { padding: theme.spacing((i + 1) * 2, (i + 1) * 2 + 1) },
    ]),
  ),
  ...Object.entries(paperViewportSizes).reduce(
    (acc, [key, size]) => ({
      ...acc,
      ...{
        [`paper_${key}_h`]: { height: `${size}vh` },
        [`paper_${key}_w`]: { width: `${size}vw` },
      },
    }),
    {
      paper_auto_h: { height: 'auto' },
      paper_auto_w: { width: 'auto' },
    }
  )
}));
export default function App() {
  const classes = useStyles(); // Record<"App" | "paper_auto_h" | "paper_auto_w", string>
  return (
    <>
    <div className={classes.App}>
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
    </div>
    </>
  );
}

Is there anything that I am missing?

enter image description here

Upvotes: 0

Views: 80

Answers (1)

jabuj
jabuj

Reputation: 3649

The problem is that Object.fromEntries hardly infers anything at all. If you look its type declarations, you will see this:

interface ObjectConstructor {
    fromEntries<T = any>(entries: Iterable<readonly [PropertyKey, T]>): { 
        [k: string]: T 
    };
    fromEntries(entries: Iterable<readonly any[]>): any;
}

You can see, that it either returns any, or { [k: string]: T }. In the former case, there's no inference, in the latter - there are no limitations on the keys type. Consider this example:

const foo = Object.fromEntries<number>([
  // I'm writing "as const" because typescript expects to get
  // type "[string | number | symbol, number]", but if you don't
  // add "as const", it will consider ['a', 1] to be "(string | number)[]"
  ['a', 1] as const,
  ['b', 2] as const,
])

Here foo is of type { [k: string]: number }, and not of type { [k in 'a' | 'b']: number }, even though this type would be more precise. Now see what happens with spread operator:

const bar = {
   ...foo,
   c: 8
}

Here bar is of type { c: number }. You can see typescript just ignored foo. I'm not an expert in the way spread operator inference works in TS, if you are interested, you can google that.

Any way this is what happens in your code and the only way to get correct types would be to cast the type manually

...(
  Object.fromEntries(/*...*/) as Record<
    'contentPaddingSm' | 'contentPaddingLg' | 'contentPaddingMd',
    { padding: string }
  >
)

Upvotes: 1

Related Questions