Doofus
Doofus

Reputation: 1102

String type to number

Is there a way to convert a string type literal (not runtime value) to its corresponding number type literal, e.g. '1' to 1?


I want to narrow a variable to being a key of an object (note the programmer asserts the type of obj is exact, as TS doesnt yet have support for it):

const isKeyOfExactTable = <O extends object, T extends string | number>(o: O, key: T): key is ??? => Object.hasOwnProperty.call(o, key);
// what to put here? -------------------------------------------------------------------------^^^

There has always been the issue of JS object properties only being strings, but every member access implicitly converting numbers. As such, TS properties can actually be numbers, and even if not, the problem would then appear at the member access, just one step earlier.

Since TS 4.1, template literal types solve one direction, which makes this almost fully possible (if either side has number literals, one can just convert, and check), if the properties have a number index, can just take all numbers from the key. Sadly, if the key is number, and the object has strings as properties, which represent numbers, there simply seems to be no way. One can even check, that there really is a problem:

type HasStringNumber<T> = T extends `${number}`
  ? 'houston, we have a problem'
  : 'all systems green';
type Test<O extends object, K extends string | number> = number extends K
  ? HasStringNumber<keyof O>
  : 'all systems green';

But no way to identify the actual number, e.g. this just more or less expectedly infers the input string:

type WhatNumber<T extends string> = T extends `${infer N}` ? N : 'Not a number';`

To give a straight in-/output example for the problem:

declare const o: { '1': 0 };
declare const k: number;
if (isKeyOfExactTable(o, k)) { /* typeof k is 1 */ }

Upvotes: 4

Views: 1312

Answers (1)

jcalz
jcalz

Reputation: 327624

No, there is currently no direct support for a type StrToNum<T extends string> = ... which does the inverse of type NumToStr<N extends number> = `${N}`. It looks like microsoft/TypeScript#42938 might be the issue tracking this request.


For now, all you can do is work around it. If you are willing to restrict your set of numeric strings to those which represent non-negative whole numbers less than some reasonably small value, you can shove the following StrToNum implementation in a library somewhere:

type Nums = [
  0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
  10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
  20, 21, 22, 23, 24, 25, 26, 27, 28, 29
]; // make this as long as you need
type StrToNum<T extends string> =
  Extract<T extends keyof Nums ? Nums[T] : never, number>;

This works by indexing into a fixed tuple Nums, whose keys are automatically interpreted as numeric strings, and whose values are the corresponding number. If you need to support larger numbers, you can manually lengthen Nums. It turns out that for now this is probably close to the best you can do.

You can try to use recursive conditional types to compute this sort of StrToNum for arbitrary whole numbers, by building up the tuple programmatically. But the straightforward implementation will hit recursion limits for numbers less than 25 or so, which is worse than the above Nums. Instead you'd need to do something more complex, like making the compiler compute the binary digits of the number in question, as in this GitHub issue comment. That would give you the ability to compute StrToNum<"12345">. But even this method would end up building a tuple of the same length as your desired output, and so StrToNum<"8675309"> would probably crash or hang the compiler. It doesn't seem worth it to me.


Anyway, the above definition of StrToNum works well enough to deal with your example:

function isKeyOfExactTable<T extends object>(t: T, k: PropertyKey): k is
  keyof T | StrToNum<Extract<keyof T, string>> {
  return k in t;
}    

declare const o: { '1': 0 };
declare const k: number;
if (isKeyOfExactTable(o, k)) {
  k // 1
}

If you need arbitrarily large numbers, or numbers with fractional parts, or negative numbers, or numbers requiring exponential notation, I really don't know of any good way to implement that.

Other than just going to microsoft/TypeScript#42938 and giving it a 👍, I'd probably try to figure out how to invert the problem so that I only need to deal with converting string literals to numeric literals instead. For example:

function toStringKeyOfExactTable<T extends object, K extends number>(t: T, k: K):
  Extract<keyof T, `${K}`> | (`${K}` extends keyof T ? never : undefined) {
  return ((k in t) ? String(k) as `${K}` : undefined) as any;
}
const newKey = toStringKeyOfExactTable(o, k); // "1" | undefined
if (newKey !== undefined) {
  newKey // "1"
  const val = o[newKey] // 0
}
const newKey2 = toStringKeyOfExactTable(o, 1); // "1"
const newKey3 = toStringKeyOfExactTable(o, 2); // undefined

Here we convert a numeric key to a string (or possibly undefined), and then use that string as a key. This isn't the code you wanted to write, but at least the compiler can follow it.


Playground link to code

Upvotes: 3

Related Questions