Skip to content

Negated types - #29317

Closed
Wesley Wigham (weswigham) wants to merge 2 commits into
microsoft:mainfrom
weswigham:negated-types
Closed

Negated types#29317
Wesley Wigham (weswigham) wants to merge 2 commits into
microsoft:mainfrom
weswigham:negated-types

Conversation

@weswigham

@weswigham Wesley Wigham (weswigham) commented Jan 9, 2019

Copy link
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'.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not complaining, but why'd this go away?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I accidentally fixed a bug where we duplicated the elaboration - this is as the same as the line above.

@DanielRosenwasser

Copy link
Copy Markdown
Member

First take, without having tried this out:

  • Intersections appear to be the correct mechanism to check for potential overlap, but that also means almost no object type can satisfy a negated object type. If we add negated types, I would prefer not to give the check up at all though.
  • I find it weird, but understandable, that you treat fresh object types differently. But it's clearly weird that these are the only exception to the above issue.

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.

@weswigham

Wesley Wigham (weswigham) commented Jan 9, 2019

Copy link
Copy Markdown
Member Author

Intersections appear to be the correct mechanism to check for potential overlap, but that also means almost no object type can satisfy a negated object type. If we add negated types, I would prefer not to give the check up at all though.

not (object & SomeInterface)

It works just dandy~

@DanielRosenwasser

Daniel Rosenwasser (DanielRosenwasser) commented Jan 9, 2019

Copy link
Copy Markdown
Member

It works just dandy~

What do you mean? Yesterday you mentioned that you couldn't assign an array to a not PromiseLike<any>.

@weswigham

Wesley Wigham (weswigham) commented Jan 9, 2019

Copy link
Copy Markdown
Member Author

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 T[] & not PromiseLike<any>

@weswigham

Wesley Wigham (weswigham) commented Jan 9, 2019

Copy link
Copy Markdown
Member Author

Per offline feedback from Anders Hejlsberg (@ahejlsberg) I've changed from ~ unary operator to a not keyword type operator (and updated all the text in this PR thus far to match). It does read nicer.

@Jessidhia

Copy link
Copy Markdown

Can this be used as a way to restrict the potential type of unbounded types? For example, number & not 0 to allow any number except the literal 0.

This doesn't look very useful at first glance but it can be powerful on mapped types. For example, [key in string and not keyof CSS.Properties<any>] in https://github.com/DefinitelyTyped/DefinitelyTyped/blob/e836acc75a78cf0655b5dfdbe81d69fdd4d8a252/types/styled-components/index.d.ts#L16-L25 could allow the constrained index signature to not have to include CSS.Properties<string | number>[keyof CSS.Properties<string | number>].

@weswigham

Copy link
Copy Markdown
Member Author

Can this be used as a way to restrict the potential type of unbounded types? For example, number & not 0 to allow any number except the literal 0.

Yep. That's a primary driver for 'em.

@Jessidhia

Copy link
Copy Markdown
type Exact<T extends object> = T & Partial<Record<not keyof T, never>>

🤔

@jack-williams

Jack Williams (jack-williams) commented Jan 10, 2019

Copy link
Copy Markdown
Collaborator

Is there an outline of how not interacts with narrowing? Might we have something like?

function foo(x: unknown) {
  if (typeof x === "number") {
    const num: number = x;
  } else {
    const numNot: not number = x;
  }
}

Assignability Addendum for Fresh Object Types

Is there a short example that demonstrates wanting a not for an object literal?


Daniel Rosenwasser (@DanielRosenwasser) Do you mean something like { x: number } is not assignable to not { x: boolean } because { x : number} & { x: boolean } does not get reduced to never.

Meta question for Daniel Rosenwasser (@DanielRosenwasser): You say:

convenience, intent, and safety

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 not keyof T includes any type which isn't one of the keys, such as boolean. You might need:

type Exact<T extends object> = T & Partial<Record<(not keyof T) & string, never>>

though I'm not sure what the semantics will be for not types appearing in mapped type constraints.

@weswigham

Wesley Wigham (weswigham) commented Jan 10, 2019

Copy link
Copy Markdown
Member Author

Is there an outline of how not interacts with narrowing? Might we have something like?

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

Is there a short example that demonstrates wanting a not for an object literal?

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

though I'm not sure what the semantics will be for not types appearing in mapped type constraints.

Right now they're quietly dropped (aside from filtering out mismatching concrete types), like symbol for the same reason. There's no non-generic concept to map them into. I have a test to that effect. The arbitrary index signatures PR fixes this and would allow an index signature of a, say, string & not "". With that in place mapped types work correctly with 'em, since the intersections trivially desugar to an index signature.

@jack-williams

Copy link
Copy Markdown
Collaborator

Wesley Wigham (@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.

@weswigham

Wesley Wigham (weswigham) commented Jan 10, 2019

Copy link
Copy Markdown
Member Author

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 Distinct unneeded, you'd still need the relationship to allow a type like Distinct to be satisfiable (with fresh object types) should one be used.

@SalathielGenese

Salathiel Genese (SalathielGenese) commented Jan 15, 2019

Copy link
Copy Markdown

Good job so far, I delayed some projects to wait for this feature - for more than a year.

@jack-williams

Copy link
Copy Markdown
Collaborator

Do these identities hold?
T & (not T) is never.
T & (not U) is T when T and U are disjoint.
"2b" | (not "2b") is unknown, or generally T | (not T) is unknown.

Just wondering what it would take to fix #28131

@weswigham

Copy link
Copy Markdown
Member Author

T & (not T) is never.

Yes. More generally, when U extends T, U & not T is never.

T & (not U) is T when T and U are disjoint.

Yes.

"2b" | (not "2b") is unknown, or generally T | (not T) is unknown

More generally when U extends T, T | not U is just unknown (quite literally the opposite of the intersection into never rule). That's correct though I don't remember if I'd actually implemented this one yet - afaik we don't currently have any union identities that cause unions to "simplify" to unknown yet so I remember thinking about how it's best accomplished for a bit.

Comment thread src/compiler/checker.ts Outdated
const result = isRelatedTo(source, (target as NegatedType).type);
return result === Ternary.Maybe ? Ternary.Maybe : result ? Ternary.False : Ternary.True;
}
// Relationship check is S ⊂ T

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@jack-williams

Jack Williams (jack-williams) commented Jan 24, 2019

Copy link
Copy Markdown
Collaborator

Wesley Wigham (@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 not?

@weswigham

Wesley Wigham (weswigham) commented Jan 24, 2019

Copy link
Copy Markdown
Member Author

It doesn't reduce because we don't reduce reducible conditionals with infer's in them right now. IMO, it certainly still makes sense to allow an infer inside a not, since not A extends not infer B ofc should infer A for B.

@jack-williams

Copy link
Copy Markdown
Collaborator

Yep, you're right. I guess it could infer U = not string?

@qwelias

Copy link
Copy Markdown

Love coming back to this PR about once or twice a year because of not being able to do something.
Is there a chance to get any update on the state of it?

@k4b7

Copy link
Copy Markdown

Would support for negated types make type inference involving conditional types easier and/or more powerful?

@sandersn

Copy link
Copy Markdown
Member

This experiment is pretty old, so I'm going to close it to reduce the number of open PRs.

@jsejcksn

Copy link
Copy Markdown

Nathan Shively-Sanders (@sandersn) Is that a "no" to negated types, or will the concept be tracked somewhere else now? If the latter, I'd like to know where so that I can subscribe to the new conversation.

@sandersn

Copy link
Copy Markdown
Member

Not "no", but discussion should happen on an issue, #4196 probably, instead of a PR. PRs go stale.

@weswigham

Wesley Wigham (weswigham) commented May 25, 2022

Copy link
Copy Markdown
Member Author

(Though admittedly the proposal in the PR description is admittedly way more concrete and detailed than anything in that issue rn, and the blockers for this are less about the syntax and behavior and more about implementation and performance + language complexity concerns, all of which are more tied to this PR specifically than the original idea)

@dimitropoulos

Copy link
Copy Markdown
Contributor

while I certainly can relate to wanting to keep the PR count down to a manageable number (we have the same problem over on insomnia), I would like to echo the above from Wesley Wigham (@weswigham). I've been following this conversation very closely for years, since it and #29729 are the two features I am the most interested in.

I guess at the end of the day as long as this PR isn't locked we can all keep talking here, but it'd be fantastic to have a better understanding of what needs to happen next for negated types to rise-the-ranks a little in the prioritization. I don't want to spam iteration plans (e.g. #49074) with my wants-and-needs... but I'm not sure what else to do at this point.

I hit the need for negated types on a weekly basis. Should I just drop those use-cases in here whenever I hit them?

@sandersn

Copy link
Copy Markdown
Member

Dimitri Mitropoulos (@dimitropoulos) #49220 addresses the scenario in #29729 in a different way.

@falfiya

Copy link
Copy Markdown

Thanks for mentioning #49220. I didn't even know I had this problem till I read it. What I'm hearing is that I should follow future discussion of negated types in #4196? They'd be absolutely wonderful to have for stuff like "number without NaN".

@Autumn-one

Copy link
Copy Markdown

Why was such a basic and important feature rejected for a type system? That's a pity!

@MartinJohns

Copy link
Copy Markdown
Contributor

Autumn-one It wasn't rejected, the issue is still open: #4196

As to why it's not implemented yet? Priorities, time constraints, language constraints, limited resources. Feel free to fork the TypeScript repo and try to implement it, you'll see that it's not an easy task.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Author: Team Experiment A fork with an experimental idea which might not make it into master typescript@experimental Pull requests tagged with this label are automatically rebased together with master and published