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

Subtraction types #4183

Closed
zpdDG4gta8XKpMCd opened this issue Aug 6, 2015 · 80 comments
Closed

Subtraction types #4183

zpdDG4gta8XKpMCd opened this issue Aug 6, 2015 · 80 comments
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@zpdDG4gta8XKpMCd
Copy link

zpdDG4gta8XKpMCd commented Aug 6, 2015

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...
@RyanCavanaugh
Copy link
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
Copy link
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
Copy link
Member

danquirk commented Aug 6, 2015

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
Copy link
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)

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Aug 21, 2015
@stepancar
Copy link

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

@zpdDG4gta8XKpMCd
Copy link
Author

zpdDG4gta8XKpMCd commented Apr 26, 2016

open, needs a proposal, considered as a suggestion

@mhegazy
Copy link
Contributor

mhegazy commented Apr 26, 2016

@stepancar
Copy link

@mhegazy, sorry, thank you!

@rederteph
Copy link

It seems like very useful functionality.

@Ciantic
Copy link

Ciantic commented Jun 26, 2016

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
Copy link
Author

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

#9407 (comment)

@bcherny
Copy link

bcherny commented Jul 30, 2016

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

@Aleksey-Bykov Aren't Number and NaN the same type, but different domain of values? How would this work?

@tejacques
Copy link

I'd like this feature, but its implementation might be pretty tricky.

Specifically I'd like it to make it easier to write fluent APIs that prevent repeated calls, similar to the original example when loading data.

It seems like a possible way to implement this might be to create a NOT type ~A:

~any = {}
~{} = any
~A = any, without A or any | ~A, currently A | any reduces to any which would have to change

With unions: A | ~A exposes the properties in {}

function guard(val: A | ~A | B) {
  if(isA(val)) {
    // val: A
  } else {
    // val: ~A | B
    // exposes properties in B that aren't in A
    if (isB(val)) {
      // val: B this is probably OK if B has properties that are present in A, because B is after ~A in the type definition
    } else {
      // val: ~A
    }
}

With intersections: A & ~A exposes the properties in A & any (currently A & any reduces to just any, which would have to change)

A question is whether A & B | ~A is equivalent to B | ~A

For simplicity it may make sense to not treat them as distinct types. In either case it looks like we can run into trouble with type guards:

function impossibleGuard(val: A & B | ~A) {
  // val exposes properties in B but not A
  if(!notA(val)) {
    // val: A & B instead of just B, but val shouldn't actually have any properties that are in A
}

function impossibleGuard2(val: B | ~A) {
  // val exposes properties in B but not A
  if(isB(val)) {
    // val: B, but it can't actually have defined properties that are in A
  }
}

The major issue here really seems to be that order matters now when NOT types are in play. A | ~A is not the same type as ~A | A. If that's the case maybe a different approach that doesn't mess up existing union/intersection type logic would be better, perhaps that would just look like a more explicit A - B, and not allow the unary type -A at all.

@zpdDG4gta8XKpMCd
Copy link
Author

might be related #7993

@mhegazy
Copy link
Contributor

mhegazy commented Nov 15, 2016

#12215 seems to have a proposal that is more inline with ideas discussed previously in the TS design meeting. We would favor the approach in #12215 for supporting this feature.

@pelotom
Copy link

pelotom commented Nov 23, 2017

@masaeedu I'm interested in having a clean, comprehensible and universal algebra of types. It's not the business of a type operator to exclude things like null and undefined, it's the job of the client code that's using it. Also, there's nothing inherently unsafe about undefined or null; they're only unsafe when they are implicitly part of every single type! With strictNullChecks enabled we know exactly when they may be present and are forced to check for them, so they're perfectly safe.

@masaeedu
Copy link
Contributor

masaeedu commented Nov 23, 2017

It's a question of convenience. When you say !number with strictNullChecks enabled, do you want the term to suddenly become nullable? It seems natural to say {} - number if you're interested in continuing to talk about non-nullable things besides numbers, and ({} - number) | undefined if you want to also deal with undefined.

A clean and comprehensible algebra of types is a goal I share, but there's nothing inherently unclean or opaque about logical disjunction.

@KiaraGrouwstra
Copy link
Contributor

Having them become nullable is probably fine -- real-life use-cases likely all do something like (string | number) & !number, meaning null and undefined are automatically out of the picture anyway.

@pelotom
Copy link

pelotom commented Nov 23, 2017

When you say !number with strictNullChecks enabled, do you want the term to suddenly become nullable?

Yes. I want !number to mean exactly this: “anything that is not a number”.

@masaeedu
Copy link
Contributor

masaeedu commented Nov 23, 2017

@tycho01 In situations where you're intersecting the negated type with a non-nullable type, both {} - T and !T would behave identically. The tradeoff is between having to do | null | undefined to get {} - T to accommodate nulls, and doing & !(null | undefined) to get negated types to exclude nulls. I prefer inconvenience in glomming on null/undefined to inconvenience in remaining null-free.

If I have a {} - number (or more realistically perhaps, a T extends thereof), I can treat it as an object without being forced to do a null check, which is my intent in most cases. The type system ensures that even public APIs that use {} - style negated types will not hand me nulls and force me to deal with them.

@KiaraGrouwstra
Copy link
Contributor

@masaeedu: I thought in {} - number, {} does not include null / undefined (assuming strictNullChecks), while the subtraction should remove some more, rather than adding null back in. Did I misunderstand?

@masaeedu
Copy link
Contributor

@tycho01 In {} - number you're correct, you wouldn't get any null/undefined. In !number you would. So I can e.g. do hasOwnProperty on {} - number, but not on !number.

@piotrwitek
Copy link

@marsiancba I think Minus mapped type can be useful, thanks for the idea.
Although I would suggest to use slightly different Omit implementation that will properly handle optional properties edge case, see the following example (btw. it's not mine, you can find it somewhere in this thread):

export type Minus<T, U> = {[P in Diff<keyof T, keyof U>]: T[P]};
Minus<{ a: string, b?: number, c: boolean }, { a: any }>; // { b: number | undefined; c: boolean; }

export type Minus2<T, U> = Pick<T, Diff<keyof T, keyof U>>;
Minus2<{ a: string, b?: number, c: boolean }, { a: any }>; // { b?: number | undefined; c: boolean; }

@mhegazy
Copy link
Contributor

mhegazy commented Feb 10, 2018

With #21847 Exclude is now part of the standard library, the scenarios listed in the OP involving unions of literal types should be possible. Exclude<T, U> excludes the types from T that are not in U. e.g.:

type C15 = Exclude<number | string, string>; // number
type C16 = Exclude<"a" | "b" | "c", "b">;  // "a" | "c"

Another type added in #21847 is NonNullable that filters out null | undefined from the type.

type C17 = NonNullable<number | undefined | null>; // number

@tejacques
Copy link

Awesome news! Can't wait to play around with this.

@zpdDG4gta8XKpMCd
Copy link
Author

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

@SalathielGenese
Copy link

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
Copy link

bcherny commented Mar 9, 2018

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

npm install typescript@next

@SalathielGenese
Copy link

SalathielGenese commented Mar 9, 2018

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
Copy link

mbrowne commented Apr 19, 2018

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
Copy link

pelotom commented Apr 19, 2018

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
Copy link
Contributor

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

@pelotom
Copy link

pelotom commented Apr 19, 2018

@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
Copy link

Ciantic commented Apr 21, 2018

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
Copy link
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
Copy link
Author

zpdDG4gta8XKpMCd commented Apr 21, 2018

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
Copy link

pelotom commented Apr 21, 2018

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

@KiaraGrouwstra
Copy link
Contributor

KiaraGrouwstra commented Apr 22, 2018

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.

@microsoft microsoft locked and limited conversation to collaborators Jul 31, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests