我想根据提供给函数的参数来区分联合类型,但由于某种原因,我不能对数据形状使用泛型类型。它打破了我的狭窄。您认为我该如何实现这一目标?
export type DiscriminateUnionType<Map, Tag extends keyof Map, TagValue extends Map[Tag]> = Map extends Record<
Tag,
TagValue
>
? Map
: never;
function inStateOfType<Map extends { [index in Tag]: TagValue }, Tag extends keyof Map, TagValue extends Map[Tag], DiscriminatedState extends DiscriminateUnionType<Map, Tag, TagValue>>(tag: Tag, value: TagValue, state: Map): DiscriminatedState | undefined {
return state[tag] === value ? state as DiscriminatedState : undefined
}
type State = { type: 'loading', a: string } | { type: 'loaded', b: string } | { type: 'someOtherState', c: string }
export function main(state: State) {
const loadedState = inStateOfType('type', 'loading', state)
if (loadedState) {
loadedState.b // Property 'b' does not exist on type 'State'. Property 'b' does not exist on type '{ type: "loading"; a: string; }'
}
}
function inStateOfType<Map extends { type: string }, Tag extends 'type', TagValue extends Map[Tag], DiscriminatedState extends DiscriminateUnionType<Map, Tag, TagValue>>(state: Map, value: TagValue): DiscriminatedState | undefined {
return state['type'] === value ? state as DiscriminatedState : undefined
}
function main(state: State) {
// { type: "loaded"; b: string }, everything is fine, narrowing works
// but in this case, inStateOfType function is not generic
const loadedState = inStateOfType(state, 'loaded')
if (loadedState) {
loadedState.b
}
}
为了调查这一点,我创建了一个带有代码的可执行片段,因此您可以在 TS 操场上调试它
在下文中,我将更改类型参数的名称,使其更符合 TypeScript 约定(单个大写字符);Map
会变成M
,Tag
会变成K
(因为它是M
的ky),TagValue
会变成V
,index
会变成I
,DiscriminatedState
会变成S
。 所以现在我们有:
function inStateOfType<
M extends { [I in K]: V },
K extends keyof M,
V extends M[K],
S extends Extract<M, Record<K, V>>
>(tag: K, value: V, state: M): S | undefined {
return state[tag] === value ? state as S : undefined
}
请注意,{ [I in K]: V }
等效于使用Record<K, V>
实用程序类型的Record<K, V>
,并且
type DiscriminateUnionType<M, K extends keyof M, V extends M[K]> =
M extends Record<K, V> ? M : never;
可以省略,转而使用内置的Extract<T, U>
实用程序类型作为Extract<M, Record<K, V>>
,所以现在我们有:
function inStateOfType<
M extends Record<K, V>,
K extends keyof M,
V extends M[K], S extends Extract<M, Record<K, V>>
>(tag: K, value: V, state: M): S | undefined {
return state[tag] === value ? state as S : undefined
}
我们几乎完成了清理,直到我们可以回答。 还有一件事;S
类型参数是多余的。它没有好的推理站点(没有参数是S
类型或S
函数),所以编译器只会回退到S
完全Extract<M, Record<K, V>>
,这意味着它只是它的同义词。
如果你要写return xxx ? yyy as S : undefined
那么你根本不需要注释返回类型,因为它将被推断为S | undefined
。
因此,您可以编写以下内容,并使所有工作(或无法正常工作)相同:
function inStateOfType<
M extends Record<K, V>,
K extends keyof M,
V extends M[K]
>(tag: K, value: V, state: M) {
return state[tag] === value ?
state as Extract<M, Record<K, V>> :
undefined
}
那为什么不起作用呢? 这里最大的问题是M
应该是完全可区分的联合类型,所以你不能将其限制为Record<K, V>
,因为V
只是键K
的各种可能值之一。 如果将M
限制为Record<K, V>
,则编译器不会允许您传入state
的值,除非它已经知道其tag
属性与value
的类型相同。 或者,就像您的情况一样,编译器将扩大V
,使其成为tag
的全套可能性。 哎呀。
那么,如果我们不能约束M
Record<K, V>
,我们应该约束它做什么呢?它需要一个键在K
,但那里的值类型应该只被约束为一个可行的判别性质。 类似的东西
type DiscriminantValues = string | number | boolean | null | undefined;
让我们试试吧:
function inStateOfGenericType<
M extends Record<K, DiscriminantValues>,
K extends keyof M,
V extends M[K]
>(tag: K, value: V, state: M) {
return state[tag] === value ?
state as Extract<M, Record<K, V>> :
undefined
}
function main(state: State) {
const loadedState = inStateOfGenericType('type', 'loaded', state)
if (loadedState) {
loadedState.b // okay
}
}
这就行了!
请注意,在 TypeScript 中,将其重写为用户定义的类型保护函数会更传统一些,其中inStateOfType()
返回一个可用于决定编译器是否可以将state
缩小到Record<K, V>
的boolean
:
function inStateOfGenericType<
M extends Record<K, DiscriminantValues>,
K extends keyof M,
V extends M[K]
>(tag: K, value: V, state: M):
state is Extract<M, Record<K, V>> {
return state[tag] === value
}
function main(state: State) {
if (inStateOfGenericType('type', 'loaded', state)) {
state.b // okay
}
}
操场链接到代码
你试图做一个反向歧视,这真的是不可能的。这是因为您的TagValue extends Map[Tag]
,不幸的是,Tag = type
将始终产生联合'loaded' | 'loading' | 'someOtherValue'
。当你extend Map[Tag]
它本身就是一种缩小,所以 TS 不会进一步缩小它,因为'loaded'
会完成它通过那里的联合,但随后继续将整个联合传递给DiscriminatedState
。
因此,当它尝试在DiscriminateUnionType<>
中使用TagValue
时,它将在整个联合中通过,并且根本不会真正缩小它。
与其TagValue extends Map[Tag]
,不如允许任何字符串,然后有条件地稍后检查它。这样,您的缩小发生在泛型之外,TS 可能会错误地推断和缩小它。
function inStateOfGenericTypeFix<
Map extends Record<string, any>,
Tag extends keyof Map,
TagValue extends string = Map[Tag],
> (tag: Tag, value: TagValue, state: Map):
TagValue extends Map[Tag]
? DiscriminateUnionType<Map, Tag, typeof value>
: unknown
| undefined
{
return state[tag] === value ? state as any : undefined
}
const loadedState = inStateOfGenericTypeFix('type', 'loaded', state)
// ^?
if (loadedState) {
loadedState.b //No more error!
}
不幸的是,这意味着您可以将任何字符串传递给value
,但如果字符串不是有效的键,它将返回unknown
,基本上让您知道预期的用途是错误的。您还可以根据需要将其配置为返回未定义或其他内容。
这是很多例子/操场上的代码
这是我个人进行这种类型推理的方法,如果其他人有更好的方法,我很想听听。