Skip to content

More accurate typing of Object.assign and React component setState() #6613

Description

This is a proposal for solving the second use case mentioned in Issue #6218

Problem

/**
 * This class is a given by the React framework
 */
class ReactComponent<S> {
    protected _state: S;

    /**
     * This method actually accepts any supertype of S
     */
    protected setState(newState: S): void {
        for (let name in newState) {
            if (newState.hasOwnProperty(name)) {
                this._state[name] = newState[name];
            }
        }
    }

    protected componentWillMount(): void {
        // abstract
    }
}

/**
 * Some state interface declaration. Note all members are optional to allow setState to
 * be called with supertypes of BaseState
 */
interface BaseState {
    a?: string;
}

/**
 * My own base class for certain React widgets
 */
class BaseWidget<S extends BaseState> extends ReactComponent<S> {

    constructor() { 
        super();
        this._state = {};
    }

    protected componentWillMount(): void {
        this.setState({ a: "boo" });
    }
} 
$ tsc v1.ts
v1.ts(39,9): error TS2322: Type '{}' is not assignable to type 'S'.
v1.ts(43,23): error TS2345: Argument of type '{ a: string; }' is not assignable to parameter of type 'S'.

The compiler cannot know that the setState() method will accept any super-type of S, i.e. an object with any subset of the members of S.

Proposal

Add a keyword to type setState() properly. To avoid confusion, I chose partof instead of e.g. 'supertype of' (see also initial confusion in #6218). For me, 'partof' conveys that I can give the method any object with a subset of the members of S.

/**
 * This class is a given by the React framework
 */
class ReactComponent<S extends {}> {
    protected _state: S;

    /**
     * This method accepts any supertype of S
     */
    protected setState(newState: partof S): void {
    }
}

Properties of partof

  • Only works for object types (hence the 'extends {}' above) because I wouldn't know how to pass part of e.g. a string
  • Allows any supertype of the given type
  • Incorporates knowledge of the generic parameter. Given the class declaration class ReactComponent<S extends { foo: number; }>, one is able to call setState({ foo: 3}) and setState({}) but not setState({ bar: 3 }) inside the class definition.

Comments more than welcome, I'm not a compiler expert. This is just to get the discussion going. If you think there already exists a way of typing this, please check with the original issue for a couple of failed examples.

Activity

  1. rogierschouten commented on Jan 25, 2016

    @rogierschouten
    Author
  2. RyanCavanaugh commented on Jan 25, 2016

    @RyanCavanaugh
    Member

    Let's say for a moment we had a type operator optionalize that took an object type and produced a new object type whose properties were all optional. What would the trade-offs of that be vs partof for solving the React setState problem?

  3. rogierschouten commented on Jan 25, 2016

    @rogierschouten
    Author

    I think that your description is more concise and clear, and I think optionalize is a very intuitively clear keyword. At first glance, I see no difference in usability between our proposals, do you?

  4. ahejlsberg commented on Jan 27, 2016

    @ahejlsberg
    Member

    A more general purpose alternative would be to allow super constrains for type parameters:

    class ReactComponent<S extends {}> {
        protected _state: S;
    
        /**
         * This method accepts any supertype of S
         */
        protected setState<T super S>(newState: T): void {
        }
    }

    The requirement implied by T super S is that T must be a type to which S is assignable.

  5. rogierschouten commented on Jan 27, 2016

    @rogierschouten
    Author

    Anders Hejlsberg (@ahejlsberg) that would work too, but I would avoid the word 'super' because one would not know whether you mean supertype or superclass or super-something-else. Maybe use 'S assignable T'?

    However I've found a number of use cases for a type operator 'optionalize' like Ryan Cavanaugh (@RyanCavanaugh) suggested - that way I can declare new types and declare variables of optionalized types.

  6. RyanCavanaugh commented on Jan 27, 2016

    @RyanCavanaugh
    Member

    The problem with super here is that it's inaccurate in several ways:

    declare function setState<T super { s1: { x: Derived; y: number; } }>(s: T);
    
    setState( { x1: 32 }); // No error, but wrong
    setState( { s1: { y: 42 } }); // No error, but wrong
    setState( { s1: { x: new Base(), y: 32 } }); // No error, but wrong
  7. kitsonk commented on Jan 28, 2016

    @kitsonk
    Contributor

    I also want to say that this use case is important to other libraries as well (and I think the title is a bit misleading, really what the intent of this is the way to operate on a type to make all its members optional).

    I think it is a common pattern to pass an object literal to a constructor (or factory) function that can be any of the public properties of the instance class, which is then mixed in upon construction. I ran into this issue when trying to type Dojo 1 Dijits. The only option, if I wanted to type the constructor functions, is to create a wholly separate interface which has all optional members.

    I agree super might give the wrong semantic impression. I suspect optionalize or optional would be more semantically clear:

    class A {
      foo: string;
      bar(): string { return 'bar'; };
      constructor<O optionalize this>(options: O) { };
    }
    
    type OptionalA = optionalize A;
    
    /*or*/
    
    class B {
      foo: string;
      bar(): string { return 'bar'; };
      constructor(options: $Optionalize<this>) { };
    }
    
    type OptionalB = $Optionalize<B>;
  8. ahejlsberg commented on Jan 28, 2016

    @ahejlsberg
    Member

    Whatever we do here also applies to Object.assign that was introduced in ES2015.

  9. changed the title [-]Proposal for supporting React component setState() typing[/-] [+]More accurate typing of Object.assign and React component setState()[/+] on Jan 28, 2016
  10. Strate commented on Jan 28, 2016

    @Strate

    Somewhere here in issues I see this proposal for similar case:

    function assign<R extends A,B,C>(a:A, b:B, c:C):R
    

    Isn't it the similar thing?

  11. rogierschouten commented on Jan 28, 2016

    @rogierschouten
    Author

    Artur Eshenbrener (@Strate) I don't think so, see the first use case of post #6218. The first use case was solved like you said but the setState() use case wasn't.

  12. mhegazy commented on Feb 24, 2016

    @mhegazy
    Contributor

    We'd need type operators to solve this, which is a huge work item; Workaround of specifying all-optionals in state definition, which isn't too bad.

  13. 20 remaining items

  14. sfrooster commented on Oct 11, 2016

    @sfrooster

    I can't follow the entire discussion here, but this seems to be related to the reason why I can't find a construct that allows me to say:

    class Message {
        public static TypeID: string;
        public TypeID: string;
    
        public Sequence: number;
    
        constructor() {
        }
    }
    

    and then:

    public receive<T extends Message>(rcvType: T, handler: (msg: T) => void): void {...}
    

    Such that the "type" I'm specifying for T will work. For example:

    class SomeMessage extends Message {
        public static TypeID = "Messages.Commands.SomeMessage";
        public TypeID = "Messages.Commands.SomeMessage";
    
        constructor(public Payload: string){
            super();
        }
    }
    

    This is currently generating the error:

    receive(SomeMessage, (msg: SomeMessage) => {
        console.log(msg.Payload);
    });
    
    The type argument for type parameter 'T' cannot be inferred from the usage. Consider specifying the type arguments explicitly.
      Type argument candidate 'typeof SomeMessage' is not a valid type argument because it is not a supertype of candidate 'SomeMessage'.
        Property 'prototype' is missing in type 'SomeMessage'.
    

    I can shift various and sundry things around, but I cannot find a combination that:
    a) doesn't complain
    b) explicitly ties the two types such that they must be the same type (I can specify things in a way that two different values for rcvType and handler/msg will work, where the intent is that they are the same)

    FWIW, rcvType is important because it's used internally to set things up. I'd also love if I could just not specify rcvType (but I realize type erasure precludes that).

  15. mDibyo commented on Nov 17, 2016

    @mDibyo

    Looks like this has been resolved with #12114.

  16. sfrooster commented on Nov 17, 2016

    @sfrooster

    I'm still trying to digest this PR...

    On Wed, Nov 16, 2016 at 6:17 PM, Dibyo Majumdar notifications@github.com
    wrote:

    Looks like this has been resolved with #12114
    #12114.

    —
    You are receiving this because you are subscribed to this thread.
    Reply to this email directly, view it on GitHub
    #6613 (comment),
    or mute the thread
    https://github.com/notifications/unsubscribe-auth/AC-vl_bEXaisDZ_JaeaRwVSdpr1lC7CPks5q-7kcgaJpZM4HL6Cd
    .

  17. dgoldstein0 commented on Dec 28, 2016

    @dgoldstein0

    #12793 should have solved the react problem. Is there something still open here about Object.assign, or is Pick<...> good enough for it too?

  18. AlexGalays commented on Dec 28, 2016

    @AlexGalays

    Object.assign don't want Pick<> as you can merge any kind of objects into any kind of object.
    If you want to update an object with only objects of the same shape, then you will want to code something that looks a lot like the solution in #12793 indeed.

    I think the only issue with the current Object.assign type definition is when the right hand side object has a key with a different type. Instead of it completely replacing the initial type (like in the implementation) the result becomes the union of both types, which sometimes don't even make sense (e.g number & string)

  19. Igorbek commented on Dec 28, 2016

    @Igorbek
    Contributor

    #10727 would fix it because type of Obejct.assign(a, b) is exactly type of {...a, ...b}.

  20. mhegazy commented on Jun 14, 2017

    @mhegazy
    Contributor

    relevant discussion about distinguishing missing and undefined is tracked in #16524

  21. RyanCavanaugh commented on Aug 15, 2017

    @RyanCavanaugh
    Member

    Fixed with mapped types. Please log new issues if you find any shortcomings in this area

  22. locked and limited conversation to collaborators on Jun 19, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

CommittedThe team has roadmapped this issueSuggestionAn idea for TypeScript

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions