打字稿泛型,获取数组内容的类型
Typescript generics, get type of Array contents
我已经构建了一个方法,它将一个数组与传入的另一个数组连接起来。该数组位于一个名为 state
的父对象上,该对象通常在我的方法中键入。
我的方法是这样的:
export const mutateConcat = <T, K extends keyof T>(state: T, key: K, val: T[K]) => {
((state[key] as unknown) as any[]) = ((state[key] as unknown) as any[]).concat(val);
}
显然这很丑陋,我想摆脱对 unknown
和 any[]
的强制转换。我还希望能够确定数组内容的类型。
基本上,我需要确保 T[K] 是一个数组,理想情况下我想将数组内容的类型分配给另一个通用参数,比如 U
。
我试过 <T, K extends keyof T, U extends Array<T[K]>>
,但它会将 U
分配给数组本身的类型,而不是数组内容的类型。
我问的可能吗? extends keyof
是否可以仅过滤为数组类型的键,以及这些数组内容的类型是否可以分配给另一个类型参数?
提前致谢
编辑:
因此,我设法通过关闭 val
变量来获取数组的类型。所以如下:
export const mutateConcat = <T, U, K extends keyof T>(state: T, key: K, val: T[K] & U[], arrayDistinctProperty?: keyof U) => {
// U now contains the array type, and `arrayDistinctProperty` is typed to a property on that array type object
((state[key] as unknown) as any[]) = ((state[key] as unknown) as any[]).concat(val);
}
然而,这并不能解决转换的丑陋问题,因为打字稿不确定 T[K]
是数组类型。是否可以告诉它只允许我的 key
参数是 T
上的数组类型?
方法 #1
/**
* Push into the existing array.
*/
export const mutateConcat1 = <
TState extends Record<TKey, unknown[]>,
TKey extends string,
>(
state: TState,
key: TKey,
newArray: TState[TKey]
) => {
state[key].push(...newArray);
};
方法 #2
这里as
的演员阵容可能是不可避免的。如果没有它,我们会收到,Type 'unknown[]' is not assignable to type 'TState[TKey]'. 那是因为我们告诉编译器数组的类型是 unknown[]
在 Record
。我尝试 infer
数组类型但无法使其工作。不过,考虑到我们确定 state[key]
和 newArray
属于同一类型,转换看起来非常安全。要是我知道如何向编译器解释就好了。
/**
* Overwrite the original array with concat.
*/
export const mutateConcat2 = <
TState extends Record<TKey, unknown[]>,
TKey extends string,
>(
state: TState,
key: TKey,
newArray: TState[TKey]
) => {
state[key] = state[key].concat(newArray) as TState[TKey];
};
测试
/**
* Test Cases
*/
const goodState = { array1: [1, 2, 3] };
const badState = { array1: new Date() };
// ok
mutateConcat1(goodState, 'array1', [4, 5, 6]);
mutateConcat2(goodState, 'array1', [4, 5, 6]);
// errors
mutateConcat1(goodState, 'array1', ['4', 5, 6]);
mutateConcat1(goodState, 'array2', [4, 5, 6]);
mutateConcat1(badState, 'array1', [4, 5, 6]);
mutateConcat2(goodState, 'array1', ['4', 5, 6]);
mutateConcat2(goodState, 'array2', [4, 5, 6]);
mutateConcat2(badState, 'array1', [4, 5, 6]);
另一个答案是正确的,但我想补充一点,如果你使 state
成为一个相对具体的 Record<K, U[]>
类型,而不是更通用的 T extends Record<K, U[]>
类型。这有助于编译器认为赋值是安全的,因为 T extends {a: string[]}
可能是 {a: Array<"lit">}
,然后 obj.a.push("oops")
将是一个错误:
const mutateConcat = <K extends keyof any, U>(
state: Record<K, U[]>,
key: K,
val: U[]
) => {
state[key] = state[key].concat(val); // okay
};
我看到的唯一问题是,如果您在调用 mutateConcat()
时将对象文字传递给 state
参数,您可能会在非数组属性上得到不受欢迎的 excess property checking:
mutateConcat({ a: [1, 2, 3], b: 1 }, "a", [4, 5, 6]); // excess property checking
我对 mutateConcat()
的签名所做的任何事情都会导致编译器在实现内部过于混乱,你又回到了 casting asserting。如果你仍然想走这条路,你可以通过不使用新的对象文字来避免过多的 属性 检查,如:
const obj = { a: [1, 2, 3], b: 1 };
mutateConcat(obj, "a", [4, 5, 6]); // okay
这是否满足您的用例取决于您。好的,祝你好运!
所以,在这个和 reddit 线程之间,我相信我有满足我需求的最佳解决方案。这是我想出的:
const mutateConcat = <
TState extends Record<TKey, any[]>,
TKey extends keyof TState,
TElement extends TState[TKey] extends Array<infer TEl> ? TEl : never
>(state: TState, key: TKey, val: T[K], elementProperty?: keyof TElement) => {
state[key].push(...val);
if (elementProperty) {
// filter state[key] to have distinct values off of element property
}
}
我选择在 TState
中使用 any[]
因为当我尝试 TElement[]
时还有其他问题,因为状态可以有任何类型的属性并且它没有对齐以它被调用的方式。这个方法实际上被用作 vuex 突变方法作为高阶函数,所以我不关心 state 参数的极端类型安全。 keyof
参数是重要的参数,因为它们将由开发人员传递,并且此解决方案强制要求这两个参数都对传递的对象有效。
编辑:这里是所有感兴趣的人的完整功能:
const mutateConcat = <
TState extends Record<TKey, any[]>,
TKey extends keyof TState,
TElement extends TState[TKey] extends Array<infer TEl> ? TEl : never
>(
key: TKey,
sessionKey?: string,
distinctProperty?: keyof TElement,
) => (state: TState, val: TElement[]) => {
state[key].push(...val);
if (distinctProperty) {
removeDuplicates(state[key], distinctProperty);
}
if (sessionKey) {
setSessionItem(sessionKey, state);
}
};
我已经构建了一个方法,它将一个数组与传入的另一个数组连接起来。该数组位于一个名为 state
的父对象上,该对象通常在我的方法中键入。
我的方法是这样的:
export const mutateConcat = <T, K extends keyof T>(state: T, key: K, val: T[K]) => {
((state[key] as unknown) as any[]) = ((state[key] as unknown) as any[]).concat(val);
}
显然这很丑陋,我想摆脱对 unknown
和 any[]
的强制转换。我还希望能够确定数组内容的类型。
基本上,我需要确保 T[K] 是一个数组,理想情况下我想将数组内容的类型分配给另一个通用参数,比如 U
。
我试过 <T, K extends keyof T, U extends Array<T[K]>>
,但它会将 U
分配给数组本身的类型,而不是数组内容的类型。
我问的可能吗? extends keyof
是否可以仅过滤为数组类型的键,以及这些数组内容的类型是否可以分配给另一个类型参数?
提前致谢
编辑:
因此,我设法通过关闭 val
变量来获取数组的类型。所以如下:
export const mutateConcat = <T, U, K extends keyof T>(state: T, key: K, val: T[K] & U[], arrayDistinctProperty?: keyof U) => {
// U now contains the array type, and `arrayDistinctProperty` is typed to a property on that array type object
((state[key] as unknown) as any[]) = ((state[key] as unknown) as any[]).concat(val);
}
然而,这并不能解决转换的丑陋问题,因为打字稿不确定 T[K]
是数组类型。是否可以告诉它只允许我的 key
参数是 T
上的数组类型?
方法 #1
/**
* Push into the existing array.
*/
export const mutateConcat1 = <
TState extends Record<TKey, unknown[]>,
TKey extends string,
>(
state: TState,
key: TKey,
newArray: TState[TKey]
) => {
state[key].push(...newArray);
};
方法 #2
这里as
的演员阵容可能是不可避免的。如果没有它,我们会收到,Type 'unknown[]' is not assignable to type 'TState[TKey]'. 那是因为我们告诉编译器数组的类型是 unknown[]
在 Record
。我尝试 infer
数组类型但无法使其工作。不过,考虑到我们确定 state[key]
和 newArray
属于同一类型,转换看起来非常安全。要是我知道如何向编译器解释就好了。
/**
* Overwrite the original array with concat.
*/
export const mutateConcat2 = <
TState extends Record<TKey, unknown[]>,
TKey extends string,
>(
state: TState,
key: TKey,
newArray: TState[TKey]
) => {
state[key] = state[key].concat(newArray) as TState[TKey];
};
测试
/**
* Test Cases
*/
const goodState = { array1: [1, 2, 3] };
const badState = { array1: new Date() };
// ok
mutateConcat1(goodState, 'array1', [4, 5, 6]);
mutateConcat2(goodState, 'array1', [4, 5, 6]);
// errors
mutateConcat1(goodState, 'array1', ['4', 5, 6]);
mutateConcat1(goodState, 'array2', [4, 5, 6]);
mutateConcat1(badState, 'array1', [4, 5, 6]);
mutateConcat2(goodState, 'array1', ['4', 5, 6]);
mutateConcat2(goodState, 'array2', [4, 5, 6]);
mutateConcat2(badState, 'array1', [4, 5, 6]);
另一个答案是正确的,但我想补充一点,如果你使 state
成为一个相对具体的 Record<K, U[]>
类型,而不是更通用的 T extends Record<K, U[]>
类型。这有助于编译器认为赋值是安全的,因为 T extends {a: string[]}
可能是 {a: Array<"lit">}
,然后 obj.a.push("oops")
将是一个错误:
const mutateConcat = <K extends keyof any, U>(
state: Record<K, U[]>,
key: K,
val: U[]
) => {
state[key] = state[key].concat(val); // okay
};
我看到的唯一问题是,如果您在调用 mutateConcat()
时将对象文字传递给 state
参数,您可能会在非数组属性上得到不受欢迎的 excess property checking:
mutateConcat({ a: [1, 2, 3], b: 1 }, "a", [4, 5, 6]); // excess property checking
我对 mutateConcat()
的签名所做的任何事情都会导致编译器在实现内部过于混乱,你又回到了 casting asserting。如果你仍然想走这条路,你可以通过不使用新的对象文字来避免过多的 属性 检查,如:
const obj = { a: [1, 2, 3], b: 1 };
mutateConcat(obj, "a", [4, 5, 6]); // okay
这是否满足您的用例取决于您。好的,祝你好运!
所以,在这个和 reddit 线程之间,我相信我有满足我需求的最佳解决方案。这是我想出的:
const mutateConcat = <
TState extends Record<TKey, any[]>,
TKey extends keyof TState,
TElement extends TState[TKey] extends Array<infer TEl> ? TEl : never
>(state: TState, key: TKey, val: T[K], elementProperty?: keyof TElement) => {
state[key].push(...val);
if (elementProperty) {
// filter state[key] to have distinct values off of element property
}
}
我选择在 TState
中使用 any[]
因为当我尝试 TElement[]
时还有其他问题,因为状态可以有任何类型的属性并且它没有对齐以它被调用的方式。这个方法实际上被用作 vuex 突变方法作为高阶函数,所以我不关心 state 参数的极端类型安全。 keyof
参数是重要的参数,因为它们将由开发人员传递,并且此解决方案强制要求这两个参数都对传递的对象有效。
编辑:这里是所有感兴趣的人的完整功能:
const mutateConcat = <
TState extends Record<TKey, any[]>,
TKey extends keyof TState,
TElement extends TState[TKey] extends Array<infer TEl> ? TEl : never
>(
key: TKey,
sessionKey?: string,
distinctProperty?: keyof TElement,
) => (state: TState, val: TElement[]) => {
state[key].push(...val);
if (distinctProperty) {
removeDuplicates(state[key], distinctProperty);
}
if (sessionKey) {
setSessionItem(sessionKey, state);
}
};