Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Negated types #4196

Open
zpdDG4gta8XKpMCd opened this issue Aug 6, 2015 · 54 comments
Open

Negated types #4196

zpdDG4gta8XKpMCd opened this issue Aug 6, 2015 · 54 comments
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@zpdDG4gta8XKpMCd
Copy link

Sometimes it is desired to forbid certain types from being considered.

Example: JSON.stringify(function() {}); doesn't make sense and the chance is high it's written like this by mistake. With negated types we could eliminate a chance of it.

type Serializable = any ~ Function; // or simply ~ Function
declare interface JSON {
   stringify(value: Serializable): string;
}

Another example

export NonIdentifierExpression = ts.Expression ~ ts.Identifier
@wclr
Copy link

wclr commented Sep 5, 2016

It is interesting, it is possible to achieve somehow?

@siegebell
Copy link

siegebell commented Nov 11, 2016

Edit: this comment probably belongs in #7993 instead.

@Aleksey-Bykov This would allow unions with a catch-all member, without overshadowing the types of the known members.

interface A { type: "a", data: number }
interface B { type: "b", data: string }
interface Unknown { type: string ~"a"|"b", data: any }
type ABU = A | B | Unknown

var x : ABU = {type: "a", data: 5}
if(x.type === "a") {
  let y = x.data; // y should be inferred to be a number instead of any
} 

@SalathielGenese
Copy link

SalathielGenese commented Mar 9, 2018

Following @mhegazy request at #18280, I copy-paste this suggestion here...


I upvote A & !B especially the !B part... Over Exclude from #21847

@jack-williams
Copy link
Collaborator

Do negated types rely on completeness for type-checking?

@zpdDG4gta8XKpMCd
Copy link
Author

zpdDG4gta8XKpMCd commented Mar 9, 2018

I think you should never put a question of what exactly any - MyClass is. I think negated types should be evaluated loosely lazily and only when it comes to typechecks against certain types.

@jack-williams
Copy link
Collaborator

I agree. Is that not sort of like many types now, e.g. number. You never consider how to construct number because it's infinite: only test that a value belongs to it when you need it. What would be the (lazy) procedure for checking that T belongs to A - B, or !B?

@SalathielGenese
Copy link

// exclude all match of T from U
U & !T

// extract all match of T within U
T & U

// type to all but T
!T

What would be the (lazy) procedure for checking that T belongs to A - B, or !B?

T extends (A & !B)
// or
T extends !B

@zpdDG4gta8XKpMCd
Copy link
Author

zpdDG4gta8XKpMCd commented Mar 9, 2018

not until all type parameters (A, B and T in your example) are resolved to concrete types (string, MyClass, null) can you tell what A - B is

@zpdDG4gta8XKpMCd
Copy link
Author

zpdDG4gta8XKpMCd commented Mar 9, 2018

so the procedure would be:

  1. keep type expressions unevaluated until all type parameters are resolved
  2. once all type parameters are known, replace them with the concrete types and see if the expression makes any sense

@jack-williams
Copy link
Collaborator

jack-williams commented Mar 9, 2018

Understood! I guess my question is when you have some concrete types, say A, B, C, and you want to know if A is assignable to B - C, is it something like.

  • if A is assignable to B
  • and A is not assignable to C
  • then A is assignable to B - C.

For A assignable to !C it would just be is A not assignable to C. (Thanks @SalathielGenese)

Sorry if that's not clear!

@SalathielGenese
Copy link

For A assignable to !C it would just be is A not assignable to B.

I think you meant A not assignable to C

@zpdDG4gta8XKpMCd
Copy link
Author

zpdDG4gta8XKpMCd commented Mar 9, 2018

not sure if you can apply sort of a type algebra here, because it's unclear how assignability relates to negation

what you can do is to build a concrete type out of B - C and name it D (provided both B and C are known) and then ask a question whether or not A is assignable to the concrete type D

my naive 5 cents

question still stands what to do when B is too broad like any

@SalathielGenese
Copy link

I think a cleaner way to see B - C would be much like a type constraint rather a type by essence.

@SalathielGenese
Copy link

If by some logic it can be resolved to a type, that would be great, otherwise, it is just a type constraint

@zheeeng
Copy link

zheeeng commented Apr 11, 2018

Expected progress on negating operate.
We already have Exclude<T, U> it is awesome that if the second type U is optional. We can easy implements Not<T> to exclude T from all types.
I also upvote using ~ T or unlike T to constraints types.

@the1mills
Copy link

export type NotUndefined = !undefined;

would be extremely useful IMO

@RyanCavanaugh RyanCavanaugh added the Declined The issue was declined as something which matches the TypeScript vision label Aug 15, 2018
@RyanCavanaugh
Copy link
Member

Exclude gives the possibility to remove something from a union, and conditional types do some other good stuff.

For cases of actual subtype exclusion, the possibility of aliasing makes this idea sort of bonkers. "Animal but not Dog" doesn't make sense when you can alias a Dog via an Animal reference and no one can tell.

Anyway here's something that kinda works!

type Animal = { move: string; };
type Dog = Animal & { woof: string };

type ButNot<T, U> = T & { [K in Exclude<keyof U, keyof T>]?: never };

function getPet(allergic: ButNot<Animal, Dog>) { }

declare const a: Animal;
declare const d: Dog;
getPet(a); // OK
getPet(d); // Error

@jpike88
Copy link

jpike88 commented Aug 28, 2018

Shouldn't that ButNot example be included in TypeScript, simply with a check that prevents people from committing the aliasing mistake you described?

@RyanCavanaugh
Copy link
Member

simply with a check that prevents people from committing the aliasing mistake you described?

What mistake?

@jpike88
Copy link

jpike88 commented Aug 29, 2018

If 'Animal but not Dog' doesn't make sense, that is something TS can be aware of and disallow. But including something like ButNot into TS syntax I think is a good idea

@ORESoftware
Copy link

I might be having a brainfart but how does typeof (Animal && !Dog) not make sense?

@jpike88
Copy link

jpike88 commented Aug 29, 2018

If Dog = Animal & { woof:string } then Animal && !Dog would be equivalent to Animal & !(Animal & { woof:string }), which would always evaluate to false.

But @RyanCavanaugh, if certain combinations are logically problematic, does TS not have the ability to know this and just throw an error on parse?

@jack-williams
Copy link
Collaborator

If Dog = Animal & { woof:string } then Animal && !Dog would be equivalent to Animal & !(Animal & { woof:string }), which would always evaluate to false.

Can you not do this:

  • Animal && !Dog = Animal & !(Animal & { woof:string })
  • Animal & !(Animal & { woof:string }) = Animal & (!Animal | !{woof: string}) by DeMorgan
  • Animal & (!Animal | !{woof: string}) = (Animal & !Animal) | (Animal & !{woof: string}) by union distribution
  • (Animal & !Animal) | (Animal & !{woof: string}) = never | (Animal & !{woof: string}) by contradiction
  • never | (Animal & !{woof: string}) = Animal & !{woof: string} by lattice minimum

That seems a reasonable type to me: anything that is an animal, but not with a woof field of type string.

@RyanCavanaugh
Copy link
Member

Apologies, this one was mis-tagged. Negated types are still "on the table" so to speak.

@RyanCavanaugh RyanCavanaugh reopened this Jun 21, 2023
@dead-claudia
Copy link

@RyanCavanaugh Might be worth creating a label to force-keep issues like this open.

@jakebailey
Copy link
Member

Note the label change above; we have a set of labels which imply an eventual close (but were inconstantly done automatically) and this issue no longer has one.

@mitchell-merry
Copy link

mitchell-merry commented Jul 11, 2023

I'd like to throw my use-case into the mix (playground):

type MyType = string | 'some-specific-string';

type IsSpecific<T extends MyType> = T extends 'some-specific-string' ? true : false;
type ShouldBeTrue = IsSpecific<'some-specific-string'>;
//   ^?
type ShouldBeFalse = IsSpecific<'unspecific'>;
//   ^?

type MyMappedType = {
  [K in MyType]: IsSpecific<K>
};

const current: MyMappedType = {
  'unspecific': false, // OK
  'some-specific-string': false, // should error
};

Here, I want to allow arbitrary keys into a dictionary, but if a key matches a particular string sub-type then the value should be different. This doesn't work, since some-specific-string gets resolved to the type string and therefore has a value of false (or something like this).

I think if the not operator was a thing, then the following would do what I need:

type MyType = (string & not 'some-specific-string') | 'some-specific-string';

This way, MyType still matches any string, but it would prevent the arbitrary string type from swallowing some-specific-string. I'm not sure if there's a way to solve this in TypeScript as it stands.

+1 to this feature request.

Edit: The type as written might just get resolved back to string. I'm sure some variant of this would solve the problem, though... or maybe this specific problem is more closely related to another

@BetaZhang
Copy link

BetaZhang commented Aug 8, 2023

type Falsy = false | 0 | 0n | "" | null | undefined | Document["all"] | NaN;
type Truthy = !Falsy;

@ljharb
Copy link
Contributor

ljharb commented Aug 8, 2023

you forgot 0n and NaN and document.all :-)

@thw0rted
Copy link

If you, like any reasonable person, said "WTF" out loud on reading the above comment, feel free to marvel at the bad decisions we sometimes make in pursuit of backward-compatibility.

@BetaZhang
Copy link

BetaZhang commented Aug 14, 2023

you forgot 0n and NaN and document.all :-)

Thanks for your correction. Is this definition correct now?(PS: If Typescript has NaN type).

@yamiteru
Copy link

yamiteru commented Sep 9, 2023

I'd like to add my use-case. I'm working on a data validation library and I want to have an operation not which takes as a parameter another operation (for example literal(null)) and it should return a type that's anything except the output type of the input operation so in this case something like !null.

The types can also be custom so I cannot use Exclude in this case because I don't know all of the custom types beforehand so I cannot create a union of all those possible types.

@MKRhere
Copy link

MKRhere commented Dec 30, 2023

Telegram Bot API recently added type MaybeAccessibleMessage = Message | InaccessibleMessage, of which the specified way to check is message.date === 0 (InaccessibleMessage).

As the maintainers of very popular Bot API libs for TypeScript, @telegraf and @grammyjs, we take effort to model these types for TypeScript as accurately as possible. Negated types would be super useful in this among many other usecases (here to define Message["date"] as number & not 0) so you can discriminate based on it.

I just hope we can restart a conversation on this long pending issue. Are there obvious design reasons this is not viable today?

@matthew-dean
Copy link

I would love a negated type, for the purposes of discriminating between a string literal and general string, like:

interface SpecificNodeType {
  type: 'specific'
  value: SpecificNode
}
interface GeneralNodeType {
  type: Exclude<string, 'specific'>
  value: GeneralNode
}

export type Node = SpecificNodeType | GeneralNodeType

For a function or class creating a Node, a type of 'specific' would expect / type a value of value to SpecificNode (and all of its properties and methods etc. Anything else would expect a value of GeneralNode.

@jorenbroekema
Copy link

I'd love negated types for the following use case:
https://www.typescriptlang.org/play/?#code/LAKFFMA8AcHsCcAuACAlgO0eeAzAhgMbjIDyARgFbIDeoyyOssA-AFzIDOi8GA5gNx1kAbQDW4AJ7suPdLwC67chUEgAvqCA

export interface Obj {
  foo?: string; // this errors, because Property 'foo' of type 'string | undefined' is not assignable to 'string' index type 'Obj'
  [key: string]: Obj;
}

I'm solving it now by doing this:

export interface Obj {
  foo?: string;
  [key: string]: Obj | string | undefined;
}

But that's a little bit weird because I know that when the key is "foo", it's string | undefined and when the key is NOT "foo", it means it is a nested version of itself.

So preferably I would do this:

export interface Obj {
  foo?: string;
  [key: string & not keyof Obj]: Obj;
}

But I'm not really sure if that use case would work because technically [key: string & not keyof Obj]: Obj; is a part of keyof Obj?

@kaleidawave
Copy link

I don't think this would be too hard to implement. T being a subtype of 'not U' is equivalent to T being disjoint from U (aka no common members). This disjoint logic is already done for the checking of JS equality, so could probably could be reused in the subtyping logic.

This would also solve some of the problems with Exclude. Exclude normally works using the extends distribution mechanism, but this only happens for union types and doesn't distribute across all floating point types. With p: Not<2> the first two checks fail disjoint checks and therefore wouldn't be allowed to be assigned to the parameter.

function func<const T>(p: Exclude<T, 2>) {}

func(2) // <- errors
func(2 as number) // <- no error (when there should be)
func(6) // <- works as intended
https://www.typescriptlang.org/play/?#code/GYVwdgxgLglg9mABKSAeCCDOVEBUB8AFAA4BciAogB4QA2IAJgKaq4A0iATPgJSIDeAXwBQwlBEKceY8BIBs08ZMQBDTIjAgAtgCMmAJx5A

@onx2
Copy link

onx2 commented Jul 24, 2024

It may have been stated before in another ticket but the use case I have for this feature is containing a string so:

Pseudo code

// Where `T` is a union of string literals
type AnyStringExcept<T> = string & !T;
type NotAllowed = "a" | "b";

AllowedStrings = string | AnyStringExcept<NotAllowed>;

As its been mentioned before, this behavior can exist in a function but not at a "type-only level".

The project specific use-case for me is constraining the keys that are pressed for a keybinding to only valid characters.

// Every possible universal key across all keyboards
export type UniversalKey = 
  // Control Keys
  | "BACKSPACE" | "TAB" | "ENTER" | "ESCAPE" | "DELETE"
  // Modifier Keys
  | "SHIFT" | "CONTROL" | "ALT" | "META"
  // Arrow Keys
  | "ARROWLEFT" | "ARROWUP" | "ARROWRIGHT" | "ARROWDOWN"
  // Number Keys
  | "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
  // Numpad Keys
  | "NUMPAD0" | "NUMPAD1" | "NUMPAD2" | "NUMPAD3" | "NUMPAD4"
  | "NUMPAD5" | "NUMPAD6" | "NUMPAD7" | "NUMPAD8" | "NUMPAD9"
  | "NUMPADMULTIPLY" | "NUMPADADD" | "NUMPADSUBTRACT" | "NUMPADDECIMAL" | "NUMPADDIVIDE"
  // Function Keys
  | "F1" | "F2" | "F3" | "F4" | "F5" | "F6" 
  | "F7" | "F8" | "F9" | "F10" | "F11" | "F12"
  // Navigation Keys
  | "HOME" | "END" | "PAGEUP" | "PAGEDOWN"
  // Other Special Keys
  | "CAPSLOCK" | "INSERT" | "PAUSE" | "PRINTSCREEN" | "SCROLLLOCK";

// a constrained list of universal keys that are acceptable for keybindings
export type ValidUniversalKey = Extract<UniversalKey,
  // Control Keys
  "BACKSPACE" | "TAB" | "ENTER" | "ESCAPE" | "DELETE"
  // Modifier Keys
  | "SHIFT" | "CONTROL" | "ALT" | "META"
  // Arrow Keys
  | "ARROWLEFT" | "ARROWUP" | "ARROWRIGHT" | "ARROWDOWN"
  // Number Keys
  | "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
  // Numpad Keys
  | "NUMPAD0" | "NUMPAD1" | "NUMPAD2" | "NUMPAD3" | "NUMPAD4"
  | "NUMPAD5" | "NUMPAD6" | "NUMPAD7" | "NUMPAD8" | "NUMPAD9">;

export type InvalidUniversalKey = Exclude<UniversalKey, ValidUniversalKey>;

export type ValidShortcutKey = (ValidUniversalKey | ValidUniqueKey) & AnyStringExcept<InvalidUniversalKey>;

Having this would prevent me from loosening the type def or defining a massive character list (string literals).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests