Typing the reduce method [duplicate]

How would you declare a type for this properly?

interface MediaQueryProps {
  [key: string]: number;
}

const size: MediaQueryProps = {
  small: 576,
  medium: 768,
  large: 992,
  extra: 1200
};

export default Object.keys(size).reduce((acc, cur) => {
  acc[cur] = `(min-width: ${size[cur]}px)`;

  return acc;
}, {});

acc[cur] is complaining because

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{}'.
  No index signature with a parameter of type 'string' was found on type '{}'

Is there any way I can declare a type for this without using any?


Solution 1:

If you want the accumulator value to be indexable by string, Record<string, string> should do the trick. You can pass this as the type argument to reduce

interface MediaQueryProps {
  [key: string]: number;
}

const size: MediaQueryProps = {
  small: 576,
  medium: 768,
  large: 992,
  extra: 1200
};

export default Object.keys(size).reduce<Record<string, string>>((acc, cur) => {
  acc[cur] = `(min-width: ${size[cur]}px)`;
  return acc;
}, {});

Playground link

Solution 2:

You can do it like this using Record and keyof:

export default Object.keys(size).reduce((acc, cur) => {
  acc[cur] = `(min-width: ${size[cur]}px)`;

  return acc;
}, {} as Record<keyof MediaQueryProps, string>);

Solution 3:

With TypeScript 4.1 you can also take advantage of Template Literal Types with as type casting. TypeScript Playground Link.

// .ts
interface MediaQueryProps {
  [key: string]: number;
}

const size = {
  small: 576,
  medium: 768,
  large: 992,
  extra: 1200
} as const;

const mediaQueryKeys = Object.keys(size) as Array<keyof typeof size>;

const mediaQueries = mediaQueryKeys.reduce((acc, cur) => {
  acc[cur] = `(min-width: ${size[cur]}px)`;

  return acc;
}, {} as Record<`${keyof typeof size}`, string>);

export default mediaQueries;

// d.ts
declare const mediaQueries: Record<"small" | "medium" | "large" | "extra", string>;
export default mediaQueries;

Solution 4:

Approved answear uses only string type, but I wanted more accurate types, so this is my approach:

type Breakpoints = {
  small: number;
  medium: number;
  large: number;
  extra: number;
};

const size: Breakpoints = {
  small: 576,
  medium: 768,
  large: 992,
  extra: 1200
};

// Type of entry after Object.entries() is used
type BreakpointEntry = [keyof Breakpoints, Breakpoints[keyof Breakpoints]];

// Generic helper to make all of the properties' types: string | undefined
type Stringify<T> = { [key in keyof T]?: string };

// Override Object interface for custom Object.entries():
interface CustomObject extends ObjectConstructor {
  entries<K extends keyof Breakpoints, T>(
    o: { [s in K]: T } | ArrayLike<T>
  ): [K, T][];
}

// Let's use this interface now:
const obj: CustomObject = Object;

export default obj
  .entries(size)
  .reduce<Stringify<Breakpoint>>((acc, cur: BreakpointEntry) => {
    const [key, value] = cur;
    acc[key] = `(min-width: ${value}px)`;

    return acc;
  }, {});