0

Is it possible to cast the ...rest in an object destructuring assignment to be the same type as the original object?

The use case: a method that processes a Partial<Record> of 10-ish properties and returns a new record of the same type as the input record, but with some properties removed, some properties modified, and most others unchanged.

A vastly simplified version of this function is below to illustrate the problem. TS Playground link.

type ABCD = Partial<{ a: number, b: number, c: number, d: number }>;

/** If `a` is positive, double it and remove `b`. If negative, just remove `a`. */
function f(record: ABCD) {
  const {a, b, ...result} = record;
  if (a !== undefined && a > 0) {
    result.a = a * 2;
    // Property 'a' does not exist on type '{ c?: number | undefined; d?: number | undefined; }'
  } else {
    result.b = b;
    // Property 'b' does not exist on type '{ c?: number | undefined; d?: number | undefined; }'
  }
  return result;
}

I could solve the problem by removing type safety, as suggested in this answer:

  const {a, b, ...result}: {a?: number, b?: number, [key: string]: number | undefined } = record;

But I'd prefer to retain type safety.

I could also fix this type issue by changing runtime code: use delete instead of destructuring, assign result to a new variable with the correct type, etc.

But this function is part of a TS port of a JS library that uses this "destructure and add properties back" pattern, and it's really convenient to retain wherever possible the same runtime code as the original library so that when the underlying JS library changes, the it's easier to apply those diffs to our TS port.

So I'm wondering if there's a way to solve this problem with only TS type casts or other TS syntax that's transpiled away at runtime, but without losing type safety on the result. Is there a way?

Justin Grant
  • 44,807
  • 15
  • 124
  • 208
  • 1
    This question is constrained to the point where it's hard for me to imagine an answer that isn't "no, there is no way". Do I understand properly that you want compiler-verified type safety without altering the runtime code in any way at all? If so I'm stumped. Without something like [ms/TS#45516](https://github.com/microsoft/TypeScript/issues/45516) I don't think you can annotate the type of `result`, and anything else you do will either require minor changes to the runtime code or unsafe type assertions (like [these](https://tsplay.dev/WvanAm)). What can we do here? – jcalz Jan 15 '23 at 03:17
  • is this acceptable: type Unmodified = Partial<{c: number, d: number }>; type ABCD = Partial<{a: number, b: number, c: number, d: number}>; declare function cr1(x: ABCD): x is ({a: number} & Unmodified); function c2(x: ABCD): x is ({b: number} & Unmodified) { return !c1(x); } function f(record: ABCD): ({ a: number } | { b: number }) & Unmodified { const {a,b,...rest} = record; if (c1(record)) { record.a = record.a * 2; return {a: record.a, ...rest}; } if (c2(record)) { record.b = record.b; return {b: record.b, ...rest}; } throw new Error("unreachable"); } – user-id-14900042 Jan 15 '23 at 03:26
  • @jcalz - Yep, "compiler-verified type safety without altering the runtime code in any way at all" is what I'm looking for. You're confirming what I suspected, that what I'm trying to do is not possible with a single type cast. Instead, I suspect the only way to solve the problem without changing runtime code nor giving up type safety would be to cast the object whenever we add a property to it, rather than at its declaration. Also, thanks for pointing me to https://github.com/microsoft/TypeScript/issues/45516. If future SO readers also want that change, you may want to vote for it! – Justin Grant Jan 15 '23 at 23:13

0 Answers0