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;
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:
&
|
-
so that number & boolean | string - string
, means ((number & boolean) | string) - string
type Minus<A, B> = A - B; // yet to be calculated
-
operation should result to an type errornever
and any
should be specially handledtype 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
-
operation that produces the type of the resulting property of the same name -
on 2 properties of the same name gives {}
, the property gets dropped from the resulting typetype 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
-
operator-
on 2 parameters gives {}
the resulting parameter is {}
{}
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
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
ts
class MyElement {
private boundResult = ...
public getBoundingClientRect() {
if(boundResult) return boundResult
...
boundResult = ...
return ...
}
}```
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
Please see https://github.com/Microsoft/TypeScript/wiki/FAQ#what-do-the-labels-on-these-issues-mean for label description.
@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
https://github.com/Microsoft/TypeScript/pull/9407#issuecomment-232047639
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 {}
``` .ts
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:
``` .ts
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
https://github.com/Microsoft/TypeScript/issues/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 https://github.com/Microsoft/TypeScript/issues/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:
T | zero == T
T & zero == zero
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:
https://github.com/Microsoft/TypeScript/issues/15480#issuecomment-324270056
@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 https://github.com/Microsoft/TypeScript/pull/17961/commits/4d14c9fb1da5cf6f5db024e8b25c87336d834103.
@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 https://github.com/Microsoft/TypeScript/issues/7599#issuecomment-329972756 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 (https://github.com/Microsoft/TypeScript/issues/15794#issuecomment-301170109).
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:
void
, but void
is assignable ~from null
~ and undefined
const x: void = undefined;
void
doesn't need to have a return
statement in itfunction doThings(): void { }
void
is assignable by a function returning any typeconst 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: https://github.com/Microsoft/TypeScript/issues/4183#issuecomment-320735727
@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:
!
(called divergent)Nothing
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:
However it has some limitations:
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)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:
never
properties:@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/presentnever
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.
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
withstrictNullChecks
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 https://github.com/Microsoft/TypeScript/pull/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
@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.
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
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
type because it is trivially written as Pick >.
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.
Most helpful comment
What's the status of this suggestion/proposal?