2

I have an indexable type in TypeScript with keys that can either be strings or numbers, like so:

export type MyRecords = { [name: string]: string | number };

const myRecords: MyRecords = {
  foo: 'a',
  bar: 1,
};

I want to create a string literal type that includes only the string keys of this type, so that I can use it to ensure type safety in my code. For example:

type KeysOfMyRecords = /* ??? */;

const key: KeysOfMyRecords = 'foo'; // should be OK
const invalidKey: KeysOfMyRecords = 'invalid'; // should cause a  error

I have tried the following without success:

type KeysOfMyRecords = keyof typeof myRecords;

Would this be possible?

cyrus-d
  • 749
  • 1
  • 12
  • 29
  • 1
    If you want to do that you can't declare `MyRecords` to be a record type (e.g. `Record` or the way you wrote it). You would have to declare *specific* keys with *specific* types. You can have dynamism or you can have statically defined compile time keys, but you can't have both (at least not in the way you're doing it). – Jared Smith May 02 '23 at 16:54
  • Yes, that is what I thought too, I thought perhaps there was a way out, but apparently not. – cyrus-d May 02 '23 at 16:56
  • Note that TS is structurally typed, even if you don't annotate it `myRecords` [qualifies as that type](https://www.typescriptlang.org/play?#code/KYDwDg9gTgLgBDAnmYcCyiBKwDG0AmAznALxwDecA2gHYCGAtsAFxyExQCWNA5gLqt2XXnAA+cGgFcGAI2BQ4AXwDcAKFV4a7OAyy4CxMuVVw4AMwgRWAcjrWANCbgy6UVgEZHK9Zu0Wr6Hp4UESkOkEGqkA). You *could* leave it unannotated, then you could use a recursive conditional type to pull out the ones with string values. – Jared Smith May 02 '23 at 16:58
  • 2
    Does [this approach](https://tsplay.dev/WYeAgW) using the `satisfies` operator instead of an annotation meet your needs? If so I could write up an answer (although I think there are probably existing questions that this would duplicate); if not, what am I missing? – jcalz May 02 '23 at 17:04
  • Do we only want foo above and not bar @cyrus-d? – Tushar Shahi May 02 '23 at 17:16
  • 1
    While the @TusharShahi answer does work, I think "satisfies" is more appropriate. Thank you – cyrus-d May 02 '23 at 17:16

1 Answers1

2

I think you can use a generic to achieve this:

export type MyRecords<T extends string | number | symbol> = Record<T, string | number>

const myRecords : MyRecords<'foo' | 'bar'>= {
  foo: 'a',
  bar: 1,
};


type KeysOfMyRecords = keyof typeof myRecords;

const key: KeysOfMyRecords = 'foo'; // should be OK
const invalidKey: KeysOfMyRecords = 'invalid'; // should cause a  error

The downside as you saw above is that you will have to mention the keys while defining your object.

Link

The issue with your expectation is that we want myRecords to be of a certain type MyRecords. Now we have explicityle defined MyRecords type to be having string as key, and that means any string. We could also have used const assertion in your code directly but it would not guarantee type safety with MyRecords type.

Tushar Shahi
  • 16,452
  • 1
  • 18
  • 39
  • Thanks for your answer, but I was hoping not to modify MyRecords, and it appears that the "satisfies" feature is the only way to accomplish this. – cyrus-d May 02 '23 at 17:22