typescript - 防止函数调用中的类型冲突
我有很多像这两个这样的接口:
interface A {
type: 'A';
data: {
a: string;
};
}
interface B {
type: 'B';
data: {
b: string;
};
}
而我有一种类型可以将它们全部联合起来:
type Item = A | B;
我想创建一个函数,将新项目推送到定义为 const items: Item[]=[]; 的数组中。
到目前为止,我有这个:
const create = <I extends Item>(type: I['type'], data: I['data']) => items.push({type, data});
然而,我本以为这会引起类型错误,因为 type 和 data 的类型冲突,但事实并非如此:
create('A', {b: '1'});
我如何定义 create,以使不允许出现冲突的 type 和 data 值?
请注意,在我的实际代码中,我的接口中有比由我的 create 函数生成的 type 和 data 更多的字段,所以我不能简单地将 Item 作为参数。
提前感谢您的帮助!
答案#1
这里的问题是,在调用 create() 时,没有合适的 类型推断候选 来推断 I。你的调用签名要求编译器根据类型为 I['type'] 和 I['data'] 的值来推断 I,而编译器根本没有能力做到这一点。曾经在 microsoft/TypeScript#20126 中有一些工作尝试实现这一点,但出于某种原因从未合并。由于 I 未被推断,编译器放弃,I 被扩大为其约束类型,即 Item:
create('A', { b: '1' });
/* const create: <Item>(type: "A" | "B", data: {
a: string;
} | {
b: string;
}) => number */
并且没有错误,因为 "A" 肯定可以赋值给 Item["type"],而 {b: '1'} 也肯定可以赋值给 Item["data"]。这并不是你想要的,因此这个调用签名对你来说将不起作用。
编译器在从类型为 X 的值推断类型参数 X 时效果最好(而不是从类型为 X['someProperty'] 的值推断)。所以我们来做这个,并且 计算 你需要的类型:
const create = <K extends Item["type"]>(
type: K, data: Extract<Item, { type: K }>['data']
) => items.push({ type, data } as Item);
在这里,我们从一个类型为 K 的值 type 中推断 K。这被限制为 Item["type"]。然后,我们通过 Extract 联合类型中 type 属性类型为 K 的成员来计算 data 所需的类型,然后查看其 data 属性。这样就能达到预期效果:
create("A", { a: "1" }); // okay
create("B", { b: "1" }); // okay
create("A", { b: "1" }); // error!
// ---------> ~~~~~~
// Argument of type '{ b: string; }' is not assignable to
// parameter of type '{ a: string; }'
这里的另一种方法是放弃泛型,只让函数接受一个剩余参数,其类型是由 Item 推导出的剩余元组的联合类型:
type CreateArgs = Item extends infer I ? I extends Item ?
[type: I['type'], data: I['data']] : never : never;
/* type CreateArgs = [type: "A", data: {
a: string;
}] | [type: "B", data: {
b: string;
}] */
在这里,我使用条件类型将参数列表操作分配到 Item 联合上。现在 create() 看起来可能是这样的:
const create = (...[type, data]: CreateArgs) =>
items.push({ type, data } as Item);
它的行为与以前类似。从调用者的角度来看,就好像 create() 是一个重载函数:
create("A", { a: "1" }); // okay
create("B", { b: "1" }); // okay
create("A", { b: "1" }); // error!
// ---------> ~~~~~~
// Type '{ b: string; }' is not assignable to parameter of type '{ a: string; }'
无论哪种方式对你都可以。
代码的 Playground 链接
答案#2
你可以使用判别联合的概念加上函数重载来为你实现。在这种情况下,我们可以创建一个 BaseItem 接口,它将接受两个泛型:type 和 data 属性的类型:
interface BaseItem<T, U extends { [key: string]: string }> {
type: T,
data: U
}
那么,你的项目只是这些的联合类型:
type Atype = 'A';
type Adata = { a: string };
type Btype = 'B';
type Bdata = { b: string };
type Item = BaseItem<Atype, Adata> | BaseItem<Btype, Bdata>;
您可以使用函数重载来确保 create() 函数只接受特定的组合:
function create<A>(type: Atype, data: Adata): number;
function create<B>(type: Btype, data: Bdata): number;
function create<I extends Item>(type: I['type'], data: I['data']) {
return items.push({type, data});
}
create('A', {b: '1'}); // Expected error: no overload matches this call
create('B', {a: '1'}); // Expected error: no overload matches this call
create('A', {a: '1'}); // Ok
create('B', {b: '1'}); // Ok
在 TypeScript playground 上查看概念验证。
