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

5

35

In Typescript you can declare that all elements in an array are of the same type like this:

const theArray: MyInterface[]

Is there anything similar you can do that declares that ALL of an object's property values must be of the same type? (without specifying every property name)

For example, I'm currently doing this:

interface MyInterface {
    name:string;
}

const allTheThingsCurrently = {
    first: <MyInterface>{name: 'first thing name' },
    second: <MyInterface>{name: 'second thing name' },
    third: <MyInterface>{name: 'third thing name' },
    //...
};

...note how I have to specify <MyInterface> for every single property. Is there any kind of shortcut for this? i.e. I'm imagining something like this...

const allTheThingsWanted:MyInterface{} = {
    first: {name: 'first thing name' },
    second: {name: 'second thing name' },
    third: {name: 'third thing name' },
    //...
};

MyInterface{} is the part that's invalid code and I'm looking for a way to do with less redundancy, and optionally the extra strictness that prevents any other properties being adding to the object of a differing type.

Autosuggestion answered 9/7, 2018 at 2:48 Comment(1)
Similar question, potential dupe. #50869351Theo
W
55

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.

Wimble answered 9/7, 2018 at 3:9 Comment(4)
Not sure I agree with the description of the generic option as a 'hack' or 'abuse', it is a good option :)Tovatovar
Sure, it works, I just wished there were a more transparent option than a useless function call, but oh wellWimble
love solution 2! wonder if it would be possible to make the Thing being generic to make createThings reusable as createThings<ValueType>({}). Then the last thing would be to have some good common (guava like?) place to put it thereStrafe
@Strafe TypeScript does not support generics of generics, or higher kinded types. I needed it for a different use case.Theo
E
21

Some alternatives for single level (flat) objects:

Alternative 1 (indexable type):

const exampleObj: { [k: string]: string } = {
  first: 'premier',
  second: 'deuxieme',
  third: 'troisieme',
}

Alternative 2 (Record):

const exampleObj: Record<string, string> = {
  first: 'premier',
  second: 'deuxieme',
  third: 'troisieme',
}

Alternative 3 (Record / Union):

const exampleObj: Record<'first' | 'second' | 'third', string> = {
  first: 'premier',
  second: 'deuxieme',
  third: 'troisieme',
}
Eleanoreleanora answered 19/4, 2020 at 10:19 Comment(0)
T
8

Use generic and specify which properties type do you want.

type SamePropTypeOnly<T> = {
  [P: string]: T;
}

interface MyInterface {
  name: string;
}

const newObj: SamePropTypeOnly<MyInterface> = {
  first: { name: 'first thing name' },
  second: { name: 'second thing name' },
  third: { name: 'third thing name' },
  // forth: 'Blah' // Type 'string' is not assignable to type `MyInterface`
}

newObj.not_there; // undefined - no error

Note: if the list of property names has to be limited, keys have to be specified explicitly:

interface MyInterface {
  name: string;
}

type OptionKeys = 'first' | 'second' | 'third';

const newObj: Record<OptionKeys, MyInterface> = {
  first: { name: 'first thing name' },
  second: { name: 'second thing name' },
  third: { name: 'third thing name' },
  // forth: 'Blah' // error
}

newObj.not_there // Property 'not_there' does not exist on type...
Thallophyte answered 20/11, 2019 at 16:32 Comment(6)
When I use newObj, TS doesn't know what properties exist. It doesn't complain if I use newObj.not_thereTheo
@JuanMendes which TS version do you use?Thallophyte
4.3.5 See typescriptlang.org/play?#code/…Theo
Note: goal was to check the type of the each property not to limit its name: newObj.foo.not_there // Property 'not_there' does not exist on type 'MyInterface'Thallophyte
Yes, that would do the trick but specifying the properties as strings seems smellyTheo
Well, that's same that Record type requires (updated example)Thallophyte
F
4

Approach Generics with a no-op function can be extended to have a generic function accepting a type of required values, which itself returns no-op function. This way it won't be required to create new function for each type

export const typedRecord = <TValue>() => <T extends Record<PropertyKey, TValue>>(v: T): T => v;

To understand what happens here below is alternative declaration of typedRecord function from above. typedRecord function accepts type parameter TValue for the property type of the record and returns another function which will be used to validate structure of the type T passed to it (TS compiler will infer T from invocation)

export function typedRecord<TValue>() {
  return function identityFunction<T extends Record<PropertyKey, TValue>>(v: T): T {
    return v;
  };
}

This covers all requirements

const allTheThings = typedRecord<Thing>()({
  first: { name: "first thing name" },
  second: { name: "second thing name" },
  third: { name: "third thing name" },
  fourth: { oops: "lol!" }, // error here
});

allTheThings.first;
allTheThings.nonexistent; // error here
Footman answered 23/2, 2021 at 4:12 Comment(2)
I wish I understood that export line. There are way too many Ts and Vs.Stateless
@Stateless I've edited my answer. Hope it's easier to understand it now.Footman
L
1

this one working perfectly

const exampleObj: { [k: string]: string } = {
  first: 'premier',
  second: 'deuxieme',
  third: 'troisieme',
}
Lossa answered 20/6, 2022 at 10:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.