I'm trying to constrain the input of a generic to be one of several types. The closest notation I've found is using union types. Here is a trivial example:
interface IDict<TKey extends string | number, TVal> { // Error! An index signature parameter type must be // a 'string' or a 'number' [key: TKey]: TVal; } declare const dictA: IDict<string, Foo>; declare const dictB: IDict<number, Foo>;
What I'm looking for, in this example, is a way to say that TKey
should be either string
or number
, but not the union of them.
Thoughts?
Note: This is a specific case of a broader question. For example, I have another case where I have a function that accepts text
which can be either a string
or StructuredText
(parsed Markdown), transforms it, and returns exactly the corresponding type (not a subtype).
function formatText<T extends string | StructuredText>(text: T): T {/*...*/}
Technically I could write that as an overload, but that doesn't seem like the correct way.
function formatText(text: string): string; function formatText(text: StructuredText): StructuredText; function formatText(text) {/*...*/}
An overload also proves problematic, because it won't accept a union type:
interface StructuredText { tokens: string[] } function formatText(txt: string): string; function formatText(txt: StructuredText): StructuredText; function formatText(text){return text;} let s: string | StructuredText; let x = formatText(s); // error
Using type parameters in generic constraintsTypeScript allows you to declare a type parameter constrained by another type parameter. The following prop() function accepts an object and a property name. It returns the value of the property.
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.
TypeScript supports generic classes. The generic type parameter is specified in angle brackets after the name of the class. A generic class can have generic fields (member variables) or methods. In the above example, we created a generic class named KeyValuePair with a type variable in the angle brackets <T, U> .
Generics allow creating 'type variables' which can be used to create classes, functions & type aliases that don't need to explicitly define the types that they use. Generics makes it easier to write reusable code.
Updated for TS3.5+ on 2019-06-20
K extends string | number
for the index signature parameter:Yeah, this can't be done in a very satisfying way. There are a few issues. The first is that TypeScript only recognizes two direct index signature types: [k: string]
, and [k: number]
. That's it. You can't do (edit the following is no longer true as of TS4.4) a union of those (no a subtype of those (no [k: string | number]
), or[k: 'a'|'b']
), or even an alias of those: (no .[k: s]
where type s = string
)
The second issue is that number
as an index type is a weird special case that doesn't generalize well to the rest of TypeScript. In JavaScript, all object indices are converted to their string value before being used. That means that a['1']
and a[1]
are the same element. So, in some sense, the number
type as an index is more like a subtype of string
. If you are willing to give up on number
literals and convert them to string
literals instead, you have an easier time.
If so, you can use mapped types to get the behavior you want. In fact, there is a type called Record<>
that's included in the standard library that is exactly what I'd suggest using:
type Record<K extends string, T> = { [P in K]: T; }; type IDict<TKey extends string, TVal> = Record<TKey, TVal> declare const dictString: IDict<string, Foo>; // works declare const dictFooBar: IDict<'foo' | 'bar', Foo>; // works declare const dict012: IDict<'0' | '1' | '2', Foo>; // works dict012[0]; // okay, number literals work dict012[3]; // error declare const dict0Foo: IDict<'0' | 'foo',Foo>; // works
Pretty close to working. But:
declare const dictNumber: IDict<number, Foo>; // nope, sorry
The missing piece getting number
to work would be a type like numericString
defined like
type numericString = '0'|'1'|'2'|'3'|'4'|'5'|'6'|'7' // ... etc etc
and then you could use IDict<numericString, Foo>
which would behave like you want IDict<number, Foo>
to. Without a type like that, there's not much point trying to force TypeScript to do this. I'd recommend giving up, unless you have a very compelling use case.
I think I understand what you want here. The idea is that you'd like a function that takes an argument of a type that extends a union like string | number
, but it should return a type which is widened to one or more of the elements of that union. You're trying to avoid an issue with subtypes. So, if the argument is 1
, you don't want to commit to outputting a 1
, just a number
.
Before now, I'd say just use overloads:
function zop(t: string): string; // string case function zop(t: number): number; // number case function zop(t: string | number): string | number; // union case function zop(t: string | number): string | number { // impl return (typeof t === 'string') ? (t + "!") : (t - 2); }
This behaves how you'd like:
const zopNumber = zop(1); // return type is number const zopString = zop('a'); // return type is string const zopNumberOrString = zop( Math.random()<0.5 ? 1 : 'a'); // return type is string | number
And that's the suggestion I'd give if you just have two types in your union. But that could get unwieldy for larger unions (e.g., string | number | boolean | StructuredText | RegExp
), since you need to include one overload signature for every nonempty subset of elements from the union.
Instead of overloads we can use conditional types:
// OneOf<T, V> is the main event: // take a type T and a tuple type V, and return the type of // T widened to relevant element(s) of V: type OneOf< T, V extends any[], NK extends keyof V = Exclude<keyof V, keyof any[]> > = { [K in NK]: T extends V[K] ? V[K] : never }[NK];
Here is how it works:
declare const str: OneOf<"hey", [string, number, boolean]>; // string declare const boo: OneOf<false, [string, number, boolean]>; // boolean declare const two: OneOf<1 | true, [string, number, boolean]>; // number | boolean
And here's how you can declare your function:
function zop<T extends string | number>(t: T): OneOf<T, [string, number]>; function zop(t: string | number): string | number { // impl return (typeof t === 'string') ? (t + "!") : (t - 2); }
And it behaves the same as before:
const zopNumber = zop(1); // 1 -> number const zopString = zop('a'); // 'a' -> string const zopNumberOrString = zop( Math.random()<0.5 ? 1 : 'a'); // 1 | 'a' -> string | number
Whew. Hope that helps; good luck!
Link to code
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With