Skip to content

Partial object application/"outersection" types #6895

Closed

Description

I've searched for how to do this and if there are any open issues requesting it, but couldn't find anything. Apologies if I didn't search enough and there is an issue somewhere.

What I'd like to do is essentially the opposite of intersection types. I want to subtract one type from another, and get the delta as a new type.

Here's a code example:

type X = {
  prop1: string;
  prop2: string;
  prop3: string;
}

type Y = {
  prop1: string;
}

function giveMeX(x: X) {

}

function partiallyApplyGiveMeX(y: Y) {
  return (z: X - Y) => giveMeX(Object.assign(y, z));
}

var y: Y = {
  prop1: ''
};

var wrappedGiveMeX = partiallyApplyGiveMeX(y);
// Don't have to pass prop1 to wrappedGiveMeX
var x = wrappedGiveMeX({prop2: '', prop3: ''});

The type annotation z: X - Y is something I just made up, but what it does is almost the opposite or z: X & Y.

The real world use case I have for this is that in the React world, it's very common to wrap components in other components. I might have one component which expects five props, but when I use that component I don't want to have to specify all of them. Instead I would wrap that component in another component that would pass some of the props to the inner component. And I would like the wrapping component to be typed as a component that expects the other props.

Here's a simple example:

type X = {
  prop1: string;
  prop2: string;
  prop3: string;
}

type Y = {
  prop1: string;
}

function InnerComponent(props: X) {
  return (
    <div></div>
  );
}

function wrapComponent<T1, T2>(Component: React.StatelessComponent<T1>, fn: () => T2) {
  return (props: T1 - T2) => {
    return <Component {...props} {...fn()} />;
  };
}

var WrappedComponent = wrapComponent(InnerComponent, () => ({prop1: ''}));
// Now I don't have to pass prop1 to WrappedComponent
var jsx = <WrappedComponent prop2="" prop3="" />;

A question is what happens if the subracting type isn't a strict subset of the other type. I'd say that it would be a compile error and that the right side type has to be a subset of the left type.

Another thing is that it shoould be composable. I could say x: X - Y - Z which would subtract left to right like this x: ((X - Y) - Z).

Activity

  1. andersekdahl commented on Feb 16, 2016

    @andersekdahl
    Author

    Here's another use case:
    In some cases you want to extend an existing type, but you want to change the type of one or more properties. Something like:

    type TypeA = {
      url: string;
      text: string;
    }
    
    type TypeB = TypeA - {
      url: string;
    } & {
      url: {path: string, query: any};
    }
    

    The real world use case I have for this is that I want to define a React component that takes all props that an <input> does, but I want to redefine what's allowed for the type property. Something like:

    type Props = React.HTMLAttributes - {
      type?: string;
    } & {
      type?: 'text' | 'email';
    }
    

    In some cases it can be solved by creating a new base type without the properties that differ, but that's not really possible if the type isn't defined in your own code.

  2. yortus commented on Mar 8, 2016

    @yortus
    Contributor

    I wonder if 'outersections' could also be useful in making type guards more general (i.e. not just always starting from a union type and removing things from it). E.g.:

    interface Thing {a;b;c;then;e;f}
    interface Thenable {then}
    
    function isThenable(obj: any): obj is Thenable {
        return 'then' in obj;
    }
    
    function foo(x: Thing) {
        if (isThenable(x)) {
            x.then(...)        
        }
        else {
            // here type of x is Thing - Thenable
            x.a     // OK
            x.then  // ERROR
        }
    }
  3. silviogutierrez commented on Mar 8, 2016

    @silviogutierrez

    I posted #7423 and was linked to here. Indeed this is very common. Particularly for decorators or component's like Provider from redux.

  4. added
    Too ComplexAn issue which adding support for may be too complex for the value it adds
    RevisitAn issue worth coming back to
    and removed on Mar 15, 2016
  5. RyanCavanaugh commented on Mar 15, 2016

    @RyanCavanaugh
    Member

    This would be a huge amount of work (basically the same amount as intersection or union types were) for a much smaller set of scenarios, but it's still something we want to address at some point. Likely if/when we have a general notion of type operators, this would be the sort of thing that they would handle.

    If you run into other scenarios where this would be useful, please do leave a comment so we can prioritize accordingly.

  6. jkillian commented on Apr 6, 2016

    @jkillian

    I came across this when working with React also. In my situation, I needed to do something like this:

    interface A extends B, C { }
    interface B { field: SomeType; ... }
    interface C { field: SomeOtherType; ... }

    Since B and C have conflicting types for field, they can't both be extended by A.

    I understand that this is somewhat an untypical scenario and would be a lot of work on the tsc end, but an "outersection" or "subtraction" type would be neat!

  7. raveclassic commented on Jan 18, 2017

    @raveclassic

    Seems like basic type outersection is already partially implemented when destructuring objects with spreads:

    type TProps = {
        foo: string,
        bar: any,
        name: number
    };
    
    declare const props: TProps;
    
    const {
        foo, //string
        ...rest ////type is correctly inferred -  {bar: any, name: number}
    } = props;
    
    //type of rest can be treated as an outersection of TProps and {foo: TProps['foo]}

    Can this logic be reused for deconstructuring types?
    Something like:

    type {
        foo: TFoo, //I believe this can be related to new TS2.1 lookup types
        ...TRest //{bar: any, name: number}
    } = TProps;
    

    UPDATE Have found #10727 and #12215

  8. olyis commented on Jun 5, 2017

    @olyis

    Another use case for this (in particular, permitting subtraction of literals) would be to enable the following:

    type SizedStringPairs = {
        count: number,
        [key: string - 'count']: string
    }
    

    which could be useful for typing a reasonably large class of objects.

  9. 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

Assignees

No one assigned

    Labels

    RevisitAn issue worth coming back toSuggestionAn idea for TypeScriptToo ComplexAn issue which adding support for may be too complex for the value it adds

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions