5
type Scales = 'xxs' | 'xs' | 's' | 'm' | 'l' | 'xl' | 'xxl'
type TScale = { [k in Scales]: number }
type TSizing1 = { [k in Scales]?: string }
type TSizing2 = { [k in Scales]: string }

const scale: TScale = {
    xxs: 1 / 8,
    xs: 1 / 4,
    s: 1 / 2,
    m: 1,
    l: 2,
    xl: 4,
    xxl: 8,
}

const sizing1: TSizing1 = {}
Object.entries(scale).forEach(([key, value]: [string, number]) => {
    sizing1[key] = `${value * 1.6}rem`
})
const sizing2: TSizing2 = Object.assign(
    {},
    ...Object.keys(scale).map(k => ({ [k]: `${scale[k] * 1.6}rem` })),
)

Both sizing1 and sizing2 will error because the lack of index signatures on TSizing1 and TScale respectively. But how am I supposed to add an index signature to a mapped type?

bitten
  • 2,463
  • 2
  • 25
  • 44

2 Answers2

2

The problem is not about in how TSizing1 or TSizing2 are defined, but how object types are treated when calling Object.prototype.entries or Object.prototype.keys.

Solution

Tell TypeScript to use the narrowed, specific types when operating on objects. Replace these:

Object.entries(scale)
Object.keys(scale)

with those:

(Object.entries(scale) as [Scales, number][])
(Object.keys(scale) as Scales[])

So the solution looks like:

(Object.entries(scale) as [Scales, number][]).forEach(([key, value]) => {
    sizing1[key] = `${value * 1.6}rem`;
});

const sizing2: TSizing2 = Object.assign(
    {},
    ...(Object.keys(scale) as Scales[]).map(k => ({ [k]: `${scale[k] * 1.6}rem` })),
);

Explanation

TypeScript is pretty lousy when it comes to inferring object types. Even though we know exactly what keys of scale are (they are very clearly defined Scales), TypeScript will treat them as if they were just some data of type string. Information is lost.

To compensate for this loss, assert their types to be what you know they are.

Karol Majewski
  • 23,596
  • 8
  • 44
  • 53
  • is there a difference in the `as` vs `:` syntax? – bitten Feb 04 '19 at 16:11
  • There is, but in your case, both will work. The former denotes a _type assertion_, the latter is a _type definition_. One tricks the compiler into thinking different than it would normally do, while the other tells the compiler what your intention is. I've chosen to go with an assertion as early as possible because then if you change `map` to any other method like `reduce` it will still work, whereas adding an inline definition to `forEach` would work only within its callback. – Karol Majewski Feb 04 '19 at 17:15
1

The errors make sense because you can't assign a string key to an interface with specific keys and without index signature (index signature is an instruction saying what an interface should return for unknown keys i.e. string keys).

Hence you should change your code like this:

Object.entries(scale).forEach(([key, value]: [Scales /* EDIT1 */, number]) => {
    sizing1[key] = `${value * 1.6}rem`
})
const sizing2: TSizing2 = Object.assign(
    {},
    ...Object.keys(scale).map((k) => ({ [k]: `${scale[k as Scales] /*EDIT2*/ * 1.6}rem` })),
)

The first edit (EDIT1) is clear, I hope. The second one (EDIT2) is a necessary evil because Object.keys unfortunately returns just string[] even if the argument has known type.

Nurbol Alpysbayev
  • 19,522
  • 3
  • 54
  • 89