在Typescript泛型中约束类型是几种类型之一

时间:2018-03-12 18:33:31

标签: typescript generics discriminated-union

我正在尝试将泛型的输入限制为几种类型之一。我发现最接近的符号是使用联合类型。这是一个简单的例子:

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>;

在这个例子中,我想要的是TKey应该是stringnumber,而不是它们的联合。

思想?

注意:这是一个更广泛问题的具体案例。例如,我有另一种情况,我有一个接受text的函数,可以是stringStructuredText(已解析的Markdown),转换它,并返回完全相应的类型(不是子类型)。

function formatText<T extends string | StructuredText>(text: T): T {/*...*/}

从技术上讲,我可以把它写成一个重载,但这似乎不是正确的方法。

function formatText(text: string): string;
function formatText(text: StructuredText): StructuredText;
function formatText(text) {/*...*/}

过载也证明是有问题的,因为它不接受联合类型:

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

1 个答案:

答案 0 :(得分:8)

问题#1:K extends string | number索引签名参数:

是的,这不能以令人满意的方式完成。有一些问题。第一个是TypeScript只识别两种直接索引签名类型:[k: string][k: number]。就是这样。你不能联合那些(没有[k: string | number]),或者那些(不是[k: 'a'|'b'])的子类型,甚至是那些的别名:(没有[k: s]其中{ {1}})。

第二个问题是type s = string作为索引类型是一个奇怪的特殊情况,并不能很好地概括为TypeScript的其余部分。在JavaScript中,所有对象索引在使用之前都会转换为其字符串值。这意味着numbera['1']是相同的元素。因此,在某种意义上,a[1]类型作为索引更像是number的子类型。如果您愿意放弃string文字并将其转换为number文字,那么您就会有更轻松的时间。

如果是这样,您可以使用mapped types来获得所需的行为。事实上,有一种名为string的{​​{3}}类型正是我建议使用的:

Record<>

非常接近工作。但是:

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

declare const dictNumber: IDict<number, Foo>; // nope, sorry 工作的缺失部分类似number类似

numericString

然后你可以使用type numericString = '0'|'1'|'2'|'3'|'4'|'5'|'6'|'7' // ... etc etc ,它的行为就像你想要IDict<numericString, Foo>一样。如果没有这样的类型,尝试强制TypeScript执行此操作并没有什么意义。除非你有一个非常引人注目的用例,否则我建议放弃。

问题#2:可以从列表中扩展为类型的泛型:

我想我明白你想要什么。这个想法是你喜欢一个函数,它接受一个类型的参数来扩展像IDict<number, Foo>之类的联合,但它应该返回一个扩展到该联合的一个或多个元素的类型。您试图避免子类型的问题。因此,如果参数为string | number,则您不想承诺输出1,只需1

在此之前,我只是说使用重载:

number

这表现得如下:

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);
}

如果你的工会中只有两种类型,那就是我给出的建议。但是对于较大的联盟(例如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 )来说,这可能会变得难以处理,因为你需要为联盟中每个非空元素子集包含一个重载签名。

好吧,有一个名为included in the standard library的新TypeScript功能将在TypeScript 2.8发布时引入。 (您可以在string | number | boolean | StructuredText | RegExp之前访问它。)此时,您可以执行以下操作:

typescript@next

以下是它的工作原理:

// Diff<T,U> is just a helper here:
// return elements of T that are not present in U
type Diff<T extends string, U extends string> =
    ({ [P in T]: P } & { [P in U]: never } & { [x: string]: never })[T];

// 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 = Diff<keyof V, keyof any[]>> =
    { [K in NK]: T extends V[K] ? V[K] : never }[NK]

以下是如何宣布您的功能:

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

它的行为和以前一样:

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);
}

呼。希望有所帮助;祝你好运!