我有以下用于跟踪异步请求状态的代码。它使用_type
和status
作为鉴别符。
在以下代码中,我定义了两种AsyncStatus类型:LoginAsyncStatus
和SearchAsyncStatus
。它们的区别在于_type
和success
value
。
问题在于TypeScript似乎错误地缩小了所区分联合的类型。
export type AsyncStatus<BrandT extends string, T = undefined> =
| { id: string; _type: BrandT; error?: never; state: "loading"; value?: never; }
| { id: string; _type: BrandT; error: Error; state: "error"; value?: never }
| { id: string; _type: BrandT; error?: never; state: "success"; value: T };
export type ExtractAsyncStatusByType<
TName extends ApiAsyncStatus["_type"],
TType
> = TType extends AsyncStatus<TName, any> ? TType : never;
export type LoginAsyncStatus = AsyncStatus<"LOGIN", { refreshToken: string }>;
export type SearchAsyncStatus = AsyncStatus<"SEARCH", string[]>;
export type ApiAsyncStatus = LoginAsyncStatus | SearchAsyncStatus;
export type Registry = Partial<Record<ApiAsyncStatus["id"], ApiAsyncStatus>>;
export const getApiAsyncStatus = <T extends ApiAsyncStatus["_type"]>(
registry: Registry,
id: string,
type: T,
): ExtractAsyncStatusByType<T, ApiAsyncStatus> | undefined => {
let status = registry[id];
if (status !== undefined && status._type !== type) {
/**
* Property 'value' is missing in type
* '{ _type: T; error: Error; id: string; state: "error"; }'
* but required in type
* '{ id: string; _type: "SEARCH"; error?: undefined; state: "success"; value: string[]; }'
* .ts(2322)
*/
status = {
_type: type,
error: new Error(`Expected _type ${type}, but received ${status._type}`),
id,
state: "error",
}; // err
}
return status as ExtractAsyncStatusByType<T, ApiAsyncStatus> | undefined;
};
我已经更新了最初的问题,其中的问题是在我不尝试动态创建状态的情况下返回适当的类型。
答案 0 :(得分:0)
我将重复我的评论并从那里继续:
区别联合仅在您希望使用具体类型时才真正起作用,而不是泛型。此外,在infer_datetime_format
的实现中,类型getApiAsyncStatus()
是一个无法解析的泛型参数,并且编译器在尝试验证值是否可以分配给依赖于此条件类型的条件类型方面没有做太多工作未解决的泛型。您最好的选择是只使用type assertion或类似的overload签名。这种条件类型的好处是给调用者,而不是实现者。
如果您将TypeScript 3.5与smarter union type checking配合使用,则可以像上面这样修改您的代码:
T
从export const getApiAsyncStatus = <T extends ApiAsyncStatus["_type"]>(
registry: Registry,
id: string,
type: T,
) => {
let status = registry[id];
if (status !== undefined && status._type !== type) {
// annotate as error state
const errorStatus: Extract<ApiAsyncStatus, { state: "error" }> = {
_type: type as ApiAsyncStatus["_type"], // widen to concrete union
error: new Error(`Expected _type ${type}, but received ${status._type}`),
id,
state: "error"
};
status = errorStatus; // this assignment is okay
}
// still need this assertion
return status as ExtractAsyncStatusByType<T, ApiAsyncStatus> | undefined;
};
到type
的{{1}}的扩展将其从通用类型(缺乏编译器的演绎技能)改变为具体的并集(更好)。为了使编译器理解T
类型的值可分配给ApiAsyncStatus["_type"]
类型的变量,必须使用TS3.5中的更智能的联合检查。对于TS3.4及以下版本,编译器would not do such analysis at all。因此,即使是上述版本,在那些早期版本中仍然会出错。
要支持这些内容,您还可以使您的断言更宽泛,输入类型更安全:
{_type: A | B, error: Error, state: "error"}
因此,根据您使用的TypeScript的版本,这两种方法均应工作。我倾向于将此问题视为与correlated types相关的一般问题,如果您可以convince the compiler多次键入检查代码块的次数,每次缩小一些范围,事情都会顺利进行联合类型的变量。对于您的情况,对于{_type: A, error: Error, state: "error"} | {_type: B, error: Error, state: "error"}
(此处为export const getApiAsyncStatus = <T extends ApiAsyncStatus["_type"]>(
registry: Registry,
id: string,
type: T,
) => {
let status = registry[id];
if (status !== undefined && status._type !== type) {
status = {
_type: type,
error: new Error(`Expected _type ${type}, but received ${status._type}`),
id,
state: "error"
} as Extract<ApiAsyncStatus, { state: "error", _type: T }>; // assert as error type
}
// still need this assertion
return status as ExtractAsyncStatusByType<T, ApiAsyncStatus> | undefined;
};
和T
)的每个可能值,您的代码都应检查正常。但是,当“一次”查看联合或联合的通用扩展时,编译器认为某些被禁止的情况是可能的,并且不予理and。恐怕没有很好的答案。我的建议是断言您的出路并继续前进。
好的,希望能有所帮助;祝你好运!