Negated types #29317
Negated types #29317
Conversation
@@ -46,7 +46,6 @@ tests/cases/conformance/types/keyof/keyofAndIndexedAccessErrors.ts(87,5): error | |||
tests/cases/conformance/types/keyof/keyofAndIndexedAccessErrors.ts(103,9): error TS2322: Type 'Extract<keyof T, string>' is not assignable to type 'K'. | |||
Type 'string & keyof T' is not assignable to type 'K'. | |||
Type 'string' is not assignable to type 'K'. | |||
Type 'string' is not assignable to type 'K'. |
DanielRosenwasser
Jan 9, 2019
Member
I'm not complaining, but why'd this go away?
I'm not complaining, but why'd this go away?
weswigham
Jan 9, 2019
Author
Member
I accidentally fixed a bug where we duplicated the elaboration - this is as the same as the line above.
I accidentally fixed a bug where we duplicated the elaboration - this is as the same as the line above.
First take, without having tried this out:
I do have some reservations on the feature for these reasons. If you think about our features trying to satisfy convenience, intent, and safety, then I don't know if this appropriately weighs convenience and intent. |
It works just dandy~ |
What do you mean? Yesterday you mentioned that you couldn't assign an array to a |
Nope, you can't, because you can trivially make a interface PromiseLikeArray extends Array<any> implements PromiseLike<any> { /*...*/ } so, if you go to the example, I just state that I explicitly return an array that isn't PromiseLike - that is |
Per offline feedback from @ahejlsberg I've changed from |
Can this be used as a way to restrict the potential type of unbounded types? For example, This doesn't look very useful at first glance but it can be powerful on mapped types. For example, |
Yep. That's a primary driver for 'em. |
type Exact<T extends object> = T & Partial<Record<not keyof T, never>> |
Is there an outline of how function foo(x: unknown) {
if (typeof x === "number") {
const num: number = x;
} else {
const numNot: not number = x;
}
}
Is there a short example that demonstrates wanting a @DanielRosenwasser Do you mean something like Meta question for @DanielRosenwasser: You say:
that seem like some internal principles the TS team have for designing features? Is there a public description of these? I think it would help feature proposals if external contributors could frame their suggestions with the same language used internally. @Kovensky I'm not sure that will type-check. The type type Exact<T extends object> = T & Partial<Record<(not keyof T) & string, never>> though I'm not sure what the semantics will be for |
In this PR no negated types are produced by control flow yet; however we've talked over it and negated types make the lib type facts PR elegant to implement, since we can skip using conditionals (which don't compose well) and just filter with intersections of negated types :D
Aye, a test case with an example I pulled from a related issue: // from https://github.com/Microsoft/TypeScript/issues/4183
type Distinct<A, B> = (A | B) & not (A & B);
declare var o1: {x};
declare var o2: {y};
declare function f1(x: Distinct<typeof o1, typeof o2>): void;
f1({x: 0}); // OK
f1({y: 0}); // OK
f1({x: 0, y: 0}); // Should error
Right now they're quietly dropped (aside from filtering out mismatching concrete types), like |
@weswigham Thanks! And this PR is very cool :) Re: the object literal example. Should that not be a case where EPC raises an error? I know it doesn't right now because there is no discriminant, but it probably should. Will using negation types become the canonical way of dealing with examples like this? If not, and assuming EPC does get fixed, are there many other use-cases for the special object literal relation. |
Even if excess property checking makes defining a type like |
Good job so far, I delayed some projects to wait for this feature - for more than a year. |
Do these identities hold? Just wondering what it would take to fix #28131 |
Yes. More generally, when U extends T,
Yes.
More generally when U extends T, |
const result = isRelatedTo(source, (target as NegatedType).type); | ||
return result === Ternary.Maybe ? Ternary.Maybe : result ? Ternary.False : Ternary.True; | ||
} | ||
// Relationship check is S ⊂ T |
DanielRosenwasser
Jan 23, 2019
Member
Do you really need to use this symbol? It almost sounds like a joke, but this could affect memory footprint when bootstrapping the compiler since modern engines can avoid full UTF16 representations https://blog.mozilla.org/javascript/2014/07/21/slimmer-and-faster-javascript-strings-in-firefox/
Do you really need to use this symbol? It almost sounds like a joke, but this could affect memory footprint when bootstrapping the compiler since modern engines can avoid full UTF16 representations https://blog.mozilla.org/javascript/2014/07/21/slimmer-and-faster-javascript-strings-in-firefox/
peter-leonov
Jan 23, 2019
A minification step will simply remove this line. The article suggests to minify the code to utilise the inline strings and also help with strings interning. I guess, v8 should not be so much different in this sense and also win from code minification.
A minification step will simply remove this line. The article suggests to minify the code to utilise the inline strings and also help with strings interning. I guess, v8 should not be so much different in this sense and also win from code minification.
@weswigham Seems to work, right? type UnionToInterWorker<T> = not (T extends T ? not T : never);
type UnionToInter<T> = [T] extends [never] ? never : UnionToInterWorker<T>; Not quite sure what should happen here: type NotInfer<T> = T extends not (infer U) ? U : never;
type NotString = NotInfer<string> // string extends not U ? U : never; Currently it doesn't reduce; maybe infer types should be disallowed under |
It doesn't reduce because we don't reduce reducible conditionals with |
Yep, you're right. I guess it could infer |
@weswigham Is there a way we can test this out on the playground/in our projects, I saw there was a special build for #38305. Could we have the same here to give feedback? |
It may need a rebase for that, but we can see |
Would this enable us to inform consumers of functions that they can't pass an array? Please excuse the contrived example. But it illustrates the signature I'm looking to write. function getObjectKeys<T extends object not Array<unknown>>(input: T) {
return Object.keys(input);
} Because the current need to have to throw at runtime feels like it's against TypeScript's goals of catching bugs at runtime. i.e.
If this change does not plan to solve this use case, please let me know and I can make a new issue. |
function getObjectKeys<T extends object & not Array<unknown>>(input: T) {
return Object.keys(input);
} You forgot an |
I guess the |
@dgreene1 |
I appreciate the suggestion, but that solution does not work for all cases. In fact, it might be a little dangerous because it requires the input type to have an iterable index which might lead developers to add an index signature to their interfaces once they see this error:
So ultimately I think I'll stick to the runtime check until this PR is approved. Thank you @weswigham for creating this PR and thank you @isiahmeadows / @ExE-Boss for confirming the |
I would really love to try this out with TypeScript 4.1 but the conflicts are massive and unmanagable to resolve if you don't know a lot about the internals. Would you be willing to rebase @weswigham? I know this is a lot to ask for. |
A thought on identity rules. I think these should be the proper identities1 Explanation
|
@AmitDigga I would've thought that since |
this will not become a problem, right?
|
1*0=0 and 2*0=0 doesn't make 1=2, maybe this relation also applies to the |
right, totally get that. that's why I was asking (to just make sure) that we think we will not run into an issue with identity related to how |
Its little confusing to me, but let me explain my point We are dealing with set theory. So, lets say if our universal set
Expansion
@isiahmeadows Can you clear confusion over here. I sill think that @dimitropoulos @Jack-Works dividing by 0 analogy wont work over here as dividing by zero gives undefined. In set theory, |
Re @AmitDigga #29317 (comment)
If you're thinking of types as sets where "T ⊆ U" encodes "T is assignable to U" then:
In terms of assignability:
Re "unknown is some subset of universal set, which is not known yet":
|
Here's how those two narrows in practice, if it helps: Playground Link |
It would amazing to have this functionality! Should we expect this PR to be included in some minor release of Typescript 4? Or is that too far fetched? |
Hmm, so the root feature, as described in this PR isn't too contentious (there's some hesitation because the concept may be too complex), but the driver for implementing the feature, using them in control flow to more precisely narrow negative branches on generics, last time I checked (in a different branch based on this PR) had bad performance implications that I couldn't easily work around. Other changes, like caching relationship results on intersection comparisons (we have an open pr for that), or better alias finding (and that) might improve that performance profile. |
I think I've been following this PR in hopes that negated-types gives me an easy way to express index signatures with exceptions, e.g. class Foo {
bar(): boolean;
baz(): number;
[k: string & not ("bar" | "baz")]: string;
}
const f = new Foo();
foo.bar(); // boolean
foo.catz; // string If I'm remembering how I got here correctly, just as a data point, there are a lot more people like me waiting for a simple solution to "indexable classes" (without using intersection types). |
@thw0rted you can do it today using a hack: class Foo {
// @ts-ignore
bar(): void {}
// @ts-ignore
baz(): boolean { return true }
[k: string]: string;
}
const f = new Foo();
f.bar(); // void
f.catz; // string
f.baz(); // boolean |
@Jack-Works that's a more palatable way of keeping a broken declaration inside one class, but it's still broken for interfaces: interface Oops {
[k: string]: string;
// @ts-ignore
bar: number;
// @ts-ignore
baz: boolean;
}
// "Property 'bar' is incompatible with index signature."
const o: Oops = {
bar: 1,
baz: true,
catz: "hey"
}; I haven't had a chance to try this branch yet but I think it would solve both cases, without any messy |
Long have we spoken of them in hushed tones and referenced them in related issues, here they are:
Negated Types
Negated types, as the name may imply, are the negation of another type. Conceptually, this means that if
string
covers all values which are strings at runtime, a "notstring
" covers all values which are... not. We had hoped that conditional types would by and large subsume any use negated types would have... and they mostly do, except in many cases we need to apply the constraint implied by the conditional's check to it's result. In thetrue
branch, we can just intersect theextends
clause type, however in thefalse
branch we've thus far been discarding the information. This means unions may not be filtered as they should (especially when conditionals nest) and information can be lost. So that ended up being the primary driver for this primitive - it's taking what a conditional typefalse
branch implies, and allowing it to stand alone as a type.Syntax
where
T
is another type. I'm open to bikeshedding this, or even shipping without syntax available, but among alternatives (!
,~
)not
reads pretty well.Identities
These are little tricks we do on negated type construction to help speed things along (and give negations on algebraic types canonical forms).
not not T
isT
not (A | B | C | ...)
isnot A & not B & not C & not ...
not (A & B & C & ...)
isnot A | not B | not C | not ...
not unknown
isnever
not never
isunknown
not any
isany
(sinceany
is theNaN
of types and behaves as both the bottom and top)Assignability Rules
Negated types, for perhaps obvious reasons, cannot be related structurally - the only sane way to relate them is in a higher-order fashion. Thus, the rules governing these relations are very important.
not S
is related to a negated typenot T
if T is related to S.This follows from the set membership inversion that a negation implies - if normally a type
S
and a typeT
would be related ifS
is a subset ofT
, when we take the complements of those sets,not S
andnot T
, those sets share an inverse relationship to the originals.S
is related to a negated typenot T
if the intersection ofS
andT
is emptyWe want to check if for all values in S, none of those values are also in T (since if they are, S is not in the negation of T). The intersection of S and T, when simplified and evaluated, is exactly the description of the common domain of the two. If this domain is empty (
never
), then we can conclude that there is no overlap between the two and thatS
must lie withinnot T
.not S
is not related to a typeT
.A negated type describes a set of values that reaches from
unknown
to its bound, while a normal type describes values from its bound tonever
- it's impossible for a negated type to satisfy a normal typeAssignability Addendum for Fresh Object Types
Frequently we want to consider a fresh object type as a singleton type (indeed, some examples in the refs assume this) - it corresponds to one runtime value, not the bounds on a value (meaning, as a type, both its upper and lower bounds are itself). Using this, we can add one more rule that allows fresh literal types to easily satisfy negated object types.
not T
if S is not related to T.Since S is a singleton type, we can assume that so long as it's type is not in
T
, then it is innot T
.Examples
Examples of negated type usage can be found in the tests of this PR (there's a few hundred lines of them, and probably some more to come for good measure), but here's some of the common ones, pulled from the referenced issues:
Fixes #26240.
Ref #4183, #4196, #7648, #12215, #18280Allows #27711 to be cleanly fixed with a lib change (example in the tests).