Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Partial object application/"outersection" types #6895

Closed
andersekdahl opened this issue Feb 4, 2016 · 7 comments
Closed

Partial object application/"outersection" types #6895

andersekdahl opened this issue Feb 4, 2016 · 7 comments
Labels
Revisit An issue worth coming back to Suggestion An idea for TypeScript Too Complex An issue which adding support for may be too complex for the value it adds

Comments

@andersekdahl
Copy link

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

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript In Discussion Not yet reached consensus labels Feb 4, 2016
@andersekdahl
Copy link
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.

@yortus
Copy link
Contributor

yortus commented Mar 8, 2016

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
    }
}

@silviogutierrez
Copy link

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

@RyanCavanaugh RyanCavanaugh added Too Complex An issue which adding support for may be too complex for the value it adds Revisit An issue worth coming back to and removed In Discussion Not yet reached consensus labels Mar 15, 2016
@RyanCavanaugh
Copy link
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.

@jkillian
Copy link

jkillian commented Apr 6, 2016

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!

@raveclassic
Copy link

raveclassic commented Jan 18, 2017

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

@olyis
Copy link

olyis commented Jun 5, 2017

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.

@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Revisit An issue worth coming back to Suggestion An idea for TypeScript Too Complex An issue which adding support for may be too complex for the value it adds
Projects
None yet
Development

No branches or pull requests

7 participants