gpt4 book ai didi

typescript - 如何告诉 TypeScript 接口(interface) T 比具有索引签名的类型 U 窄?

转载 作者:行者123 更新时间:2023-12-01 01:39:49 28 4
gpt4 key购买 nike

我有一个函数可以验证 JSON 响应以确保它对应于给定的形状。

这是我定义所有可能的 JSON 值的类型——取自 https://github.com/microsoft/TypeScript/issues/1897#issuecomment-338650717

type AnyJson = boolean | number | string | null | JsonArray | JsonMap;
type JsonMap = { [key: string]: AnyJson };
type JsonArray = AnyJson[];

现在我有一个函数可以在给定要验证的对象和一个形状为 T 的模拟对象的情况下进行验证。 .
function isValid<T extends AnyJson>(obj: AnyJson, shape: T): obj is T {
// ... implementation
}

但是,当我尝试使用接口(interface)和真实对象调用该函数时,在 Thing 下出现类型错误。在类型参数中
interface Response {
Data: Thing[]; // Thing is an interface defined elsewhere
};

isValid<Response>(data, { Data: [] })
// ^^^^^^^^
Type 'Response' does not satisfy the constraint 'AnyJson'.
Type 'Response' is not assignable to type 'JsonMap'.
Index signature is missing in type 'Response'.

奇怪的是,当 Response 时,这不会发生。是类型而不是接口(interface),例如
type Response = {
Data: Thing[];
};

但后来我确实得到了同样的错误,但在 Thing 上更深一层。本身,它仍然是一个接口(interface):
Type 'Response' does not satisfy the constraint 'AnyJson'.
Type 'Response' is not assignable to type 'JsonMap'.
Property 'Data' is incompatible with index signature.
Type 'Thing[]' is not assignable to type 'AnyJson'.
Type 'Thing[]' is not assignable to type 'JsonArray'.
Type 'Thing' is not assignable to type 'AnyJson'.
Type 'Thing' is not assignable to type 'JsonMap'.
Index signature is missing in type 'Thing'.

所以我的问题是为什么这种预期的缩小不会发生在接口(interface)上而只发生在类型上?

最佳答案

这是一个已知问题 (see microsoft/TypeScript#15300)implicit index signatures仅针对对象字面量和 type 推断别名,而不是 interfaceclass类型。它是 currently by design ;在没有 exact types 的情况下推断隐式索引签名不是类型安全的。例如,类型为 Response 的值不知道只有 Data属性(property)。它可能具有与 AnyJson 不兼容的属性(例如,interface FunkyResponse extends Response { otherProp: ()=>void })所以编译器拒绝在那里推断隐式索引签名。对 type 执行此操作在技术上是不安全的。别名也是如此,但无论出于何种原因,一个是允许的,另一个是不允许的。如果您想看到这种变化,您可能需要转到该问题并给它一个 👍 和/或如果您认为它令人信服,请描述您的用例。实际上它看起来像 someone has mentioned this use case already .

所以,除非这个问题得到解决,否则我们能做什么?通常在这些情况下,我发现将我想要的类型表示为 generic constraint 更容易。而不是作为具体类型。索引签名改为映射类型。目标是提出一个泛型类型别名 JsonConstraint<T>这样一个有效的 JSON 类型像 Response将分配给 JsonConstraint<Response> ,但是像 Date 这样的无效 JSON 类型不能分配给 JsonConstraint<Date> .这是我可能写它的一种方式:

type JsonConstraint<T> = boolean | number | string | null | (
T extends Function ? never :
T extends object ? { [K in keyof T]: JsonConstraint<T[K]> }
: never
)

所以 T extends JsonConstraint<T>如果 T 为真是可接受的原始类型之一,如果 T 则为 false是一个函数,否则它会向下递归到 T 的属性中并检查每一个。这种递归应该适用于对象和数组,因为 TypeScript 3.1 introduced mapped tuple/array types .

现在我想写函数签名 isValid<T extends JsonConstraint<T>>(obj: AnyJson, shape: T): obj is AnyJson & T ,但这是一个 Not Acceptable 循环约束。它有时会发生。修复它的一种方法是将签名更改为 isValid<T>(obj: AnyJson, shape: T & JsonConstraint<T>): obj is AnyJson & T .这将推断 T来自 shape ,然后检查 JsonConstraint<T>仍可分配给 shape .如果是这样,那就太好了。如果不是,则该错误应提供信息。

所以这里是 isValid() :
function isValid<T>(obj: AnyJson, shape: T & JsonConstraint<T>): obj is typeof obj & T {
return null!; // impl here
}

现在让我们测试一下:
declare const data: AnyJson

declare const response: Response;
if (isValid(data, response)) {
data.Data.length; // okay
};

这样就可以正常工作,如您所愿。让我们看看它对其他类型的行为是否符合预期。我们应该不能使用 undefined作为属性类型:
isValid(data, { undefinedProp: undefined }); // error! 
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Types of property 'undefinedProp' are incompatible

或函数值属性:
isValid(data, { deeply: { nested: { property: { func: () => 1 } } } }); // error!
// Types of property 'func' are incompatible.

Date (失败是因为它有各种不可序列化的方法):
isValid(data, new Date()); // error!
// Types of property 'toString' are incompatible.

最后,我们应该可以使用 string , number , boolean , null ,以及这些没有错误的数组/对象:
isValid(data, {
str: "",
num: 1,
boo: Math.random() < 0.5,
nul: null,
arr: [1, 2, 3],
obj: { a: { b: ["a", true, null] } }
}); // no error

看起来挺好的。好的,希望有帮助;祝你好运!

Playground link to code

关于typescript - 如何告诉 TypeScript 接口(interface) T 比具有索引签名的类型 U 窄?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59620567/

28 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com