打字稿,合并对象类型?

时间:2018-04-05 22:37:49

标签: typescript object generics types merge

是否可以合并两种通用对象类型的道具? 我有一个类似的功能:

function foo<A extends object, B extends object>(a: A, b: B) {
    return Object.assign({}, a, b);
}

我希望该类型是A中B中不存在的所有属性,以及B中的所有属性。

merge({a: 42}, {b: "foo", a: "bar"});

给出了一个相当奇怪的{a: number} & {b: string, a: string}类型,a是一个字符串。 实际的返回给出了正确的类型,但我无法想象我将如何明确地编写它。

4 个答案:

答案 0 :(得分:13)

TypeScript standard library definition of Object.assign()生成的交集类型是approximation,如果后面的参数具有与先前参数同名的属性,则不能正确表示会发生什么。但直到最近,这才是TypeScript类型系统中最好的。

然而,从TypeScript 2.8中引入conditional types开始,您可以使用更接近的近似值。一个这样的改进是使用类型函数Spread<L,R>定义here,如下所示:

// Names of properties in T with types that include undefined
type OptionalPropertyNames<T> =
  { [K in keyof T]: undefined extends T[K] ? K : never }[keyof T];

// Common properties from L and R with undefined in R[K] replaced by type in L[K]
type SpreadProperties<L, R, K extends keyof L & keyof R> =
  { [P in K]: L[P] | Exclude<R[P], undefined> };

type Id<T> = {[K in keyof T]: T[K]} // see note at bottom*

// Type of { ...L, ...R }
type Spread<L, R> = Id<
  // Properties in L that don't exist in R
  & Pick<L, Exclude<keyof L, keyof R>>
  // Properties in R with types that exclude undefined
  & Pick<R, Exclude<keyof R, OptionalPropertyNames<R>>>
  // Properties in R, with types that include undefined, that don't exist in L
  & Pick<R, Exclude<OptionalPropertyNames<R>, keyof L>>
  // Properties in R, with types that include undefined, that exist in L
  & SpreadProperties<L, R, OptionalPropertyNames<R> & keyof L>
  >;

(我稍微更改了链接定义;使用标准库中的Exclude而不是Diff,并使用no-op Spread包装Id类型键入以使检查类型比一堆交叉点更容易处理。)

让我们试一试:

function merge<A extends object, B extends object>(a: A, b: B) {
  return Object.assign({}, a, b) as Spread<A, B>;
}

const merged = merge({ a: 42 }, { b: "foo", a: "bar" });
// {a: string; b: string;} as desired

您可以看到输出中的a现在已正确识别为string而不是string & number。耶!

但请注意,这仍然是一个近似值:

  • Object.assign()仅复制enumerable, own properties,类型系统不会为您提供任何方式来表示要过滤的属性的可枚举性和所有权。这意味着merge({},new Date())看起来像TypeScript类型Date,即使在运行时,也不会复制Date方法,输出基本上是{}。这是现在的硬限制。

  • 此外,Spread的定义在缺少属性和以未定义值存在的属性distinguish 。因此merge({ a: 42}, {a: undefined}){a: number}错误地输入为{a: undefined}。这可以通过重新定义Spread来解决,但我不是100%肯定。对大多数用户来说可能没有必要。 (编辑:可以通过重新定义type OptionalPropertyNames<T> = { [K in keyof T]-?: ({} extends { [P in K]: T[K] } ? K : never) }[keyof T]

  • 来解决此问题
  • 类型系统无法对其不了解的属性执行任何操作。 declare const whoKnows: {}; const notGreat = merge({a: 42}, whoKnows);在编译时的输出类型为{a: number},但如果whoKnows恰好是{a: "bar"}(可分配给{}),则notGreat.a 1}}是运行时的字符串,但是在编译时是一个数字。哎呀。

所以要警告;将Object.assign()键入为交集或Spread<>是一种“尽力而为”的事情,并且可能会使您在边缘情况下误入歧途。

无论如何,希望有所帮助。祝你好运!

*注意:有人将身份映射类型的Id<T>定义编辑为T。这样的改变确切地说并不是错误的,但它违背了目的......即迭代键以消除交叉点。比较:

type Id<T> = { [K in keyof T]: T[K] }

type Foo = { a: string } & { b: number };
type IdFoo = Id<Foo>; // {a: string, b: number }

如果您检查IdFoo,您将看到交叉点已被消除,并且两个成分已合并为单一类型。同样,FooIdFoo在可转让性方面没有真正的区别;只是后者在某些情况下更容易阅读。不可否认,有时编译器的类型的字符串表示只是opaque-ish Id<Foo>,所以它并不完美。但它确实有目的。如果您想在自己的代码中将Id<T>替换为T,请成为我的访客。

答案 1 :(得分:2)

我找到了一种语法来声明一种合并任意两个对象的所有属性的类型。

type Merge<A, B> = { [K in keyof (A | B)]: K extends keyof B ? B[K] : A[K] };

答案 2 :(得分:1)

我认为你正在寻找更多的 union |)类型而不是交集(&)类型。它更接近你想要的......

function merge<A, B>(a: A, b: B): A | B {
  return Object.assign({}, a, b)
}

merge({ a: "string" }, { a: 1 }).a // string | number
merge({ a: "string" }, { a: "1" }).a // string

学习TS我花了很多时间回到this page ...这是一个很好的阅读(如果你是那种东西)并提供了很多有用的信息

答案 3 :(得分:1)

如果要保留属性顺序,请使用以下解决方案。

查看实际情况here

@EnableWs
@Configuration
public class WebServiceConfig extends WsConfigurerAdapter {

    @Bean
    public ServletRegistrationBean messageDispatcherServlet(ApplicationContext applicationContext) {
        MessageDispatcherServlet servlet = new MessageDispatcherServlet();
        servlet.setApplicationContext(applicationContext);
        servlet.setTransformWsdlLocations(true);
        return new ServletRegistrationBean(servlet, "src/main/resources/wsdl/*");
    }

    @Bean(name = "evaluateIMSRule")
    public DefaultWsdl11Definition getRuleEngineSchema(XsdSchema ruleEngineSchema) {
        DefaultWsdl11Definition wsdl11Definition = new DefaultWsdl11Definition();
        wsdl11Definition.setPortTypeName("port");
        wsdl11Definition.setLocationUri("src/main/resources/wsdl/evaluateIMSRule/");
        wsdl11Definition.setTargetNamespace("services/mobility");
        wsdl11Definition.setSchema(ruleEngineSchema);
        return wsdl11Definition;
    }

    @Bean(name = "itemNOSService")
    public DefaultWsdl11Definition getItemNOSSchema(XsdSchema itemNOSSchema) {
        DefaultWsdl11Definition wsdl11Definition = new DefaultWsdl11Definition();
        wsdl11Definition.setPortTypeName("port");
        wsdl11Definition.setLocationUri("src/main/resources/wsdl/itemNotOnShelf/");
        wsdl11Definition.setTargetNamespace("services/mobility");
        wsdl11Definition.setSchema(itemNOSSchema);
        return wsdl11Definition;
    }

    @Bean
    public XsdSchema ruleEngineSchema() {
        return new SimpleXsdSchema(new ClassPathResource("src/main/resources/wsdl/evaluateIMSRule/evaluateIMSRule.xsd"));
    }

    @Bean
    public XsdSchema itemNOSSchema() {
        return new SimpleXsdSchema(new ClassPathResource("src/main/resources/wsdl/itemNotOnShelf/itemNotOnShelf.xsd"));
    }
}