我正在尝试将泛型的输入限制为几种类型之一。我发现最接近的符号是使用联合类型。这是一个简单的例子:
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
应该是string
或number
,而不是它们的联合。
思想?
注意:这是一个更广泛问题的具体案例。例如,我有另一种情况,我有一个接受text
的函数,可以是string
或StructuredText
(已解析的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
答案 0 :(得分:8)
K extends string | number
索引签名参数:是的,这不能以令人满意的方式完成。有一些问题。第一个是TypeScript只识别两种直接索引签名类型:[k: string]
和[k: number]
。就是这样。你不能联合那些(没有[k: string | number]
),或者那些(不是[k: 'a'|'b']
)的子类型,甚至是那些的别名:(没有[k: s]
其中{ {1}})。
第二个问题是type s = string
作为索引类型是一个奇怪的特殊情况,并不能很好地概括为TypeScript的其余部分。在JavaScript中,所有对象索引在使用之前都会转换为其字符串值。这意味着number
和a['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执行此操作并没有什么意义。除非你有一个非常引人注目的用例,否则我建议放弃。
我想我明白你想要什么。这个想法是你喜欢一个函数,它接受一个类型的参数来扩展像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);
}
呼。希望有所帮助;祝你好运!