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

bjn*_*nsn 12 generics discriminated-union typescript

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

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>;
Run Code Online (Sandbox Code Playgroud)

在这个例子中,我正在寻找的是一种方式,它TKey应该是或者是,string或者number不是它们的结合.

思考?

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

function formatText<T extends string | StructuredText>(text: T): T {/*...*/}
Run Code Online (Sandbox Code Playgroud)

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

function formatText(text: string): string;
function formatText(text: StructuredText): StructuredText;
function formatText(text) {/*...*/}
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

jca*_*alz 12

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

是的,这不能以令人满意的方式完成.有一些问题.第一个是TypeScript只识别两种直接索引签名类型:[k: string][k: number].而已.你不能做那些(不[k: string | number])或那些(不)的子类型的联合[k: 'a'|'b'],或者甚至是那些的别名:(没有[k: s]在哪里type s = string).

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

如果是这样,您可以使用映射类型来获取所需的行为.事实上,标准库包含了一个类型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
Run Code Online (Sandbox Code Playgroud)

非常接近工作.但:

declare const dictNumber: IDict<number, Foo>; // nope, sorry
Run Code Online (Sandbox Code Playgroud)

number开始工作的缺失部分将是类似于numericString定义的类型

type numericString = '0'|'1'|'2'|'3'|'4'|'5'|'6'|'7' // ... etc etc
Run Code Online (Sandbox Code Playgroud)

然后你可以使用IDict<numericString, Foo>哪种行为就像你想要的那样IDict<number, Foo>.如果没有这样的类型,尝试强制TypeScript执行此操作并没有太大意义.我建议放弃,除非你有一个非常引人注目的用例.

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

我想我明白你想要什么.这个想法是你想要一个函数,它接受一个类似于扩展联合的类型的参数string | number,但它应该返回一个扩展为该联合的一个或多个元素的类型.您正试图避免子类型的问题.所以,如果参数是1,你不想提交输出a 1,只是a 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);
}
Run Code Online (Sandbox Code Playgroud)

这表现得如下:

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
Run Code Online (Sandbox Code Playgroud)

如果你的工会中只有两种类型,那就是我给出的建议.但是对于较大的联盟(例如string | number | boolean | StructuredText | RegExp),这可能会变得难以处理,因为您需要为联合中的每个非空元素子集包含一个重载签名.

好吧,有一个名为条件类型的新TypeScript特性将在TypeScript 2.8发布时引入.(您可以在此之前通过它访问它K extends string | number.)此时,您可以执行以下操作:

// 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];
Run Code Online (Sandbox Code Playgroud)

下面是它的工作原理:

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
Run Code Online (Sandbox Code Playgroud)

以下是如何声明您的功能:

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);
}
Run Code Online (Sandbox Code Playgroud)

它的行为和以前一样:

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
Run Code Online (Sandbox Code Playgroud)

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