英文:
Is it possible to make T[K] extend Type?
问题
通用情况
在一个函数中
const myFunction = <T, K extends keyof T>(obj: T, key: K): SpecificType => {
const x: SpecificType = obj[key] // ❌ Type 'T[string]' is not assignable to type SpecificType
}
是否可能以这样的方式定义myFunction(obj, key)
,以确保obj[key] extends SpecificType
?
示例
考虑以下函数:
type KeysMatching<T, V> = {
[K in keyof T]-?: T[K] extends V ? K : never
}[keyof T]
export const groupArrayBy = <
T extends Record<string, unknown>,
K extends KeysMatching<T, string | number | symbol>
>(
array: T[],
key: K
) => {
return array.reduce((acc, item) => {
const group = item[key]
if (!acc[group]) {
acc[group] = []
}
acc[group].push(item)
return acc
}, {} as Record<T[K], T[]>) // ❌ Type 'T[string]' is not assignable to type 'string | number | symbol'
}
TypeScript 抱怨定义{} as Record<T[K], T[]>
,因为T[K]
是unknown
(由T extends Record<string, unknown>
给出)。但实际上,它不是,T[K]
应该只是string | number | symbol
,因为K extends KeysMatching<T, string | number | symbol>
(此辅助类型从此问题中提取而来)
事实上,如果我调用
const t = [
{
valid: 'string',
alsoValid: 1,
group: 2,
invalid: [],
outroInvalid: {}
}
]
groupArrayBy(t, 'valid') // ✅ 一切正常
groupArrayBy(t, 'invalid') // ❌ 不正常,如预期所述
即使如此,TypeScript 也无法告诉,在对groupArrayBy([obj], key)
进行任何有效调用时,obj[key]
将始终是string | number | symbol
,这应该与Record<T[K], T[]>
兼容。
英文:
Generic Case
In a function
const myFunction = <T, K extends keyof T>(obj: T, key: K): SpecificType => {
const x: SpecificType = obj[key] // ❌ Type 'T[string]' is not assignable to type SpecificType
}
Is it possible to define myFunction(obj, key)
in such a way I can grant that obj[key] extends SpecificType
?
Example
Given the following function:
type KeysMatching<T, V> = {
[K in keyof T]-?: T[K] extends V ? K : never
}[keyof T]
export const groupArrayBy = <
T extends Record<string, unknown>,
K extends KeysMatching<T, string | number | symbol>
>(
array: T[],
key: K
) => {
return array.reduce((acc, item) => {
const group = item[key]
if (!acc[group]) {
acc[group] = []
}
acc[group].push(item)
return acc
}, {} as Record<T[K], T[]>) // ❌ Type 'T[string]' is not assignable to type 'string | number | symbol'
}
Typescript complains about defining {} as Record<T[K], T[]>
because T[K]
is unknown
(as given by T extends Record<string, unknown>
). But, in reality, it is not, T[K]
should only be string | number | symbol
, because K extends KeysMatching<T, string | number | symbol>
(This auxiliary type was extracted from this question)
In fact, if I call
const t = [
{
valid: 'string',
alsoValid: 1,
group: 2,
invalid: [],
outroInvalid: {}
}
]
groupArrayBy(t, 'valid') // ✅ everything ok
groupArrayBy(t, 'invalid') // ❌ not ok, as expected
And, even so, typescript cannot tell that, in any valid call to groupArrayBy([obj], key)
, obj[key]
will always be string | number | symbol
, which should be compatible with Record<T[K], T[]>
答案1
得分: 2
TypeScript 无法对依赖通用类型参数的条件类型进行深入分析,比如 KeysMatching<T, PropertyKey>
在 groupArrayBy
函数内部。 这对编译器来说更像是不透明的;它了解到 K extends KeysMatching<T, V>
将是 T
的有效键类型,但它丢失了 T[K]
将能分配给 V
的线索。 如果能有一个"本地"的 KeysMatching<T, V>
实现以保留这种意图,就会很好,正如在 microsoft/TypeScript#48992 中请求的那样,但目前它不是语言的一部分,所以我们需要解决这个问题。
类型检查器在理解通用索引访问进入映射类型方面做得更好,因此我建议的解决方法是反转约束 并使 T
依赖于 K
,如 Record<K, PropertyKey>
(使用 Record<K, V> 实用类型,它是一个形式为 {[P in K]: V}
的映射类型):
export const groupArrayBy = <
T extends Record<K, PropertyKey>,
K extends keyof T
>(
array: T[],
key: K
) => {
return array.reduce((acc, item) => {
const group = item[key]
if (!acc[group]) {
acc[group] = []
}
acc[group].push(item)
return acc
}, {} as Record<T[K], T[]>) // okay
}
英文:
TypeScript is unable to do much analysis on conditional types that depend on generic type parameters, like KeysMatching<T, PropertyKey>
inside the body of groupArrayBy
. It's more or less opaque to the compiler; it understands that K extends KeysMatching<T, V>
will be a valid key type for T
, but it loses the thread that T[K]
will be assignable to V
. It would be nice if there were a "native" KeysMatching<T, V>
implementation that preserved the intent this way, as requested in microsoft/TypeScript#48992, but for now it's not part of the language, so we'll need to work around it.
The type checker is better at understanding generic indexed accesses into mapped types, so the workaround I'd suggest is to reverse the constraint and make T
depend on K
like Record<K, PropertyKey>
(using the Record<K, V>
utility type which is a mapped type of the form {[P in K]: V}
):
export const groupArrayBy = <
T extends Record<K, PropertyKey>,
K extends keyof T
>(
array: T[],
key: K
) => {
return array.reduce((acc, item) => {
const group = item[key]
if (!acc[group]) {
acc[group] = []
}
acc[group].push(item)
return acc
}, {} as Record<T[K], T[]>) // okay
}
interface Foo {
a: string,
b: number,
c: boolean
}
declare const foos: Foo[]
groupArrayBy(foos, "a"); // okay
groupArrayBy(foos, "b"); // okay
groupArrayBy(foos, "c"); // error
// --------> ~~~~
// Argument of type 'Foo[]' is not assignable to
// parameter of type 'Record<"c", PropertyKey>[]'.
groupArrayBy(foos, "d"); // error
// --------> ~~~~
// Argument of type 'Foo[]' is not assignable to
// parameter of type 'Record<keyof Foo, PropertyKey>[]'.
That works without even using KeysMatching
. The only downside there might be that the errors are on the array
when you might expect to see them on the key
. If you care, then you can keep the original KeysMatching
constraint on K
as well:
export const groupArrayBy = <
T extends Record<K, PropertyKey>,
K extends KeysMatching<T, PropertyKey>
>(
array: T[],
key: K
) => {
return array.reduce((acc, item) => {
const group = item[key]
if (!acc[group]) {
acc[group] = []
}
acc[group].push(item)
return acc
}, {} as Record<T[K], T[]>) // okay
}
groupArrayBy(foos, "c"); // error
// --------------> ~~~~
// Argument of type '"c"' is not assignable to
// parameter of type 'KeysMatching<Foo, PropertyKey>'.(2345)
groupArrayBy(foos, "d"); // error
// --------------> ~~~~
// Argument of type '"d"' is not assignable to
// parameter of type 'KeysMatching<Foo, PropertyKey>'.(2345)
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论