Repository navigation
Subtraction types #4183
Description
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:
- intersection
& - union
| - 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 calculatedPrimitives
- if the left type (minued) isn't a sub-type of the right type (subtrahend) the
-operation should result to an type error neverandanyshould 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 ~ stringProducts
- 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 }; // errorFunctions (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); // errorOverloads
- to be continued...
Activity
RyanCavanaugh commentedon Aug 6, 2015
What about something like this?
zpdDG4gta8XKpMCd commentedon Aug 6, 2015
hm, didn't know it works this way, could be useful for anything that doesn't return
void, which is better than nothingdanquirk commentedon Aug 6, 2015
Alternatively, wouldn't you have the object type itself guard against this type of re-initialization? ie
zpdDG4gta8XKpMCd commentedon Aug 6, 2015
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)
stepancar commentedon Apr 26, 2016
Aleksey-Bykov , Ryan Cavanaugh (@RyanCavanaugh).
Whats status of this issue?
zpdDG4gta8XKpMCd commentedon Apr 26, 2016
open, needs a proposal, considered as a suggestion
mhegazy commentedon Apr 26, 2016
Please see https://github.com/Microsoft/TypeScript/wiki/FAQ#what-do-the-labels-on-these-issues-mean for label description.
stepancar commentedon Apr 26, 2016
Mohamed Hegazy (@mhegazy), sorry, thank you!
rederteph commentedon Apr 26, 2016
It seems like very useful functionality.
Ciantic commentedon Jun 26, 2016
I think there should be more set operations over the fields, like subtraction there could be intersection of fields:
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 commentedon Jul 12, 2016
numbers without NaN would be another interesting case for subtraction types:
type AlwaysANumber = number - NaN#9407 (comment)
bcherny commentedon Jul 30, 2016
Aleksey-Bykov Aren't
NumberandNaNthe same type, but different domain of values? How would this work?tejacques commentedon Aug 6, 2016
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 orany | ~A, currentlyA | anyreduces toanywhich would have to changeWith unions:
A | ~Aexposes the properties in{}With intersections:
A & ~Aexposes the properties inA & any(currentlyA & anyreduces to justany, which would have to change)A question is whether
A & B | ~Ais equivalent toB | ~AFor 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:
The major issue here really seems to be that order matters now when NOT types are in play.
A | ~Ais 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 explicitA - B, and not allow the unary type-Aat all.53 remaining items
tejacques commentedon Feb 10, 2018
Awesome news! Can't wait to play around with this.
zpdDG4gta8XKpMCd commentedon Feb 10, 2018
closing since the major part of the issue is covered by conditional types, the rest is too vague and mostly irrelevant
SalathielGenese commentedon Mar 9, 2018
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 commentedon Mar 9, 2018
Salathiel Genese (@SalathielGenese) This is shipping as part of 2.8. https://github.com/Microsoft/TypeScript/wiki/Roadmap
SalathielGenese commentedon Mar 9, 2018
Much thanks Boris Cherny (@bcherny)
[UPDATE]
I've just moved to
typescript@nextand tried to type my decorator instance side usingExclude<{}, ConstructorLike>(see comment) but still, it is not working.Seem like there no way by which I can tell TS that an object (
Objector{}) won't accept constructor ({new(...)})mbrowne commentedon 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:
pelotom commentedon Apr 19, 2018
Excludeis 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:is just
string.KiaraGrouwstra commentedon Apr 19, 2018
Tom Crockett (@pelotom) maybe something in this direction could work (with conditional types first checking for
'foo'then forstring), I dunno.pelotom commentedon 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:
Ciantic commentedon Apr 21, 2018
I don't understand how
handleNotFoo('foo' as string); // oops, that was allowedcould 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 forbiddenzpdDG4gta8XKpMCd commentedon Apr 21, 2018
it doesn't have to be forced though with the same effect
or
zpdDG4gta8XKpMCd commentedon Apr 21, 2018
downcastingupcasting 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 commentedon Apr 21, 2018
stringshould not be assignable toNot<'foo'>, because it is possibly'foo'. Otherwise you are implicitly downcasting.KiaraGrouwstra commentedon Apr 22, 2018
To forbid
stringon this specific example (untested):If you wanna generalize to automate that
stringpart, you can have something like this as a helper:On auto-widening being evil, #17785.