如何根据提供给函数的键、值和形状缩小联合类型?
How can I narrow a union type based on a key, value and a shape provided to a function?
我想根据提供给函数的参数来区分联合类型,但出于某种原因,我不能对数据形状使用通用类型。它打破了我的狭窄。您认为我怎样才能做到这一点?
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 playground
上对其进行调试
你试图做一种反向歧视,这真的不可能。这是因为您的 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
,但如果该字符串不是有效键,它将 return unknown
,基本上让您知道预期用途是错误的。您也可以根据需要将其配置为 return undefined 或其他内容。
Here is a lot of examples/the code on Playground
这是我进行这种类型推断的个人方法,如果有人有更好的方法,我很乐意听听。
在接下来的内容中,我将更改您的类型参数的名称,使其更符合 TypeScript 约定(单个大写字符); Map
将变为 M
,Tag
将变为 K
(因为它是 M
的 key), 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>
使用 the Record<K, V>
utility type 并且
type DiscriminateUnionType<M, K extends keyof M, V extends M[K]> =
M extends Record<K, V> ? M : never;
可以省去 built-in Extract<T, U>
utility type 作为 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
那么你根本不需要注释 return 类型,因为它将被推断为 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
应该是完整的 discriminated union type, so you can't constrain 到 Record<K, V>
,因为 V
只是键 [=20] 的各种可能值之一=].如果您将 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 中,将其重写为 user defined type guard function 更为传统,其中 inStateOfType()
return 是一个 boolean
,可用于决定是否编译器可能会将 state
缩小为 Record<K, V>
或不缩小:
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
}
}
我想根据提供给函数的参数来区分联合类型,但出于某种原因,我不能对数据形状使用通用类型。它打破了我的狭窄。您认为我怎样才能做到这一点?
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 playground
上对其进行调试你试图做一种反向歧视,这真的不可能。这是因为您的 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
,但如果该字符串不是有效键,它将 return unknown
,基本上让您知道预期用途是错误的。您也可以根据需要将其配置为 return undefined 或其他内容。
Here is a lot of examples/the code on Playground
这是我进行这种类型推断的个人方法,如果有人有更好的方法,我很乐意听听。
在接下来的内容中,我将更改您的类型参数的名称,使其更符合 TypeScript 约定(单个大写字符); Map
将变为 M
,Tag
将变为 K
(因为它是 M
的 key), 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>
使用 the Record<K, V>
utility type 并且
type DiscriminateUnionType<M, K extends keyof M, V extends M[K]> =
M extends Record<K, V> ? M : never;
可以省去 built-in Extract<T, U>
utility type 作为 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
那么你根本不需要注释 return 类型,因为它将被推断为 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
应该是完整的 discriminated union type, so you can't constrain 到 Record<K, V>
,因为 V
只是键 [=20] 的各种可能值之一=].如果您将 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 中,将其重写为 user defined type guard function 更为传统,其中 inStateOfType()
return 是一个 boolean
,可用于决定是否编译器可能会将 state
缩小为 Record<K, V>
或不缩小:
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
}
}