Typescript: Spread operator for object types and interfaces

Created on 3 Aug 2019  路  5Comments  路  Source: microsoft/TypeScript

Search Terms

  • merge, override, spread, rest, operator, object, interface

Suggestion

Provide an operator to be used in both {} types and interface types which inherits a set of properties from another object type whilst also allowing for those properties to be selectively overridden.

type SizeProps = { width: number, height: number }
type MyProps = {
  ...SizeProps, 
  width: number | string,
  foo: boolean,
}

// Also works with interfaces
interface IProps {
  ...SizeProps;
  width: number | string;
  foo: boolean;
}

In the example above, the MyProps type would resolve to the following:

{ width: number | string, height: number, foo: boolean }

This behavior can already be achieve with the following mapped type, but it's not as elegant (especially once formatted with Prettier):

type Merge<A, B> = {
  [K in keyof A]: K extends keyof B ? B[K] : A[K]
} & B

type SizeProps = { width: number, height: number }
type MyProps = Merge<
  SizeProps,
  {
    width: number | string,
    foo: boolean,
  }
>

Also, the Merge type has its limitations:

  • it can not be used when declaring an interface
  • it does not result in a flattened object type

Checklist

My suggestion meets these guidelines:

  • [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • [x] This wouldn't change the runtime behavior of existing JavaScript code
  • [x] This could be implemented without emitting different JS based on the types of the expressions
  • [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • [x] This feature would agree with the rest of TypeScript's Design Goals.
Duplicate

Most helpful comment

As far as I know #10727 would support both A ... {x: B} and {...A, x: B} syntaxes.

If you want to make the type "flat" you can use a mapped type (and conditional type inference) to do this:

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

type SizeProps = { width: number; height: number };
type OtherProps = { width: number | string; foo: boolean };

type MyPropsType = Merge<SizeProps, OtherProps>;
/*
type MyPropsType = {
    height: number;
    width: string | number;
    foo: boolean;
}
*/

Note that Merge doesn't necessarily do the right things with optional properties, since presumably Merge<{a: string}, {a?: number}> should become something like {a: string | number | undefined}. The proposal in #10727 goes into detail about what should happen in such cases.


All I'm seeing here that's not a duplicate of #10727 is the ability to use a spread operator to define an interface. But I wouldn't expect interfaces to ever support type operators directly (someone can correct me if I'm wrong). Instead I'd expect to continue to use extends and/or declaration merging to compose interface types.

In the above case, since you have an object type (or an intersection of object types) whose members are statically known (that is, not dependent on some unresolved generic), you can promote it to an interface via extends:

interface MyProps extends Merge<SizeProps, OtherProps> {} // okay
const myProps: MyProps = {
  height: 1,
  width: "one",
  foo: true
};

I'm just an ~obnoxious~ interested bystander, though, and don't speak for the actual TS maintainers... I expect someone will come along to give a more authoritative assessment here. Good luck!

Playground link

All 5 comments

@jcalz I've seen that proposal. Thanks for linking it. I believe it to be complementary to this one.

Using only the "spread type", my example above would look like this:

type SizeProps = { width: number, height: number }
type MyProps = SizeProps ... {
  width: number | string,
  foo: boolean,
}

Note: Interfaces are not supported with the "spread type" in #10727.

My proposal (1) provides a more elegant (IMO) syntax for "type merging" and (2) provides a way to merge an object type into an interface whilst allowing property overrides.

As far as I know #10727 would support both A ... {x: B} and {...A, x: B} syntaxes.

If you want to make the type "flat" you can use a mapped type (and conditional type inference) to do this:

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

type SizeProps = { width: number; height: number };
type OtherProps = { width: number | string; foo: boolean };

type MyPropsType = Merge<SizeProps, OtherProps>;
/*
type MyPropsType = {
    height: number;
    width: string | number;
    foo: boolean;
}
*/

Note that Merge doesn't necessarily do the right things with optional properties, since presumably Merge<{a: string}, {a?: number}> should become something like {a: string | number | undefined}. The proposal in #10727 goes into detail about what should happen in such cases.


All I'm seeing here that's not a duplicate of #10727 is the ability to use a spread operator to define an interface. But I wouldn't expect interfaces to ever support type operators directly (someone can correct me if I'm wrong). Instead I'd expect to continue to use extends and/or declaration merging to compose interface types.

In the above case, since you have an object type (or an intersection of object types) whose members are statically known (that is, not dependent on some unresolved generic), you can promote it to an interface via extends:

interface MyProps extends Merge<SizeProps, OtherProps> {} // okay
const myProps: MyProps = {
  height: 1,
  width: "one",
  foo: true
};

I'm just an ~obnoxious~ interested bystander, though, and don't speak for the actual TS maintainers... I expect someone will come along to give a more authoritative assessment here. Good luck!

Playground link

Let's track this at #10727 (since they are motivated by the same use cases and have the same solutions) and have the syntax discussion happen over there. Thanks!

This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

Was this page helpful?
0 / 5 - 0 ratings