Typescript: declare that ALL properties on an object must be of the same type
Solution 1
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.
Solution 2
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',
}
Solution 3
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...
Solution 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
LaVache
Updated on June 15, 2022Comments
-
LaVache about 2 years
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.-
Ruan Mendes almost 3 yearsSimilar question, potential dupe. stackoverflow.com/questions/50868851/…
-
-
Titian Cernicova-Dragomir almost 6 yearsNot sure I agree with the description of the generic option as a 'hack' or 'abuse', it is a good option :)
-
kingdaro almost 6 yearsSure, it works, I just wished there were a more transparent option than a useless function call, but oh well
-
ciekawy almost 5 yearslove solution 2! wonder if it would be possible to make the
Thing
being generic to makecreateThings
reusable ascreateThings<ValueType>({})
. Then the last thing would be to have some good common (guava like?) place to put it there -
Ruan Mendes almost 3 yearsWhen I use
newObj
, TS doesn't know what properties exist. It doesn't complain if I usenewObj.not_there
-
Ruan Mendes almost 3 years@ciekawy TypeScript does not support generics of generics, or higher kinded types. I needed it for a different use case.
-
am0wa almost 3 years@JuanMendes which TS version do you use?
-
Ruan Mendes almost 3 years4.3.5 See typescriptlang.org/play?#code/…
-
am0wa almost 3 yearsNote: 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' -
Ruan Mendes almost 3 yearsYes, that would do the trick but specifying the properties as strings seems smelly
-
am0wa almost 3 yearsWell, that's same that Record type requires (updated example)
-
redOctober13 over 2 yearsI wish I understood that
export
line. There are way too many Ts and Vs. -
Anton over 2 years@redOctober13 I've edited my answer. Hope it's easier to understand it now.