Home > Back-end >  Forbid record index type conversion in TypeScript
Forbid record index type conversion in TypeScript

Time:11-17

At the moment in TypeScript it is possible to write:

const r: Record<string, string> = {
  "42": "42_value"
};
console.log(r[42]);

Prints:

"42_value"

Can I somehow forbid passing number as index to Record<string, string> index operator and disable automatic conversion of number to string? Or maybe I should use some other type instead of Record to strongly specify key type and force compiler error on line console.log(r[42])?

CodePudding user response:

It is possible to forbid with help of extra function:

type NotNumber<T> = keyof T extends `${number}` ? never : keyof T extends number ? never : T

const forbidNumber = <
  Key extends string | number,
  Dict extends Record<Key, string>
>(arg: NotNumber<Dict>) => arg

forbidNumber({ 42: 'hello' }) // error
forbidNumber({ '42': 'hello' }) // error

forbidNumber({ '42foo': 'hello' }) // ok

Playground

As you might have noticed it might have a drawback. It depends what are you expecting. If you want to forbid any numerical char in the key, you might wonna use this recursive type:


type Numerical = number | `${number}`

type ReplaceNum<T, Cache extends string = ''> =
  T extends ''
  ? Cache
  : T extends `${infer Char}${infer Rest}`
  ? Char extends Numerical
  ? ReplaceNum<Rest, `${Cache}_`>
  : ReplaceNum<Rest, `${Cache}${Char}`>
  : never

type Test = ReplaceNum<'412foo'> // "___foo"

type ForbidNumbers<T extends Record<string | number, string>> = {
  [Prop in keyof T as ReplaceNum<Prop>]: T[Prop]
}

const forbidNumber = <
  Key extends string | number,
  Dict extends Record<Key, string>
>(arg: Dict extends ForbidNumbers<Dict> ? Dict : never) => arg

forbidNumber({ 42: 'hello' }) // error
forbidNumber({ '42': 'hello' }) // error

forbidNumber({ '42foo': 'hello' }) // ok

Playground

ReplaceNum - iterates through each char in an object key and checks whether it is stringified number or not. If it is a number - replace it with underscore _, otherwise leave as is.

ForbidNumbers - iterates through each key and replace it wirh return type of ReplaceNum.

Dict extends ForbidNumbers<Dict> ? Dict : never - If Dict is the same object as after calling ForbidNumbers - allow using Dict, otherwise - return never

You can find more information about type validation in my articles here and here


There is no type negation in typescript. You can only validate it with help of conditional type, like T extends number?never:T. If T meets condition return never otherwise return T. Treat it as a replacement rather forbidding. Hence it is impossible to do smth like this Record<not number, string>


And what I don't like is that when I pass number to index operator it is automatically converted to string

Please see docs

An index signature property type must be either ‘string’ or ‘number’.

It is possible to support both types of indexers... It is possible to support both types of indexers, but the type returned from a numeric indexer must be a subtype of the type returned from the string indexer. This is because when indexing with a number, JavaScript will actually convert that to a string before indexing into an object. That means that indexing with 100 (a number) is the same thing as indexing with "100" (a string), so the two need to be consistent.

  • Related