Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript - can a generic constraint provide "allowed" types?

Given the following code...

type Indexable<TKey, TValue> = { [index: TKey]: TValue }

This produces the following error:

An index signature parameter type must be 'string' or 'number'.

Is there a way to constrain TKey to be 'string' or 'number'?

like image 481
Matthew Layton Avatar asked Oct 23 '17 09:10

Matthew Layton


People also ask

How do you pass a generic type as parameter in TypeScript?

Assigning Generic ParametersBy passing in the type with the <number> code, you are explicitly letting TypeScript know that you want the generic type parameter T of the identity function to be of type number . This will enforce the number type as the argument and the return value.

What does the generic constraint of type interface do?

Interface Type Constraint You can constrain the generic type by interface, thereby allowing only classes that implement that interface or classes that inherit from classes that implement the interface as the type parameter.

What is generic type constraint?

A type constraint on a generic type parameter indicates a requirement that a type must fulfill in order to be accepted as a type argument for that type parameter. (For example, it might have to be a given class type or a subtype of that class type, or it might have to implement a given interface.)

Can a generic class have multiple constraints?

Multiple interface constraints can be specified. The constraining interface can also be generic.


2 Answers

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!

like image 159
jcalz Avatar answered Sep 30 '22 03:09

jcalz


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

like image 43
Titian Cernicova-Dragomir Avatar answered Sep 30 '22 03:09

Titian Cernicova-Dragomir