Mike
Mike

Reputation: 1011

Is it possible to enforce the type of an object's values without explicitly setting a type for its keys?

I want something like this:

type Person = { age:number, weight: number}
const people:{[key:infer_the_type]:Person} = {
  mike: {...},
  bob: {...},
}

so that if I do this, it works:

keyof typeof people // This should be "mike" | "bob"

Is this possible?

Upvotes: 3

Views: 487

Answers (2)

jcalz
jcalz

Reputation: 328067

You cannot get the behavior you're looking for by annotating the type of people; once you annotate people with a (non-union) type, the compiler will automatically throw away any more specific information it had about the initialized value. There's a (longstanding) open issue in GitHub, microsoft/TypeScript#7481 which asks for something like your infer_the_type (especially relevant is microsoft/TypeScript#38349 which was closed as a duplicate). If you want to see that happen, you might want to go to that issue and give it a 👍, but it's not clear when or if this will ever be implemented.

Instead, you can get similar behavior by replacing your type annotation with a generic helper function:

const asPeople = 
  <K extends PropertyKey>(people: { [P in K]: Person }) => people;

const people = asPeople({
  mike: { name: "Mike", weight: 150 },
  bob: { name: "Bob", weight: 200 },
}); // okay

You can verify that the compiler has inferred the keys of people:

/* const people: {
    mike: Person;
    bob: Person;
} */

And the helper function will report an error if you pass it something unexpected:

const badPeople = asPeople({
  alice: { name: "Alice", height: 75 } // error!
  // -------------------> ~~~~~~~~~~
  //  Did you mean to write 'weight'?
})

Playground link to code

Upvotes: 2

lawrence-witt
lawrence-witt

Reputation: 9354

I would be tempted to create a helper function to do this. It's a little bit ugly since it means having pointless functions floating around at runtime but will get the job done:

const getPeople = <P extends PropertyKey>(p: Record<P, Person>) => p;

const typedPeople = getPeople({
  mike: {name: "mike", weight: 32},
  bob: {name: "bob", weight: 6},
})

typedPeople will be of type Record<'mike' | 'bob', Person>.

Alternatively, since this is presumably a static list of names (I don't see how this would work otherwise), it might just be better to create the type beforehand, assming the list is not too long or subject to change:

type Names = 'mike' | 'bob';

Even more alternatively, I am a little suspicious of the need to explicitly type people at all. As you've already demonstrated, you can use keyof to get the names out of the object after instantiating it, so anywhere it's going to get used should have the types already available.

Upvotes: 1

Related Questions