GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Generic object rest variables and parameters by ahejlsberg · Pull Request #28312 · microsoft/TypeScript

Conversation

@ahejlsberg

@ahejlsberg Anders Hejlsberg (ahejlsberg) commented Nov 2, 2018

Copy link
Copy Markdown
Member

With this PR we permit rest properties in destructurings of objects of generic types. This effectively implements what is suggested in #10727 (although by different means) and complements our support for generic spread expressions in object literals implemented in #28234.

When a destructuring of an object of a generic type includes a rest variable, the type of the rest variable is an instantiation of the Pick and Exclude predefined types:

function excludeTag<T extends { tag: string }>(obj: T) {
    let { tag, ...rest } = obj;
    return rest;  // Pick<T, Exclude<keyof T, "tag">>
}

const taggedPoint = { x: 10, y: 20, tag: "point" };
const point = excludeTag(taggedPoint);  // { x: number, y: number }

Some additional examples:

const sa = Symbol();
const sb = Symbol();

function f1<T extends { [sa]: string, [sb]: number }>(obj: T) {
    let { [sa]: a, [sb]: b, ...rest } = obj;
    return rest;  // Pick<T, Exclude<keyof T, typeof sa | typeof sb>>
}

function f2<T, KA extends keyof T, KB extends keyof T>(obj: T, ka: KA, kb: KB) {
    let { [ka]: a, [kb]: b, ...rest } = obj;
    return rest;  // Pick<T, Exclude<keyof T, KA | KB>>
}

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

j-oliveras, ulrichb, hatamov, fruchtose, vkrol, olaf89, aboyton, lucasbasquerotto, wanganjun, cartant, and 24 more reacted with thumbs up emoji j-oliveras, ulrichb, hatamov, fruchtose, asyncliz, vkrol, Hotell, cartant, trotyl, cevek, and 9 more reacted with hooray emoji trotyl, cevek, j-oliveras, ksaldana1, iamolegga, and roxennnn reacted with heart emoji
All reactions
  • 👍 34 reactions
  • 🎉 19 reactions
  • ❤️ 6 reactions
Anders Hejlsberg (ahejlsberg) added 13 commits November 1, 2018 12:33
@ahejlsberg
Add Rest<T, K> type to lib.d.ts
f555261
@ahejlsberg
Use Rest<T, K> for rest properties in object destructuring
6f8959b
@ahejlsberg
Accept new baselines
18f80b8
@ahejlsberg
Merge branch 'master' into genericRest
b0a5337
# Conflicts:
#	tests/baselines/reference/objectRest.errors.txt
#	tests/baselines/reference/objectRest.types
@ahejlsberg
Define Rest<T, K> as Pick<T, Exclude<keyof T, K>> to ensure it is hom…
0b194a2
…omorphic
@ahejlsberg
Only create Rest<T, K> types when object and/or key type is generic
396642d
@ahejlsberg
Accept new baselines
f558059
@ahejlsberg
Remove Rest<T, K> and use Pick<T, Exclude<keyof T, K>> instead
9fd7d0a
@ahejlsberg
Accept new baselines
9daf69d
@ahejlsberg
Ensure '{ [key]: x } = obj' for generic key is the same as 'x = obj[k…
abebf15
…ey]'
@ahejlsberg
Accept new baselines
bcc1d2a
@ahejlsberg
Add tests
48c0aed
@ahejlsberg
Accept new baselines
00c5071
@ahejlsberg Anders Hejlsberg (ahejlsberg) mentioned this pull request Nov 2, 2018
Add spread/rest higher-order types operator #10727
Open
@ahejlsberg
Anders Hejlsberg (ahejlsberg) requested review from Daniel Rosenwasser (DanielRosenwasser), Nathan Shively-Sanders (sandersn) and Wesley Wigham (weswigham) November 2, 2018 15:56
@DanielRosenwasser Daniel Rosenwasser (DanielRosenwasser) mentioned this pull request Nov 3, 2018
Design Meeting Notes, 11/2/2018 #28326
Closed
@benjaminjackman

Ben Jackman (benjaminjackman) commented Nov 4, 2018

Copy link
Copy Markdown

Would it be worth considering adding type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>> as a predefined type and using that here?

ulrichb and ckknight reacted with thumbs up emoji
All reactions
  • 👍 2 reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ahejlsberg

Anders Hejlsberg (ahejlsberg) commented Nov 4, 2018

Copy link
Copy Markdown
Member Author

Would it be worth considering adding type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>> as a predefined type and using that here?

We considered that, but there are too many projects out there that already define their own Omit and we'd end up with duplicate definition issues.

simeyla reacted with confused emoji
All reactions
  • 😕 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

weswigham
Wesley Wigham (weswigham) approved these changes Nov 5, 2018
View reviewed changes

@weswigham Wesley Wigham (weswigham) left a comment

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a noLib test would be nice, but it looks good.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
sandersn
Nathan Shively-Sanders (sandersn) reviewed Nov 5, 2018
View reviewed changes
Comment thread tests/baselines/reference/objectRestNegative.errors.txt
@@ -34,8 +33,6 @@ tests/cases/conformance/types/rest/objectRestNegative.ts(17,9): error TS2701: Th
~

@sandersn Nathan Shively-Sanders (sandersn) Nov 5, 2018

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SHould probably remove this function generic<T ... example since the new test covers exactly the same case.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@ahejlsberg Anders Hejlsberg (ahejlsberg) Nov 5, 2018

Copy link
Copy Markdown
Member Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but no harm in keeping it.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
sandersn
Nathan Shively-Sanders (sandersn) reviewed Nov 5, 2018
View reviewed changes
Comment thread tests/baselines/reference/objectRest.errors.txt Resolved

Uh oh!

There was an error while loading. Please reload this page.

sandersn
Nathan Shively-Sanders (sandersn) reviewed Nov 5, 2018
View reviewed changes
Comment thread tests/baselines/reference/computedPropertyNamesContextualType8_ES5.errors.txt

var o: I = {
~
!!! error TS2322: Type '{ [x: string]: string | number; }' is not assignable to type 'I'.

@sandersn Nathan Shively-Sanders (sandersn) Nov 5, 2018

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any idea what's going on here? I'm not sure why we stopped issuing the per-property error.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@weswigham Wesley Wigham (weswigham) Nov 5, 2018

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assumed it was the change in looking up the type of property names.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@ahejlsberg Anders Hejlsberg (ahejlsberg) Nov 5, 2018

Copy link
Copy Markdown
Member Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's because we previously treated all computed properties as having known (unit type) names, when here they actually have type string. Since they now have type string we generate a string index signature, and the elaboration logic stops trying to drill down to the properties.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
sandersn
Nathan Shively-Sanders (sandersn) approved these changes Nov 5, 2018
View reviewed changes
@ahejlsberg
Merge branch 'master' into genericRest
f9dd445
# Conflicts:
#	src/compiler/checker.ts
@ahejlsberg
Anders Hejlsberg (ahejlsberg) merged commit 85dbc04 into master Nov 6, 2018
@ahejlsberg
Anders Hejlsberg (ahejlsberg) deleted the genericRest branch November 6, 2018 00:36
@vkrol

Veniamin Krol (vkrol) commented Nov 19, 2018

Copy link
Copy Markdown

Is this scenario is not supported? Or this is a bug?

function foo<T>(obj: T & { x: string }): T {
    const { x, ...rest } = obj;
    return rest; // Error: [ts] Type 'Pick<T & { x: string; }, Exclude<keyof T, "x">>' is not assignable to type 'T'. [2322]
}
Hotell reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ajafff

Klaus Meinhardt (ajafff) commented Nov 19, 2018

Copy link
Copy Markdown
Contributor

Veniamin Krol (@vkrol) if T contains a property x it will not be present after the destructuring. Therefore the error is correct.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@vkrol

Veniamin Krol (vkrol) commented Nov 19, 2018 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

Klaus Meinhardt (@ajafff) But T does not contains x, T & { x: string } contains x 🤔.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@sandersn

Nathan Shively-Sanders (sandersn) commented Nov 19, 2018

Copy link
Copy Markdown
Member

Veniamin Krol (@vkrol) Let T={ x: number }. Now T contains x.

Klaus Meinhardt (@ajafff) While this is correct, I think it’s actually undesired. Most of the motivation for the higher-order definition of spread as intersection was that it’s quite usable, as well as correct [1] for the common case where two disjoint types are spread together (the case here) or the same type is spread into itself.

The definition of rest we ended up with is actually more correct, as you note, but it means that it doesn’t have the same assignability behaviour as spread. I think the correct fix here would be to add a simplification rule in assignability checking that says that Pick<T & U, Exclude<keyof T, keyof U>> is assignable to T.

However, I’m not sure if such a rule would be applicable anywhere else, and I think it would be fiddly to get right since U and keyof U are not tightly linked, so you’d have to relate them via assignability instead of just checking type equality.

[1] except of course for own-ness, which the compiler doesn’t track well anyway.

Hotell and SlurpTheo reacted with thumbs up emoji
All reactions
  • 👍 2 reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@Hotell

Martin Hochel (Hotell) commented Nov 19, 2018

Copy link
Copy Markdown

What about following scenario Nathan Shively-Sanders (@sandersn) ( related to react HoC, but the same behaviour happens with raw functions )

image

Error:
image

Fix:

It needs to be casted to original P to get rid of errors, although InjectedProps are subtracted from P constraint within the implementation and the exact shape is properly passed via const injectedProps

image

So is this correct behaviour or a bug ?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@sandersn

Nathan Shively-Sanders (sandersn) commented Nov 19, 2018

Copy link
Copy Markdown
Member

Martin Hochel (@Hotell)

As far as I understand the problem, I think it's the same as Veniamin Krol (@vkrol)'s. I'm not sure I understand it, however.

If InjectedProps is the type that's related to withCounter, why does counterWannabe have to declare it as a type at all? Shouldn't extendedFunc be the one with the dependency on the counter-related types InjectedProps and ExtendedProps?

In other words, why does P extend InjectedProps? Shouldn't it be unconstrained and then innerFunc be (props: P & ExtendedProps) => P & InjectedProps ?

Of course, given the const { maxCount, ...passThroughProps } = props and the problem Veniamin Krol (@vkrol) lays out above, you'll get Pick<P & ExtendedProps, Exclude<keyof P & ExtendedProps, keyof ExtendedProps>> instead of P for the type of passThroughProps.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@Hotell

Martin Hochel (Hotell) commented Nov 20, 2018 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

Nathan Shively-Sanders (@sandersn) I wanted to simplify the example by not involving react but I guess that was bad move :D sorry about that. So here is the React HoC example, with comments:

update 11/22/18:

  • added InjectedProps definition
  • passThroughProps are casted to P -> {...passThroughProps as P}
import React, { Component } from 'react'

import { Counter } from './counter-render-prop'
import { Subtract } from '../types'

type ExtractFuncArguments<T> = T extends (...args: infer A) => any ? A : never;

// get props that Counter injects via children as a function
// InjectedProps is gonna be:
// { count: number; } & { inc: () => void; dec: () => void; }
type InjectedProps = ExtractFuncArguments<Counter['props']['children']>[0];

// withCounter will enhance returned component by ExtendedProps
type ExtendedProps = { maxCount?: number };

// P is constrained to InjectedProps as we wanna make sure that wrapped component
// implements this props API
const withCounter = <P extends InjectedProps>(Cmp: React.ComponentType<P>) => {
  class WithCounter extends Component<
    // enhanced component will not include InjectedProps anymore as they are injected within render of this HoC and API surface is gonna be extended by ExtendedProps
    Subtract<P, InjectedProps> & ExtendedProps
  > {
    static displayName = `WithCounter(${Cmp.name})`;

    render() {
      const { maxCount, ...passThroughProps } = this.props;

      return (
       // we use Counter which has children as a function API for injecting props
        <Counter>
          {(injectedProps) =>
            maxCount && injectedProps.count >= maxCount ? (
              <p className="alert alert-danger">
                You've reached maximum count! GO HOME {maxCount}
              </p>
            ) : (
              // here cast to as P is needed otherwise compile error will occur
              <Cmp {...injectedProps} {...passThroughProps as P} />
            )
          }
        </Counter>
      );
    }
  }

  return WithCounter;
};

// CounterWannabe implement InjectedProps on it's props
class CounterWannabe extends Component<
  InjectedProps & { colorType?: 'primary' | 'secondary' | 'success' }
> {
  render() {
    const { count, inc, colorType } = this.props;

    const cssClass = `alert alert-${colorType}`;

    return (
      <div style={{ cursor: 'pointer' }} className={cssClass} onClick={inc}>
        {count}
      </div>
    );
  }
}

// if CounterWannabe would not implement InjectedProps this line would get compile error
const ExtendedComponent = withCounter(CounterWannabe);
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@sandersn

Nathan Shively-Sanders (sandersn) commented Nov 21, 2018

Copy link
Copy Markdown
Member

Martin Hochel (@Hotell) What's the source of Counter? I'm not that familiar with React, so I can't guess how the children of Counter are supposed to be typed.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@sandersn Nathan Shively-Sanders (sandersn) mentioned this pull request Nov 21, 2018
Generic rest of intersection should be assignable to its type parameter constituents #28636
Open
5 tasks done
@sandersn

Nathan Shively-Sanders (sandersn) commented Nov 21, 2018

Copy link
Copy Markdown
Member

I read the intro to HOCs and I think I understand what's going on, although I haven't had a chance to play with the whole example without Counter.

  1. I still think this is the same issue as posted Veniamin Krol (@vkrol) encountered. That is, Rest<'a', T & { a }> should be assignable to T. I'm not sure why casting injectedProps is needed instead of passThroughProps though.
  2. Can't you spread <Cmp {...injectedProps} {...this.props} /> instead? It looks to me the only reason that normal React HOCs use passThroughProps is so that the passthrough component doesn't access the HOC's private state by mistake. But this is not likely in Typescript, because the type checker will complain if CounterWannabe accesses maxCount.
  3. Let's file a new issue for this: Generic rest of intersection should be assignable to its type parameter constituents #28636. I think it's a real hole in type simplifications of today.
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@Hotell

Martin Hochel (Hotell) commented Nov 22, 2018 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

What's the source of Counter?

Sorry about that, it's not really important I should explicitly provide props in that example.

👉 I've updated previous comment

type State = typeof initialState
type Props = {
  children: (
    props: State & { inc: Counter['handleInc']; dec: Counter['handleDec'] }
  ) => React.ReactChild
  count?: number
} & typeof defaultProps

const initialState = { count: 0 }
const defaultProps = {
  onChange: (value: number) => {}
}
export class Counter extends Component<Props, State> {
  static defaultProps = defaultProps
  render(){ /*...*/ }
}

I'm not sure why casting injectedProps is needed instead of passThroughProps though.

My bad, updated the code above

Can't you spread <Cmp {...injectedProps} {...this.props} /> instead?

will produce the same error

image

let's file a new issue for this: #28636.
I agree, can you update the code in that issue pls with my fixes ?

Thanks a lot Nathan Shively-Sanders (@sandersn) 💪

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@saulshanabrook Saul Shanabrook (saulshanabrook) mentioned this pull request Sep 24, 2019
Destructure and spread on generics not typing #33579
Closed
@microsoft Microsoft (microsoft) locked as resolved and limited conversation to collaborators Oct 21, 2025
@typescript-bot TypeScript Bot (typescript-bot) assigned Anders Hejlsberg (ahejlsberg) Oct 21, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Reviewers

@weswigham Wesley Wigham (weswigham) weswigham approved these changes
@sandersn Nathan Shively-Sanders (sandersn) sandersn approved these changes
@DanielRosenwasser Daniel Rosenwasser (DanielRosenwasser) Awaiting requested review from DanielRosenwasser

Uh oh!

There was an error while loading. Please reload this page.

Assignees

@ahejlsberg Anders Hejlsberg (ahejlsberg)

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

7 participants

@ahejlsberg @benjaminjackman @vkrol @ajafff @sandersn @Hotell @weswigham
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.