TypeScript - can a generic constraint provide "allowed" types?
You can constrain TKey to be derived from string or number (using extends) but that will not satisfy the compiler. index
must be either number or string, not a generic type or any other type for that matter. This is documented in the language spec
As @TitianCernicova-Dragomir indicates, you can't use TKey
as the type in an index signature, even if it is equivalent to string
or number
.
If you know that TKey
is exactly string
or number
, you can just use it directly and not specify TKey
in your type:
type StringIndexable<TValue> = { [index: string]: TValue }
type NumberIndexable<TValue> = { [index: number]: TValue }
Aside: TypeScript treats number
is usually treated as a kind of subtype of string
for key types. That's because in JavaScript, indices are converted to string
anyway when you use them, leading to this kind of behavior:
const a = { 0: "hello" };
console.log(a[0]); // outputs "hello"
console.log(a['0']) // *still* outputs "hello"
EDIT: Note that TS2.9 added support for number
and even symbol
keys in mapped types. We will use keyof any
to mean "whatever your version of TypeScript thinks are valid key types". Back to the rest of the answer:
If you want to allow TKey
to be more specific than keyof any
, meaning only certain keys are allowed, you can use mapped types:
type Indexable<TKey extends keyof any, TValue> = { [K in TKey]: TValue }
You'd use it by passing in a string literal or union of string literals for TKey
:
type NumNames = 'zero' | 'one' | 'two';
const nums: Indexable<NumNames, number> = { zero: 0, one: 1, two: 2 };
type NumNumerals = '0' | '1' | '2';
const numerals: Indexable<NumNumerals, number> = {0: 0, 1: 1, 2: 2};
And if you don't want to limit the key to particular literals or unions of literals, you can still use string
as TKey
:
const anyNums: Indexable<string, number> = { uno: 1, zwei: 2, trois: 3 };
In fact, this definition for Indexable<TKey, TValue>
is so useful, it already exists in the TypeScript standard library as Record<K,T>
:
type NumNames = 'zero' | 'one' | 'two';
const nums: Record<NumNames, number> = { zero: 0, one: 1, two: 2 };
I therefore recommend you use Record<K,T>
for these purposes, since it is standard and other TypeScript developers who read your code are more likely to be familiar with it.
Hope that helps; good luck!