如何将方法参数限制为元组类型的索引号?
How to restrict method argument to index number of tuple type?
我有一个泛型 class,其中类型参数是一个元组。我在 class 上创建一个方法时遇到问题,该方法的参数仅限于元组的索引。
例如(playground link):
class FormArray<T extends [any, ...any[]]> {
constructor(public value: T) {}
// how to restrict `I` to only valid index numbers of `T` ?
get<I extends keyof T>(index: I): T[I] {
return this.value[index];
}
}
我知道您可以做的是使用 keyof
获取元组的所有属性,其中将包括与元组包含的对象关联的键(即“0”、“1” , ETC)。不幸的是,keyof
在元组上引入了 所有 属性,包括 "length"、"splice" 等
我尝试使用 keyof
并排除所有非 number
类型的属性,但后来我意识到索引属性(“0”、“1”等)是 return由 keyof
编辑为类型 string
.
目前是否可以在 Typescript 中完成此操作?谢谢!
更新
为了补充下面已接受的答案,以下是解决方法
type ArrayKeys = keyof any[];
type StringIndices<T> = Exclude<keyof T, ArrayKeys>;
interface IndexMap {
"0": 0,
"1": 1,
"2": 2,
"3": 3,
"4": 4,
"5": 5,
"6": 6,
"7": 7,
"8": 8,
"9": 9,
"10": 10,
"11": 11,
"12": 12,
}
type CastToNumber<T> = T extends keyof IndexMap ? IndexMap[T] : number;
type Indices<T> = CastToNumber<StringIndices<T>>;
class FormArray<T extends [any, ...any[]]> {
constructor(public value: T) {}
get<I extends Indices<T>>(index: I): T[I] {
return this.value[index];
}
}
在这里,如果元组的长度小于或等于 13,我们可以成功提取元组的 属性 个索引号。否则,我们 return 通用索引 number
。
您可以从 keyof T
中排除 keyof any[]
并且只剩下适当的元组键,不幸的是它们将是字符串形式:
class FormArray<T extends [any, ...any[]]> {
constructor(public value: T) {}
get<I extends Exclude<keyof T, keyof any[]>>(index: I): T[I] {
return this.value[index];
}
}
new FormArray([1,2, 3]).get("0");
您也可以添加到数字的映射,但恐怕必须手动操作:
interface IndexMap {
0: "0"
1: "1"
2: "2"
3: "3"
4: "4"
/// add up to a resonable number
}
type NumberArrayKeys<T extends PropertyKey> = {
[P in keyof IndexMap]: IndexMap[P] extends T ? P : never
}[keyof IndexMap]
class FormArray<T extends [any, ...any[]]> {
constructor(public value: T) { }
// how to restrict I to only valid index numbers of T ?
get<I extends Exclude<keyof T, keyof any[]> | NumberArrayKeys<keyof T>>(index: I): T[I] {
return this.value[index];
}
}
let a = new FormArray([1, "2", 3]).get("0");
let b = new FormArray([1, "2", 3]).get("1");
let c = new FormArray([1, 2, 3]).get(0); // number
let d = new FormArray([1, "2", 3]).get(1); // string
注意 我很惊讶 T[I]
即使 I
是 number
即使 keyof T
returns 索引为 string
而不是 number
这种认识使我想到了另一种可能的解决方案,其中 I
也可以是 number
。如果数字在元组长度范围内,它将 return 适当的类型,否则它将 return undefined
。这不会是调用错误,但是如果您使用 strictNullChecks
,return 值将被键入为 undefined
,您几乎无能为力:
class FormArray<T extends [any, ...any[]]> {
constructor(public value: T) { }
// how to restrict I to only valid index numbers of T ?
get<I extends Exclude<keyof T, keyof any[]> | number>(index: I): T[I] {
return this.value[index];
}
}
let a = new FormArray([1, "2", 3]).get("0");
let b = new FormArray([1, "2", 3]).get("1");
let c = new FormArray([1, 2, 3]).get(0); // number
let d = new FormArray([1, "2", 3]).get(1); // string
let e = new FormArray([1, "2", 3]).get(10); // undefined
通过引入模板文字字符串,可以用通用形式解决任意长度元组的问题。第一步使用与原始解决方案相同的技术,将元组索引提取为字符串文字的并集:
type Indices<A extends any[]> = Exclude<keyof A, keyof any[]> & string;
诀窍在于第二步:我们不尝试将字符串转换为数字,而是相反:
type numToStr<N extends number> = `${N}`;
现在,我们需要做的就是 Exclude
来自索引联合的操作索引,并检查结果类型是否可分配给原始联合。如果它们不是 - 我们有一个有效的索引,如果它们是 - 索引不是原始联合的一部分:
type onlyValidIndex<A extends any[], I extends number> = Indices<A> extends Exclude< Indices<A>, numToStr<I> > ? never : I;
瞧!适用于您的情况,以下是您将如何使用它:
class FormArray<T extends [any, ...any[]]> {
constructor(public value: T) {}
// how to restrict `I` to only valid index numbers of `T` ?
get<I extends number>(index: onlyValidIndex<T, I>): T[I] {
return this.value[index];
}
}
const fa = new FormArray([ 0, "", false ]);
let a = new FormArray([1, "2", 3]).get("0"); //error
let b = new FormArray([1, "2", 3]).get("1"); //error
let c = new FormArray([1, 2, 3]).get(0); //number
let d = new FormArray([1, "2", 3]).get(1); //string
let e = new FormArray([1, "2", 3]).get(10); //error
请注意,您不能再使用索引的字符串文字版本进行索引,但如果需要,只需放宽索引约束以接受辅助类型中的 number | string
联合和 number | Indices<T>
在 get
方法签名中(因为 string
无法索引 T
)。
我有一个泛型 class,其中类型参数是一个元组。我在 class 上创建一个方法时遇到问题,该方法的参数仅限于元组的索引。
例如(playground link):
class FormArray<T extends [any, ...any[]]> {
constructor(public value: T) {}
// how to restrict `I` to only valid index numbers of `T` ?
get<I extends keyof T>(index: I): T[I] {
return this.value[index];
}
}
我知道您可以做的是使用 keyof
获取元组的所有属性,其中将包括与元组包含的对象关联的键(即“0”、“1” , ETC)。不幸的是,keyof
在元组上引入了 所有 属性,包括 "length"、"splice" 等
我尝试使用 keyof
并排除所有非 number
类型的属性,但后来我意识到索引属性(“0”、“1”等)是 return由 keyof
编辑为类型 string
.
目前是否可以在 Typescript 中完成此操作?谢谢!
更新
为了补充下面已接受的答案,以下是解决方法
type ArrayKeys = keyof any[];
type StringIndices<T> = Exclude<keyof T, ArrayKeys>;
interface IndexMap {
"0": 0,
"1": 1,
"2": 2,
"3": 3,
"4": 4,
"5": 5,
"6": 6,
"7": 7,
"8": 8,
"9": 9,
"10": 10,
"11": 11,
"12": 12,
}
type CastToNumber<T> = T extends keyof IndexMap ? IndexMap[T] : number;
type Indices<T> = CastToNumber<StringIndices<T>>;
class FormArray<T extends [any, ...any[]]> {
constructor(public value: T) {}
get<I extends Indices<T>>(index: I): T[I] {
return this.value[index];
}
}
在这里,如果元组的长度小于或等于 13,我们可以成功提取元组的 属性 个索引号。否则,我们 return 通用索引 number
。
您可以从 keyof T
中排除 keyof any[]
并且只剩下适当的元组键,不幸的是它们将是字符串形式:
class FormArray<T extends [any, ...any[]]> {
constructor(public value: T) {}
get<I extends Exclude<keyof T, keyof any[]>>(index: I): T[I] {
return this.value[index];
}
}
new FormArray([1,2, 3]).get("0");
您也可以添加到数字的映射,但恐怕必须手动操作:
interface IndexMap {
0: "0"
1: "1"
2: "2"
3: "3"
4: "4"
/// add up to a resonable number
}
type NumberArrayKeys<T extends PropertyKey> = {
[P in keyof IndexMap]: IndexMap[P] extends T ? P : never
}[keyof IndexMap]
class FormArray<T extends [any, ...any[]]> {
constructor(public value: T) { }
// how to restrict I to only valid index numbers of T ?
get<I extends Exclude<keyof T, keyof any[]> | NumberArrayKeys<keyof T>>(index: I): T[I] {
return this.value[index];
}
}
let a = new FormArray([1, "2", 3]).get("0");
let b = new FormArray([1, "2", 3]).get("1");
let c = new FormArray([1, 2, 3]).get(0); // number
let d = new FormArray([1, "2", 3]).get(1); // string
注意 我很惊讶 T[I]
即使 I
是 number
即使 keyof T
returns 索引为 string
而不是 number
这种认识使我想到了另一种可能的解决方案,其中 I
也可以是 number
。如果数字在元组长度范围内,它将 return 适当的类型,否则它将 return undefined
。这不会是调用错误,但是如果您使用 strictNullChecks
,return 值将被键入为 undefined
,您几乎无能为力:
class FormArray<T extends [any, ...any[]]> {
constructor(public value: T) { }
// how to restrict I to only valid index numbers of T ?
get<I extends Exclude<keyof T, keyof any[]> | number>(index: I): T[I] {
return this.value[index];
}
}
let a = new FormArray([1, "2", 3]).get("0");
let b = new FormArray([1, "2", 3]).get("1");
let c = new FormArray([1, 2, 3]).get(0); // number
let d = new FormArray([1, "2", 3]).get(1); // string
let e = new FormArray([1, "2", 3]).get(10); // undefined
通过引入模板文字字符串,可以用通用形式解决任意长度元组的问题。第一步使用与原始解决方案相同的技术,将元组索引提取为字符串文字的并集:
type Indices<A extends any[]> = Exclude<keyof A, keyof any[]> & string;
诀窍在于第二步:我们不尝试将字符串转换为数字,而是相反:
type numToStr<N extends number> = `${N}`;
现在,我们需要做的就是 Exclude
来自索引联合的操作索引,并检查结果类型是否可分配给原始联合。如果它们不是 - 我们有一个有效的索引,如果它们是 - 索引不是原始联合的一部分:
type onlyValidIndex<A extends any[], I extends number> = Indices<A> extends Exclude< Indices<A>, numToStr<I> > ? never : I;
瞧!适用于您的情况,以下是您将如何使用它:
class FormArray<T extends [any, ...any[]]> {
constructor(public value: T) {}
// how to restrict `I` to only valid index numbers of `T` ?
get<I extends number>(index: onlyValidIndex<T, I>): T[I] {
return this.value[index];
}
}
const fa = new FormArray([ 0, "", false ]);
let a = new FormArray([1, "2", 3]).get("0"); //error
let b = new FormArray([1, "2", 3]).get("1"); //error
let c = new FormArray([1, 2, 3]).get(0); //number
let d = new FormArray([1, "2", 3]).get(1); //string
let e = new FormArray([1, "2", 3]).get(10); //error
请注意,您不能再使用索引的字符串文字版本进行索引,但如果需要,只需放宽索引约束以接受辅助类型中的 number | string
联合和 number | Indices<T>
在 get
方法签名中(因为 string
无法索引 T
)。