Skip to content

Subtraction types #4183

@zpdDG4gta8XKpMCd

Description

@zpdDG4gta8XKpMCd

Another type-safety measure. Sometimes it's desired to limit what developers can do with a value. Not allowing them to get to certain properties of it looks sufficient.

Example: We render HTML elements to PDF on the client side. In order to do so we need to run element.getBoundingClientRect to get a bounding box in effect. It is an expensive operation that we wish could only be done once and then the result of it would be passed around along with the element to be rendered. Unfortunately nothing stops developers from ignoring that result and running the same method again and again as long as they can get to element.getBoundingClientRect. Now I wish I could strip that method so no-one can see it once the box is calculated. Subtaction types would solve the problem.

type HTMLDivSpecifics = HTMLDivElement - Element;

Proposal

add a type operator that produces a new type out of 2 given types according to the rules that follow:

type C = A - B;

This feature would require a new negated type like number ~ string which is a number that cannot take number & string.

As far as the precedence of new type operator, it should go:

  1. intersection &
  2. union |
  3. subtraction -

so that number & boolean | string - string, means ((number & boolean) | string) - string

Generics

  • should produce a yet to be resolved type, should be stored as an expression which will produce either a type or an error when all type parameters are known
type Minus<A, B> = A - B; // yet to be calculated

Primitives

  • if the left type (minued) isn't a sub-type of the right type (subtrahend) the - operation should result to an type error
  • never and any should be specially handled
type C = number - number; // {}
type C = number - {}; // number
type C = {} - number; // error
type C = number - string; // error
type C = number - 0; // number ~ 0

type C = number | string - boolean; // error
type C = number - void | null | undefined; // error

type C = number - any; // {}
type C = any - any; // any
type C = any - number; // any ~ number
type C = any - never; // error;
type C = never - any; // error;
type C = number - never; // error
type C = never - number; // error
type C = never - never; // error

type C = number | string - string; // number
type C = number - number | string; // {}
type C = number | string - {}; // number | string

type C = number & string - boolean; // error
type C = number & string - string; // number ~ string

Products

  • only matching properties should be considered, non-matching properties of the left type should stay intact, non-matching properties of the right type should be disregarded
  • if the names of 2 properties match their types are subject for - operation that produces the type of the resulting property of the same name
  • if applying - on 2 properties of the same name gives {}, the property gets dropped from the resulting type
type C = {} - { x: number }; // {}
type C = { x: number } - {}; // { x: number }
type C = { x: {} } - { x: number }; // error
type C = { x: number } - { x: {} }; // { x: number }
type C = { x: number } - { y: number }; // { x: number }
type C = { x: number } - { x: number }; // {}
type C = { x: number | string } - { x: string }; // { x: number }
type C = { x: number & string } - { x: string }; // { x: number ~ string }
type C = { x: number } - { x: string }; // error

Functions (2 certain signatures)

  • both functions must have the same number of parameters, otherwise it's an error
  • types of corresponding parameters are subject to the - operator
  • types of results must 100% match and should be kept intact, otherwise it's an error
  • if - on 2 parameters gives {} the resulting parameter is {}
  • if all resulting parameters are {} the resulting type is {}
type C = ((x: number) => string) - ((x: number) => string); // {}
type C = ((x: number) => number) - ((x: number) => number); // (x: {}) => number
type C = ((x: number | string) => string) - ((x: string) => string); // (x: number) => string
type C = ((x: number) => string) - ((x: string) => string); // error
type C = ((x: number | string) => string) - (() => string); // error
type C = (() => string) - ((x: number) => string); // error

Overloads

  • to be continued...

Activity

RyanCavanaugh

RyanCavanaugh commented on Aug 6, 2015

@RyanCavanaugh
Member

What about something like this?

interface DoNotCalculateAgain {
    getBoundingClientRect(): void;
}

let y: DoNotCalculateAgain & HTMLElement;
// z: void, so an inevitable compile error on use
let z = y.getBoundingClientRect();
zpdDG4gta8XKpMCd

zpdDG4gta8XKpMCd commented on Aug 6, 2015

@zpdDG4gta8XKpMCd
Author

hm, didn't know it works this way, could be useful for anything that doesn't return void, which is better than nothing

danquirk

danquirk commented on Aug 6, 2015

@danquirk
Member

Alternatively, wouldn't you have the object type itself guard against this type of re-initialization? ie

class MyElement {
    private boundResult = ...
    public getBoundingClientRect() {
        if(boundResult) return boundResult
        ...
        boundResult = ...
        return ...
    }
}```
zpdDG4gta8XKpMCd

zpdDG4gta8XKpMCd commented on Aug 6, 2015

@zpdDG4gta8XKpMCd
Author
  1. we are talking about a standard DOM element interface, there is no place to put that safer wrapper you are talking about
  2. not letting see a method results to statically verified and more correct code as opposed to making assumptions at runtime
  3. honestly there are a lot of ways to deal with this situation without involving subtraction types, it's not even a problem in the first place, just an example to show where a feature like this can be helpful

Another example: to support the principle of interface segregation, instead of passing a thick interface to a specialized function that only needs a few properties we could have cut it to a sub-type that would be just sufficient enough to run leaving all irrelevant members outside (same can be done today by employing existing features, however at a price of larger code base and requiring more maintenance)

added
SuggestionAn idea for TypeScript
Needs ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.
on Aug 21, 2015
stepancar

stepancar commented on Apr 26, 2016

@stepancar

@Aleksey-Bykov , @RyanCavanaugh.
Whats status of this issue?

zpdDG4gta8XKpMCd

zpdDG4gta8XKpMCd commented on Apr 26, 2016

@zpdDG4gta8XKpMCd
Author

open, needs a proposal, considered as a suggestion

mhegazy

mhegazy commented on Apr 26, 2016

@mhegazy
Contributor
stepancar

stepancar commented on Apr 26, 2016

@stepancar

@mhegazy, sorry, thank you!

rederteph

rederteph commented on Apr 26, 2016

@rederteph

It seems like very useful functionality.

Ciantic

Ciantic commented on Jun 26, 2016

@Ciantic

I think there should be more set operations over the fields, like subtraction there could be intersection of fields:

interface A {
  firstName: string;
  lastName: string;
}

interface B {
  firstName: string;
  grade: string;
}

// "Set operation of intersection of fields"
let c: typeof A fieldIntersection B

// would mean c is now
interface C {
  firstName: string;
}

Not to be confused with intersection types which are actually field union.

There are use cases for field subtraction and field intersection in a LINQ like type-safe SQL builders.

zpdDG4gta8XKpMCd

zpdDG4gta8XKpMCd commented on Jul 12, 2016

@zpdDG4gta8XKpMCd
Author

numbers without NaN would be another interesting case for subtraction types: type AlwaysANumber = number - NaN

#9407 (comment)

100 remaining items

zpdDG4gta8XKpMCd

zpdDG4gta8XKpMCd commented on Feb 10, 2018

@zpdDG4gta8XKpMCd
Author

closing since the major part of the issue is covered by conditional types, the rest is too vague and mostly irrelevant

SalathielGenese

SalathielGenese commented on Mar 9, 2018

@SalathielGenese

I plead for a reopening of this issue.

@Aleksey-Bykov , you may have seen my comment at your #22375 ... I'm unable to have my decorator accept distinct signatures from static to instance side.

#21847 seem the fix but event my TS v2.7.2 released 21 days back says it cannot find Exclude (while the PR have been merged 5 days before the release). This lead me to question which point have been released v2.7.2 -/- I'm not sure therefore how to benefit from it.

bcherny

bcherny commented on Mar 9, 2018

@bcherny

@SalathielGenese This is shipping as part of 2.8. https://github.com/Microsoft/TypeScript/wiki/Roadmap

npm install typescript@next
SalathielGenese

SalathielGenese commented on Mar 9, 2018

@SalathielGenese

Much thanks @bcherny


[UPDATE]

I've just moved to typescript@next and tried to type my decorator instance side using Exclude<{}, ConstructorLike> (see comment) but still, it is not working.

Seem like there no way by which I can tell TS that an object (Object or {}) won't accept constructor ({new(...)})

mbrowne

mbrowne commented on Apr 19, 2018

@mbrowne

I'm still playing around with TypeScript 2.8, but FYI, this is included as part of the new "Conditional Types" feature, documented here:
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html

This note from that page seems worth highlighting:

Note: The Exclude type is a proper implementation of the Diff type suggested here. We’ve used the name Exclude to avoid breaking existing code that defines a Diff, plus we feel that name better conveys the semantics of the type. We did not include the Omit<T, K> type because it is trivially written as Pick<T, Exclude<keyof T, K>>.

pelotom

pelotom commented on Apr 19, 2018

@pelotom

Exclude is a great step forward, but it does not allow true subtraction. In particular, we cannot subtract from infinite types. For example, I cannot express, "any string except 'foo'" as a type:

Exclude<string, 'foo'>

is just string.

KiaraGrouwstra

KiaraGrouwstra commented on Apr 19, 2018

@KiaraGrouwstra
Contributor

@pelotom maybe something in this direction could work (with conditional types first checking for 'foo' then for string), I dunno.

pelotom

pelotom commented on Apr 19, 2018

@pelotom

@tycho01 sure, you can pull tricks to kind of sort of fake it in certain circumstances, but even that doesn't fully work:

type NotFoo<X extends string> = X extends 'foo' ? never : X;

declare function handleNotFoo<T extends string & NotFoo<U>, U extends string = T>(val: T): void;

handleNotFoo('foo'); // correctly forbidden
handleNotFoo('foo' as string); // oops, that was allowed
Ciantic

Ciantic commented on Apr 21, 2018

@Ciantic

I don't understand how handleNotFoo('foo' as string); // oops, that was allowed could be checked? If one forcibly casts a value to certain type, it would loose the information that it is "foo".

However, for someone who always seems to be learning about new type features in TypeScript, it totally amazes me it can even be made to work normally: handleNotFoo('foo'); // correctly forbidden

zpdDG4gta8XKpMCd

zpdDG4gta8XKpMCd commented on Apr 21, 2018

@zpdDG4gta8XKpMCd
Author

it doesn't have to be forced though with the same effect

function id<T>(value: T): T { return value; }
handleNotFoo(id<string>('foo'));

or

const foo = 'foo';
let x = foo;
handleNotFoo(x);
zpdDG4gta8XKpMCd

zpdDG4gta8XKpMCd commented on Apr 21, 2018

@zpdDG4gta8XKpMCd
Author

downcasting upcasting is automatic in TS and it's somewhat convenient but unsafe assumption (compared to F# for example where you have to explicitly state it)

pelotom

pelotom commented on Apr 21, 2018

@pelotom

string should not be assignable to Not<'foo'>, because it is possibly 'foo'. Otherwise you are implicitly downcasting.

KiaraGrouwstra

KiaraGrouwstra commented on Apr 22, 2018

@KiaraGrouwstra
Contributor

To forbid string on this specific example (untested):

type NotFoo<X extends string> =
    X extends 'foo' ? never :
    string extends X ? never : X;

If you wanna generalize to automate that string part, you can have something like this as a helper:

type Widen<T> =
  T extends boolean ? boolean :
  T extends number ? number :
  T extends string ? string :
  T;`

On auto-widening being evil, #17785.

locked and limited conversation to collaborators on Jul 31, 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

    Needs ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.SuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @Ciantic@pelotom@piotrwitek@mbrowne@zpdDG4gta8XKpMCd

        Issue actions

          Subtraction types · Issue #4183 · microsoft/TypeScript