microsoft/TypeScript

Subtraction types

zpdDG4gta8XKpMCd opened this issue Β· 80 comments

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

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();

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

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

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

open, needs a proposal, considered as a suggestion

@mhegazy, sorry, thank you!

It seems like very useful functionality.

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.

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

#9407 (comment)

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?

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.

might be related #7993

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

With the introduction of mapped types, the use cases for this seem to be ever increasing.

What's the status of this suggestion/proposal?

The behavior described here is a little confusing to me. | is the sum type operator, meaning the type operator which produces a new type admitting all values from the universe of the left operand plus all values from the universe of the right operand (with the caveat that the sets of values represented by types may overlap in subtype systems like TypeScript). E.g. number | string accommodates all numbers plus all strings.

By the same token it seems like the subtraction type should obey something like (T - U) | U = T (with reasonable exceptions for overlap due to structural subtyping). E.g. it doesn't make sense to me that number - number produces {}. This is a type that admits of any value! const x: number - number = "potato" would be legal.

Wouldn't it make more sense for number - number to be never, i.e. a type that admits no values?

although technically never isn't supposed to have values, TypeScript would not say anything if you do

declare var x: never;
const y: string = x;

looks like never was design to solve very particular practical problem, number-number need to become a new type that is much like never but yet prohibits declare var x: ...

so we need an identity type let's call it zero which would:

  • not allow any values of it to be declared (by maybe undeclaring them? that would work for substracted properties)
  • posses the following properties
  1. T | zero == T
  2. T & zero == zero
  3. T - T = zero

please note that never has properties 1 and 2 already, but yet allows values being declared

simple way of making sure that zero type is never used in declarations is by not giving it any syntax (like it used to be for null and undefined types back in a day)

if we do that, then the only way for zero to emerge would be as a result of type expression T - T which seems suffice

if so, the following declaration must be banned:

declare var x : string - string;

as well as this one too:

function fn<T>() {
    var x : T - T;
}

the following one might or might not be banned, the answer to this can only be made when X and Y are resolved to concrete type arguments

function fn<X, Y>() {
   var x: X - Y;
}

That seems like a good idea. I need to maybe look at some other languages with subtyping to understand how the bottom type is treated there, I know Haskell explicitly disallows empty data types, perhaps for similar reasons.

Another thing to consider is precisely what kind of operator - is intended to mean. Given two types T and U, another type V may be a subset of one of them, both of them, or neither of them. The question is whether we want this "subtraction type" to be the binary relative complement operator (i.e. T - U is a supertype of all types that are subtypes of T, so long as they aren't also subtypes of U) or the unary absolute complement operator (i.e. -T is a supertype of any conceivable value that isn't a subtype of T).

If we mean the latter (which is perhaps what @tejacques was suggesting), then copying from the equivalence laws, T - U can be expressed as T & -U.

@masaeedu Haskell does allow empty data types, if you disregard bottom. See https://wiki.haskell.org/Empty_type and https://hackage.haskell.org/package/void-0.6.1/docs/Data-Void.html.

Ceylon has a very similar type system to typescript and the bottom type is called Nothing

Also type negation:

#15480 (comment)

@masaeedu @streamich I like the T & -U approach, using type negation instead of just a special case: it'd also allow TypeScript to properly type Promises. (I'd rather !T over -T, since set complement is closer to logical negation than numeric negation.)

interface PromiseLike<T, E = Error> { /* ... */ }

interface PromiseLikeRec1<T extends !PromiseLike<T>, E> extends PromiseLike<PromiseConvertible<T, E>, E> {}
type PromiseLikeRec<T extends !PromiseLike<T>, E = Error> =
    PromiseLike<T, E> | PromiseLikeRec1<T, E>;

type PromiseCoercible<T extends !PromiseLike<T>, E = Error> = T | PromiseLikeRec<T>;

declare class Promise<T extends !PromiseLike<T>, E = Error> {
    constructor(init: (
        resolve: (value: PromiseCoercible<T, E>) => void,
        reject: (value: E) => void
    ) => any);

    static resolve<T extends !PromiseLike<any, any>>(value: T): Promise<T, never>;
    static resolve<T extends PromiseLikeRec<U, E>, U extends !PromiseLike<T, any>, E = Error>(value: U): Promise<U, E>;
    static reject<E>(value: E): Promise<never, E>;

    then<U extends !PromiseLike<any, any>>(
        onResolve: (value: T) => PromiseCoercible<U, E>,
        onReject?: (value: E) => PromiseCoercible<U, E>
    ): Promise<U, E>;

    then(onResolve: void, onReject?: (value: E) => PromiseCoercible<T, E>): Promise<T, E>;
    catch(onReject: (value: E) => PromiseCoercible<T, E>): Promise<T, E>;

    // ...
}

I added a working Minus at 4d14c9f.

@zhaojinxiang refinement types have been proposed in #7599.

maybe we should think more over minus type

function isInt(x : number){
  return x % 1 === 0
}
type Int = Apply<isInt,number>
function isEven(x : Int){
  return x % 2 === 0
}
type Even = Apply<isEven,Int>

@zhaojinxhang That's a bad idea until you can come up with a constexpr-like (C++) syntax, so it can be guaranteed the value is known at compile time. Also, your isInt should instead be returning x % 1 === 0 - it's faster and more likely to be accepted iff your proposal here is.

@zhaojinxiang: following #7599 (comment) I would go about this like:

type Int = <
  T extends number,
  IntConstraint = ((v: 0) => 'whatever')({
      (v: 0): 0;
      (v: any): 1;
  }(Modulo<T,1>))
>(v: T) = T;`

... given a type-proof modulo operator Modulo. I had one, but currently broken and only able to deal with whitelisted input. Fixing that needs type arithmetic, an idea which had been rejected (#15794 (comment)).

Edit: to clarify, it is definitely preferable to be able to directly hook into existing function signatures, as @zhaojinxiang wanted for isInt. Now, TS runs types, rather than JS, but given a well-typed isInt that'd be conceivable. This again presumes type arithmetic though.

@Aleksey-Bykov

if the left type (minued) isn't a sub-type of the right type (subtrahend) the - operation should result to an type error

according to your example, left-type should be super-type

The void type is an empty set you were looking for, so

any = any - void

it has a logical explanation. Try this:

let x : void;

and now try to assign a value to the x. There's no value you'd be able to assign. Type is just a set of values, you'd be able to assign to such an expression.

@Durisvk I can do x = undefined.

Also, there's no sensible notion of any type operator applying to any, since it is simultaneously top and bottom. It's like an escape hatch from the type system. If you want to talk about the type of all things, it should be {}.

void has it's own semantics:

  • although there is no value of type void, but void is assignable from null and undefined
const x: void = undefined;
  • function with the return type void doesn't need to have a return statement in it
function doThings(): void { }
  • function returning void is assignable by a function returning any type
const x: () => void = () => true;

type for all things (top type) is {} | null | undefined, consider:

const allThings : {} = null; 

Ok but what I'm trying to find, is the type which is not the top type. Although it's nice to know that {} is the type of all types (set of all sets). I'm trying to find an answer to the problem with your subtraction approach. What if I say this:

type NOTHINGNESS = any - any
or like you say:
type NOTHINGNESS = {} - {}

(btw I would use void as a name of the type but it's obviously misused because it's not an empty type (set), but it contains a primitive subtype undefined)

this has to be a type which you won't be able to construct in any way. In Haskell it's Void type, but as you've pointed out in Javascript there's the primitive type called undefined.

If Typescript would introduce the type NOTHINGNESS which wouldn't be constructable with any value, it would make this really useful stuff possible:

type LikeReallyAny = any | {}; // just to make it clear
type SurelyDefined = LikeReallyAny - (undefined | null);

function x() : SurelyDefined {
     // here you !must! return some value (other than null or undefined)
     // ...
}

This would open the doors for the new operator, which from bool logic is known as not or negation, which would be defined like this:

operator !<T> = (any | {}) - T; // which is syntax I just came up with, just to illustrate.

Then you could do this:

function x() : !undefined & !null {
    // here you also must return some value...
    // ...
}

Then you could do so-called distinct union:

type Distinct<A, B> = (A | B) & !(A & B) = (A | B) & (!A | !B);

Which is similar to Either in Haskell and you can find it in literature as sum type or coproduct (in Category Theory for example).

And to your proposal, I wouldn't agree with these Products:

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

because I think (correct me if I am wrong) this is not how the subtraction works in Set Theory.

I would do it like this:

type C = {} - { x: number }; // anything but { x: number }
type C = { x: number } - {}; // NOTHINGNESS
type C = { x: {} } - { x: number }; // { x : (anything but a number) }
type C = { x: number } - { x: {} }; // { x: NOTHINGNESS }
type C = { x: number } - { y: number }; // { x: number } <= correct (because there's no intersection between those two types
type C = { x: number } - { x: number }; // NOTHINGNESS
type C = { x: number | string } - { x: string }; // { x: number } <= correct
type C = { x: number & string } - { x: string }; // this is like: void - string = void (including undefined)
type C = { x: number } - { x: string }; // NOTHINGNESS

But this is all just in theoretical fashion, because the type NOTHINGNESS is not designed neither in Javascript nor Typescript, but it could be possible to implement it in Typescript only without Javascript knowing about it. It would just serve as the type-checking helper. But this will be really hard to implement and would require a lot of thinking. Like for example:

function returnsUndefined() : NOTHINGNESS {
    // Don't do anything and don't return anything
}

This would must throw an error. Because every function that has no return statement defined explicitly returns undefined, but undefined is not an element of type NOTHINGNESS.

This is just my poor opinion, I am not an expert in this topic, but I really like Typescript and I would be glad to help it grow.

@Durisvk TypeScript has the never type which represents nothing: i.e. the empty set. No value may inhabit never, so any assignment to a reference of type never will produce an error.

Nice to hear, so the negation of any|{} could be never,

and subtraction:

(any|{}) - (any|{}) = never

@Durisvk Just FYI, fenced code blocks are a lot easier to read if you write them like

```ts

@Durisvk the original post is outdated, here are a few second thoughts on what subtraction should be: #4183 (comment)

@Aleksey-Bykov (re: this comment)

never is a proper bottom type. In theory, you can't generate a never value in TS itself, but you can violate that soundness* in type definitions by declaring a variable of type never** or accessing a property of type never. Conversely, TS trusts that your definitions files are actually true to what you're actually passing, much like C inherently trusts extern declarations. If they're wrong, you're running into undefined territory and it's no longer the language's fault if you run into issues.

The assumption with never is that accessing the value is impossible, and that it always throws. That's the whole purpose of having a bottom type assignable to everything - it's a type to reference a value that cannot possibly be accessed under any circumstances. And if you can't access a value, the type of that value is completely irrelevant.

For some analogues in other languages:

  • Rust: ! (called divergent)
  • Scala/Kotlin/Ceylon: Nothing
  • Swift: Never

* All that really needs done to fix these never unsoundness issues is implementing #12825 (allowing returned never to influence control flow analysis). That would turn such usage into compiler errors over unreachable code, even in cases like let bar = foo() or bar(foo()), where declare function foo(): never.

** Prohibiting declare var foo: never would just make sense, since there's not really any use case for it - you might as well just not declare it.

@isiahmeadows I don't understand the point of disagreement. His comment proposes a bottom type that is identical to never, but which prohibits declaration, which you seem to agree with. Merely making never participate in control flow analysis won't solve the problem if it is possible to simply declare values of type never (or of a type that indirectly resolves to never), as shown in the example in his comment:

declare var x: never;
const y: string = x;

The typing rules do not prohibit this assignment, nor should they, since the bottom type is a subtype of all types.

@masaeedu My disagreement is more so from the fact that the issues he described are fixable without introducing a whole new, highly duplicative type.

Indirectly, #12825 should fix that issue, assuming the control flow analysis in general also includes reachability analysis (which it hopefully should from a theoretical standpoint), because it would complain about unreachable code starting at const y: string = .... It can't possibly reach assignment, and by making never participate in reachability analysis, it can detect that impossibility pretty easily.

Apologies if I placed too much emphasis on the concept of a bottom type.

It is already possible to define "Minus like" type operator in current typescript:

type Minus<A extends B, B> = {[BT in keyof B]: never } & A;

Example:

image

However it has some limitations:

  • As properties from A exist in the result (with never type), they are offered in content assist and can be used in certain situations (e.g. assigned to/from any variable or passed as any argument)
  • It is not possible to create object literal for minus type, as compiler will expect that you will provide values also for never fields. But it is possible to create object literals for Partial<Minus<A,B>>.

@marsiancba: if we're talking objects, that sounds kinda like the Omit from #12215 that mhegazy mentioned.
I'd interpreted the Minus proposed in this thread as distinct from that, i.e. disallow types that satisfy all of B rather than any of it.

Another limitation of my solution for Minus:

  • TS allows calling never properties:

image

@tycho01: Yes, I know that it is different in many ways. But may be it will help someone - e.g. I have found this thread while searching for solution with objects, which is likely a more common use case than e.g. (number|string) - string. Also if implementing full minus type operator in TS will prove to be too complicated, may be perfecting TS for the above solution for objects may be easier. Seems that all it needs is just some small changes:

  • never can not be assigned to any
  • never type property does not need to be initialized/present
  • never can not be called

@marsiancba: have you checked if Omit meets your requirements? ahejlsberg's implementation there was a little more complex than yours here, but rather than using never, it'd yield an object literal without the removed properties, which seems to address some of the concerns you're raising here.

But yeah, you're right, people may well find this thread rather than that one.

@tycho01: I have checked Omit implementation and with a small addition it is exactly what I needed - thank you ;-)

For those who also need it: Here is a correct implementation of Minus for objects that works in current TS and does not have above mentioned limitations:

type Diff<T extends string, U extends string> = ({[P in T]: P } & {[P in U]: never } & { [x: string]: never })[T];
type Minus<T, U> = {[P in Diff<keyof T, keyof U>]: T[P]};

For object types A-B=Minus<A, B>.

This seems to be a solution to example use-case mentioned at the beginning of this thread.

BTW: do you know some real-life use-case that needs minus on something other than object?

@marsiancba: mostly subtracting from unions for types other than string literals. but yeah, objects should definitely cover the bulk of use-cases.

@marsiancba
We are talking about the feature that none of the existing languages have (or atleast those languages I am familiar with). Just to have union and intersection types is luxury (again the programming languages I know, have not this feature or it's limited), but if we would be able to push it further, it would be great.

As I've mentioned these constructions are not available for the majority of programmers, so to find an real-life use-case could be difficult (because we are not used to think with those features, like our brain has been trained to solve the problems very differently). But the time would show up some use-cases where it would really save the time and lines of code. More the people would know about the feature the bigger the chance is that they would encounter a problem which is way more optimal to solve with Minus/Negation.

This is just my opinion.

@Durisvk
In JavaScript there are no types written in the source code, so in certain sense you can have any types (and any type operators) you can imagine in JavaScript - but they are only in the mind of the programmer (or in documentation). Typescript is an enhancement to JavaScript where you can move these types from your mind to source code and get some benefits (like compile time type checking). As there is already a huge amount of existing JavaScript code, can you find some real-life example from some JS project where this is needed?

@marsiancba
Of course thanks for explanation, but I must point out that Typescript is not only an enhancement. It's a formal programming language (it has it's own syntax and semantics [doesn't matter that there are similarities with another language, take e.g. Java and C#] ). I understand that it has no runtime, but that's not a condition to be a programming language. Like if we've been thinking that way then every compiled language would be just an enhancement to assembly or machine code or whatever they compile to.

Follows just my personal opinion based on my personal experience (subjective)
When I work in Javascript (which I do a lot) I think totally different than when I work in TypeScript. I try to implement reusable code with different approaches. Therefore I solve problems (in my head) not thinking about the stuff like: What type should this be... Should it be a union/intersection of two or more types?.

One example that comes to my mind is this
but I have to remind you the stuff I've mentioned in the earlier comments:
let's say we have fully defined a minus operator, then we can define negation as follows:
!A = any - A
Now I would use it like this:

     type StrictlyDefined = !null & !undefined

or equally

      type StrictlyDefined = !( null | undefined )

I'm still kind of leaning towards type negation as a better, more flexible solution to this. Set subtraction is A - B = A & !B, and in particular, we do have precedent in Rust: Foo + !Bar, which does the exact same thing I'm proposing there.

@marsiancba Type negation would make it much easier to correctly define types like Promises, where you literally cannot nest resolved promises. It's also easier to implement, since it's as simple as "not this" to check, and type subtraction would likely be implemented as a desugared variant. Also, most of the time, you are probably thinking "I want this, but not that", and that reads a lot like "I want this and not that", so it fits a little more closely with your mental model.

It should be noted that negation and subtraction are equally expressive, and neither is "more flexible" than the other. It is simply a matter of which syntax you prefer. I like negation a little better because it is less awkward to write !T than {} - T.

@masaeedu it's worse than that, because {} isn't a top type. In reality it would take

({} | undefined | null) - T

to express !T if subtraction was the primitive.

@pelotom If strictNullChecks is disabled, {} includes undefined and null. If strictNullChecks is enabled, T doesn't include undefined and null (unless you explicitly add it). So in both cases, from what I understand, {} - T is equivalent to !T.

To put it another way, you'd only ever need ({} | undefined | null) if you had (T | undefined | null). It's always symmetric.

@masaeedu I think it's best to assume we're always talking about a world in which all strict flags are enabled unless explicitly stated otherwise.

If strictNullChecks is enabled, T doesn't include undefined and null (unless you explicitly add it). So in both cases, from what I understand, {} - T is equivalent to !T.

We're talking about an arbitrary type T, which could be instantiated as null or undefined. {} - T does not equal !T if T is null or undefined.

@pelotom It seems like this is actually a point in the subtraction syntax's favor. When you're working with strictNullChecks enabled, you're generally interested in dealing with a domain of types that doesn't include null or undefined at all. With the kind of negation you're suggesting you'll constantly end up introducing nulls and undefineds whenever you invert a type.

With {} - T, you can only ever end up with a type that doesn't include null | undefined, regardless of whether T contains it.

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

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.

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.

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

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

@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?

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

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

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

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

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

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.

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

npm install typescript@next

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

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

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.

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

@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

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

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

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)

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

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.