ZYinMD
ZYinMD

Reputation: 5059

How to make use of `keyof typeof myRecord` when myRecord is already typed as Record

Example 1:

const myGroup = {
  Alice: { age: 5 },
  Bob: { age: 6 }, 
}
function getAge(name: keyof typeof myGroup) {
  return myGroup[name].age;
}
getAge('Charlie'); // error: name must be 'Alice' | 'Bob'

ā†‘ This is what I want, however, if I type everything, it'll no longer work:

Example 2

interface People {
  age: number;
}

interface PeopleGroup {
  [name:string]: People;
}

const myGroup: PeopleGroup = {
  Alice: { age: 5 },
  Bob: { age: 6 }, 
}

function getAge(name: keyof typeof myGroup) {
  return myGroup[name].age;
}

getAge('Charlie'); // no error, name has type of string

I know I can solve it by using enum or union type, but that requires editing more than one place when adding people. In my real life project, myGroup is a big hard-coded dictionary with complicated structure, and I want to enjoy both typechecking when hard-coding it, and typechecking when querying it. Is there a dry way to do it?

Upvotes: 3

Views: 2197

Answers (1)

Maciej Sikora
Maciej Sikora

Reputation: 20132

Ok so the issue is that when you say something is Record<string, People> then any string key is correct and type of myGroup is not narrowed to keys which are really there. In order to achieve type safe in two places:

  • at the level of dictionary creating to have proper interface
  • have narrowed type to exact keys of the dictionary

We need to introduce value constructor:

function makeGroup<T extends PeopleGroup>(input: T) {
  return input;
}
// using
const myGroup = makeGroup({
  Alice: { age: 5 },
  Bob: { age: 6 },
});

What it does is it takes something which match the interface PeopleGroup - its a first type safe element, it means that argument needs to be PeopleGroup and returns narrowed type. The second is very important, using generic type says TS that we want to infer exact type of the argument. In result as output we have exactly type of the argument.

Full code:

interface People {
  age: number;
}

interface PeopleGroup {
  [name:string]: People;
}

// value constructor
function makeGroup<T extends PeopleGroup>(input: T) {
  return input;
}

// correct creating of group - type safe for PeopleGroup
const myGroup = makeGroup({
  Alice: { age: 5 },
  Bob: { age: 6 },
});
const myGroupError = makeGroup({
  Alice: { age: 5 },
  Bob: { age: 'a' }, // error as it should be šŸ‘
});

function getAge(name: keyof typeof myGroup) {
  return myGroup[name].age;
}

getAge('Alice') // ok !
getAge('Charlie'); // error as it should be šŸ‘

The Playground

Upvotes: 3

Related Questions