TypeScript从第二个数组元素推断第一个数组元素



假设我有一个类型为[number, 'number'][null, 'null']的变量arr。是否可以根据arr[1]的值推断出arr[0]的类型?这就是问题所在,我不认为我的用例与函数重载一起工作,因为arr[1]的值是返回它的函数的副作用。而不是arr[0]arr[1],我使用以下名称datastatus分别表示这些值。在这种情况下,data将是我试图从API获取的数据,status是API请求的状态(空闲、挂起、成功或错误)。

我认为useFetch是一个状态机。基本上,我试图让它从机器的状态(status)推断出机器上下文(data)的类型。我想深入研究XState的源代码,因为我认为他们做了类似的事情。参考XStates文档

我有下面的React钩子。参见下面的useFetch函数:

export declare namespace FetchUtil {
export type Status = 'idle' | 'pending' | 'error' | 'success'
export type Response<Data, Status> = Readonly<[Data, Status]>
export namespace States {
export type Idle = null
export type Pending = null
export type Error = { messgage: string }
export type Success<T> = T
}
}
interface Config extends Omit<RequestInit, 'body'> {
body?: Record<string, any>
}
export async function fetchUtil<T> (url: string, config?: Config): Promise<T | null> {
let args: RequestInit = {}
const { body, ...restConfig } = config ?? {}

if (body) {
args.body = JSON.stringify(body)
}

const response = await fetch(url, {
cache: 'no-cache',
...restConfig,
...args,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
...restConfig.headers,
...args.headers,
}
})
if (response.status >= 400) {
const { error } = await response.json()
throw new Error(error ?? 'failed to fetch')
}
return await response.json()
}
interface UseFetchConfig extends Config {
enabled?: boolean
}
export function useFetch<T>(url: string, config?: UseFetchConfig) {
const [status, setStatus] = React.useState<FetchUtil.Status>('idle')
const [data, setData] = React.useState<T | FetchUtil.States.Error>()
React.useEffect(() => {
setStatus('idle')
if (config?.enabled === false) {
return
}
setStatus('pending')
fetchUtil<T>(url, config)
.then(res => {
if (res !== null) {
setData(res)
setStatus('success')
} else {
setData({
messgage: 'not found'
})
setStatus('error')  
}
})
.catch(err => {
setData(err)
setStatus('error')
})
}, [url, config?.enabled])
switch (status) {
case 'idle':
return [null, status] as FetchUtil.Response<FetchUtil.States.Idle, typeof status>
case 'pending':
return [null, status] as FetchUtil.Response<FetchUtil.States.Pending, typeof status>
case 'success':
return [data, status] as FetchUtil.Response<FetchUtil.States.Success<T>, typeof status>
case 'error':
return [data, status] as FetchUtil.Response<FetchUtil.States.Error, typeof status>
}
}

我可以这样使用钩子:

function Tweet({ id }) {
const [tweet, status] = useFetch<API.Tweet>(urls.api(`/tweets/${id}`))
React.useEffect(() => {
if (status === 'idle') {
// TS should infer that type of tweet is FetchUtil.States.Idle
}
else if (status === 'pending') {
// TS should infer that type of tweet is FetchUtil.States.Pending
}
else if (status === 'success') {
// TS should infer that type of tweet is FetchUtil.States.Success<API.Tweet>
}

else if (status === 'error') {
// TS should infer that type of tweet is FetchUtil.States.Error
}
}, [tweet, status])
// ...
}

问题是TS没有根据status的类型检测tweet的类型。是否有可能根据第二个数组项的类型推断出第一个数组项的类型?提前谢谢你。

这个答案将使用顶部的示例而不是底部的示例;如果你愿意,你可以在那里应用它。

假设您有以下元组类型的并集:

type Arr = [number, 'number'] | [null, 'null'];

这样的联合被认为是歧视联合;索引1处的属性是判别符,因为您可以使用它来判断一个值满足联合的哪个成员。(只有当你检查的属性是单元类型时,这种区分才有效:也就是说,只有一个值可分配的类型:如"number"123trueundefinednull,而不是numberstring。)

这意味着您应该确实能够通过检查arr[1]来将类型为Arr的值arr缩小到合适的联合成员:

function f(arr: Arr) {
switch (arr[1]) {
case "null": {
const [data, status] = arr;
return "It's null";
}
case "number": {
const [data, status] = arr;
return data.toFixed(2);
}
}
}

注意不能做的是在执行此检查之前将arr解构为datastatus。这是因为一旦这样做,datastatus将被编译器视为不相关的联合类型。它知道datanumber | null,status"number" | "null",但是如果status"number",它无法跟踪data不能是null。歧视工会的性质丧失了。

如果编译器有某种方式来表示相关的联合是很好的,虽然我已经提交了一个问题,microsoft/TypeScript#30581,希望这样的事情,但在可预见的未来,它不太可能在TypeScript中实现。相反,我建议采用上面的解决方法:直接检查arr[1],然后在需要时解构,每次检查arr的作用域一次。这是多余的,但至少编译器可以验证类型安全性。

Playground链接到代码

最新更新