Typescript: declare that ALL properties on an object must be of the same type

Solution 1: Indexable type

interface Thing {
  name: string
}

interface ThingMap {
  [thingName: string]: Thing
}

const allTheThings: ThingMap = {
  first: { name: "first thing name" },
  second: { name: "second thing name" },
  third: { name: "third thing name" },
}

The downside here is that you’d be able to access any property off of allTheThings without any error:

allTheThings.nonexistent // type is Thing

This can be made safer by defining ThingMap as [thingName: string]: Thing | void, but that would require null checks all over the place, even if you were accessing a property you know is there.

Solution 2: Generics with a no-op function

const createThings = <M extends ThingMap>(things: M) => things

const allTheThings = createThings({
  first: { name: "first thing name" },
  second: { name: "second thing name" },
  third: { name: "third thing name" },
  fourth: { oops: 'lol!' }, // error here
})

allTheThings.first
allTheThings.nonexistent // comment out "fourth" above, error here

The createThings function has a generic M, and M can be anything, as long as all of the values are Thing, then it returns M. When you pass in an object, it’ll validate the object against the type after the extends, while returning the same shape of what you passed in.

This is the “smartest” solution, but uses a somewhat clever-looking hack to actually get it working. Regardless, until TS adds a better pattern to support cases like this, this would be my preferred route.

Leave a Comment

Hata!: SQLSTATE[HY000] [1045] Access denied for user 'divattrend_liink'@'localhost' (using password: YES)