-
Notifications
You must be signed in to change notification settings - Fork 12.9k
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 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
andany
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...
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 , @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
@mhegazy, sorry, thank you!
rederteph commentedon Apr 26, 2016
It seems like very useful functionality.
strictNullChecks
option #8904Ciantic 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)
100 remaining items
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
@SalathielGenese This is shipping as part of 2.8. https://github.com/Microsoft/TypeScript/wiki/Roadmap
SalathielGenese commentedon Mar 9, 2018
Much thanks @bcherny
[UPDATE]
I've just moved to
typescript@next
and 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 (
Object
or{}
) 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
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:is just
string
.KiaraGrouwstra commentedon Apr 19, 2018
@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 allowed
could be checked? If one forcibly casts a value to certain type, it would loose the information that it is"foo"
.However, for someone who always seems to be learning about new type features in TypeScript, it totally amazes me it can even be made to work normally:
handleNotFoo('foo'); // correctly forbidden
zpdDG4gta8XKpMCd 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
string
should not be assignable toNot<'foo'>
, because it is possibly'foo'
. Otherwise you are implicitly downcasting.KiaraGrouwstra commentedon Apr 22, 2018
To forbid
string
on this specific example (untested):If you wanna generalize to automate that
string
part, you can have something like this as a helper:On auto-widening being evil, #17785.