Repository navigation
Subtraction types #4183
Description
Activity
RyanCavanaugh commented
on Aug 6, 2015 MemberMore actionsWhat 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 commented
on Aug 6, 2015 AuthorMore actionshm, didn't know it works this way, could be useful for anything that doesn't return
void, which is better than nothingAlternatively, 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 commented
on Aug 6, 2015 AuthorMore actions- we are talking about a standard DOM element interface, there is no place to put that safer wrapper you are talking about
- not letting see a method results to statically verified and more correct code as opposed to making assumptions at runtime
- 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)
Reacted by Stepan Mikhailiuk, Maksym and Aluan Haddad- addedSuggestionAn idea for TypeScriptAn idea for TypeScriptNeeds ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.This issue needs a plan that clarifies the finer details of how it could be implemented.
on Aug 21, 2015 Aleksey-Bykov , Ryan Cavanaugh (@RyanCavanaugh).
Whats status of this issue?zpdDG4gta8XKpMCd commented
on Apr 26, 2016 AuthorMore actionsopen, needs a proposal, considered as a suggestion
Reacted by Daniel RosenwasserPlease see https://github.com/Microsoft/TypeScript/wiki/FAQ#what-do-the-labels-on-these-issues-mean for label description.
Mohamed Hegazy (@mhegazy), sorry, thank you!
It seems like very useful functionality.
Reacted by syuilo and Dmitry StatsenkoI 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 commented
on Jul 12, 2016 AuthorMore actionsnumbers without NaN would be another interesting case for subtraction types:
type AlwaysANumber = number - NaNnumbers without NaN would be another interesting case for subtraction types: type AlwaysANumber = number - NaN
Aleksey-Bykov Aren't
NumberandNaNthe 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 orany | ~A, currentlyA | anyreduces toanywhich would have to changeWith unions:
A | ~Aexposes 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 & ~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:
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 | ~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.Reacted by FG, Beeno Tung, Geo, George Thomas and gocs53 remaining items
Awesome news! Can't wait to play around with this.
zpdDG4gta8XKpMCd commented
on Feb 10, 2018 AuthorMore actionsclosing since the major part of the issue is covered by conditional types, the rest is too vague and mostly irrelevant
Reacted by Leon Adler and Alexandre GalaysReacted by Salathiel GeneseReacted by kgtkr and Jens TheisenSalathielGenese commented
on Mar 9, 2018 More actionsI 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.Salathiel Genese (@SalathielGenese) This is shipping as part of 2.8. https://github.com/Microsoft/TypeScript/wiki/Roadmap
npm install typescript@next
Reacted by Salathiel GeneseSalathielGenese commented
on Mar 9, 2018 More actionsMuch 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(...)})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.htmlThis 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>>.
Reacted by Jono Job and Denis ZhbankovExcludeis 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.Reacted by Michael Stillwell, Yoav Karako, Kael, Dmitry Demin, Linus Unnebäck, Matt Greer and Mariusz PawelskiKiaraGrouwstra commented
on Apr 19, 2018 ContributorMore actionsTom Crockett (@pelotom) maybe something in this direction could work (with conditional types first checking for
'foo'then forstring), 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
Reacted by Chris FeijooI 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 commented
on Apr 21, 2018 AuthorMore actionsit 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 commented
on Apr 21, 2018 AuthorMore actionsdowncastingupcasting is automatic in TS and it's somewhat convenient but unsafe assumption (compared to F# for example where you have to explicitly state it)stringshould not be assignable toNot<'foo'>, because it is possibly'foo'. Otherwise you are implicitly downcasting.Reacted by Chris Feijoo and Linus UnnebäckKiaraGrouwstra commented
on Apr 22, 2018 ContributorMore actionsTo forbid
stringon 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
stringpart, you can have something like this as a helper:type Widen<T> = T extends boolean ? boolean : T extends number ? number : T extends string ? string : T;`On auto-widening being evil, #17785.
- locked and limited conversation to collaborators
on Jul 31, 2018
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.getBoundingClientRectto 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 toelement.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.Proposal
add a type operator that produces a new type out of 2 given types according to the rules that follow:
This feature would require a new negated type like
number ~ stringwhich is anumberthat cannot takenumber & string.As far as the precedence of new type operator, it should go:
&|-so that
number & boolean | string - string, means((number & boolean) | string) - stringGenerics
Primitives
-operation should result to an type errorneverandanyshould be specially handledProducts
-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 typeFunctions (2 certain signatures)
-operator-on 2 parameters gives{}the resulting parameter is{}{}the resulting type is{}Overloads