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 请在下面查看我的解决方案。这就是你要的。 @James Pick 类型是您所需要的。如果您根本不想允许电话,请使用Pick&lt;IUser, 'id' | 'email'&gt;,如果您想允许电话但不需要它(同时需要 id 和电子邮件),请使用 Pick&lt;IUser, 'id' | 'email'&gt; &amp; Partial&lt;IUser&gt;。详情见我的回答。【参考方案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&lt;T&gt;,正如接受的答案中所建议的那样,将 all 字段设为可选,这不一定是您需要的。

如果您想让某些字段成为必填项(例如idemail),则需要将其与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 来“合并”我们类型的部分版本和“挑选”版本,然后将idemail 作为必填字段,而其他所有内容都是可选的——这正是我们的方式想要它。

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 是不是支持“子集类型”?的主要内容,如果未能解决你的问题,请参考以下文章

Angular 2.0:Typescript 是不是支持 ES6 的所有特性?

React+TypeScript

最新 Vue 3.0 源码开放了

TypeScript教程学习笔记16篇(完结)

Typescript本地存储兼容性[重复]

TypeScript 中的 Angular 材质示例:芯片