Typescript 是不是支持“子集类型”?
Posted
技术标签:
【中文标题】Typescript 是不是支持“子集类型”?【英文标题】:Does Typescript support "subset types"?Typescript 是否支持“子集类型”? 【发布时间】:2016-08-20 15:26:39 【问题描述】:假设我有一个接口:
interface IUser
email: string;
id: number;
phone: string;
;
然后我有一个函数需要该类型的 子集(或完全匹配)。也许它会传递整个对象,使它只会传递email: "t@g.com"
。我希望类型检查器同时允许两者。
例子:
function updateUser(user: IUser)
// Update a "subset" of user attributes:
$http.put("/users/update", user);
Typescript 是否支持这种行为?我会发现它非常有用,尤其是像 Redux 这样的范例。
为了澄清,目标是:
-
避免重写接口并手动将所有属性设置为可选。
避免分配意外属性(例如拼写错误)。
避免使用命令式逻辑,例如
if
语句,这会失去编译时类型检查的好处。
更新:Typescript 已宣布支持mapped types,一旦发布,它应该可以解决这个问题。
【问题讨论】:
不是Partial
你在找什么?
当我问这个问题时,它们并不存在。我后来添加了一个链接,在它们被添加到 TS 之后。
【参考方案1】:
您可以将部分或所有字段声明为可选字段。
interface IUser
email: string; // not optional
id?: number; // optional
phone?: string; // optional
;
【讨论】:
这很接近,但不是我想要的。对于 IUser,所有字段都是必需的。只是在某些情况下,我希望它是部分匹配。 要求是矛盾的,你不能让一个字段同时是可选的和必填的。 :P 我想你可以使用 2 个接口来获得它.. 很多时候你会想要这个并且其他语言(比如 Flow)允许它。出于 DRYness 的考虑,您希望在某些情况下允许一个子集。考虑 Backbone 模型更新,其中您已命名参数,这些参数必须是模型属性的子集。 我同意@toskv。如果在某些情况下这些字段可能不存在,则这些字段不是强制性的。您可以为这些不同的场景定义不同的接口。无论如何,我不确定为什么有人对他的回答投了反对票,这是正确的。【参考方案2】:你可以把它分成不同的界面:
interface IUser
id: number;
;
interface IUserEmail extends IUser
email: string;
interface IUserPhone extends IUser
phone: string;
让您的方法接收基本的IUser
接口,然后检查您需要的字段:
function doit(user: IUser)
if (user.email)
else if (user.phone)
【讨论】:
【参考方案3】:如果我正确理解了这个问题,你想要 Flow 的 $Shape
之类的东西
所以,在一个地方,你可能有一些需要类型的东西
interface IUser
email: string;
id: number;
phone: string;
;
然后,在另一个地方,您想要一个与 IUser 具有相同类型的类型,只是所有字段现在都是可选的。
interface IUserOptional
email?: string;
id?: number;
phone?: string;
;
您想要一种基于IUser
自动生成IUserOptional
的方法,而无需再次写出类型。
现在,我认为这在 Typescript 中是不可能的。在 2.0 中情况可能会发生变化,但我认为我们在 Typescript 中甚至还没有接近这样的东西。
你可以查看一个预编译器,它会在 typescript 运行之前为你生成这样的代码,但这听起来并不容易。
考虑到这个问题,我只能建议您改用 Flow。在流程中,您只需执行$Shape<IUser>
即可以编程方式生成您想要的类型。当然,Flow 在许多大大小小的方面都与 Typescript 不同,因此请记住这一点。 Flow 不是编译器,因此您不会得到 Enums 和实现接口的类之类的东西
【讨论】:
这违背了创建接口的目的——它们应该是可重用和可组合的。这个假设只是为了必填字段而维护同一实体的两个版本。【参考方案4】:映射类型的正确解决方案:
updateUser<K extends keyof IUser>(userData: [P in K]: IUser[P])
...
【讨论】:
Partial
更容易。但是您的回答消除了我对这种情况下的语法的一些疑问。谢谢!【参考方案5】:
Typescript 现在支持部分类型。
创建分部类型的正确方法是:
type PartialUser = Partial<IUser>;
【讨论】:
有没有办法指定一个特定的类的部分子集?即 id 和 email 是强制性的但电话不存在的课程? @James 请在下面查看我的解决方案。这就是你要的。 @JamesPick
类型是您所需要的。如果您根本不想允许电话,请使用Pick<IUser, 'id' | 'email'>
,如果您想允许电话但不需要它(同时需要 id 和电子邮件),请使用 Pick<IUser, 'id' | 'email'> & Partial<IUser>
。详情见我的回答。【参考方案6】:
你想要的是这个
type Subset<T extends U, U> = U;
这确保了 U 是 T 的一个子集并返回 U 作为一个新类型。例如:
interface Foo
name: string;
age: number;
type Bar = Subset<Foo,
name: string;
>;
您不能向 Bar 添加不属于 Foo 的新属性 - 并且您不能以不兼容的方式更改类型。这也适用于嵌套对象。
【讨论】:
哟,这很酷,我不知道你可以像这样在类型分配中进行前向引用,但我想这是有道理的。光滑的东西。【参考方案7】:值得注意的是,Partial<T>
,正如接受的答案中所建议的那样,将 all 字段设为可选,这不一定是您需要的。
如果您想让某些字段成为必填项(例如id
和email
),则需要将其与Pick
结合起来:
type UserWithOptionalPhone = Pick<IUser, 'id' | 'email'> & Partial<IUser>
一些解释:
Pick
的作用是让您简洁地指定接口的子集(无需创建一个重复字段类型的全新接口,正如其他答案所建议的那样),然后让您使用这些接口,然后 只有那些字段。
function hello1(user: Pick<IUser, 'id' | 'email'>)
hello1(email: '@', id: 1); //OK
hello1(email: '@'); //Not OK, id missing
hello1(email: '@', id: 1, phone: '123'); //Not OK, phone not allowed
现在,这并不是我们所需要的,因为我们想要允许,而不是要求电话。为此,我们通过创建intersection type 来“合并”我们类型的部分版本和“挑选”版本,然后将id
和email
作为必填字段,而其他所有内容都是可选的——这正是我们的方式想要它。
function hello2(user: Pick<IUser, 'id' | 'email'> & Partial<IUser>)
hello2(email: '@', id: 1); //OK
hello2(email: '@', id: 1, phone: '123'); //OK
hello2(email: '@'); //Not OK, id missing
【讨论】:
以上是关于Typescript 是不是支持“子集类型”?的主要内容,如果未能解决你的问题,请参考以下文章