-
Notifications
You must be signed in to change notification settings - Fork 12.8k
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
Comments
It is interesting, it is possible to achieve somehow? |
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
} |
Do negated types rely on completeness for type-checking? |
I think you should never put a question of what exactly |
I agree. Is that not sort of like many types now, e.g. |
// exclude all match of T from U
U & !T
// extract all match of T within U
T & U
// type to all but T
!T
T extends (A & !B)
// or
T extends !B |
not until all type parameters ( |
so the procedure would be:
|
Understood! I guess my question is when you have some concrete types, say
For Sorry if that's not clear! |
I think you meant |
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 my naive 5 cents question still stands what to do when B is too broad like |
I think a cleaner way to see |
If by some logic it can be resolved to a type, that would be great, otherwise, it is just a type constraint |
Expected progress on negating operate. |
export type NotUndefined = !undefined; would be extremely useful IMO |
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 |
Shouldn't that ButNot example be included in TypeScript, simply with a check that prevents people from committing the aliasing mistake you described? |
What mistake? |
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 |
I might be having a brainfart but how does |
If But @RyanCavanaugh, if certain combinations are logically problematic, does TS not have the ability to know this and just throw an error on parse? |
Can you not do this:
That seems a reasonable type to me: anything that is an animal, but not with a woof field of type string. |
|
you forgot 0n and NaN and document.all :-) |
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. |
Thanks for your correction. Is this definition correct now?(PS: If Typescript has NaN type). |
I'd like to add my use-case. I'm working on a data validation library and I want to have an operation The types can also be custom so I cannot use |
Telegram Bot API recently added 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 I just hope we can restart a conversation on this long pending issue. Are there obvious design reasons this is not viable today? |
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 |
I'd love negated types for the following use case: 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 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 |
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 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 |
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). |
This would be really nice to have because I could use combinators like AND, OR, NOT, for type predicates. /**
* a type predicate AND combinator
*/
export function andTp<A, B extends A, C extends A>(
f: (x: A) => x is B,
g: (x: A) => x is C
): (x: A) => x is B & C {
return (x: A) => f(x) && g(x);
} /**
* a type predicate OR combinator
*/
export function orTp<A, B extends A, C extends A>(
f: (x: A) => x is B,
g: (x: A) => x is C
): (x: A) => x is B | C {
return (x: A) => f(x) || g(x);
} This works great and I'm able to do point-free combinations of type predicates. But I can't see how I can do NEGATION of type predicates without negated types. // I wish I could do this...
export function notTp<A>(
f: (x: A) => x is A,
): (x: A) => x is not A {
return (x: A) => !f(x);
} Then I could nicely compose my type predicates and do things like this array.filter(andTp(isFoo, notTp(isBar))) |
I'm trying to determine the current state of this and related suggestions (going back over 10 years). It seems like it's in popular demand, and the lack of such a feature makes some valid JS unrepresentable (if you want it typed). Is it just not as high a priority as other features (and if so, how are the priorities determined)? Is it a philosophically problematic addition for some reason? Is the implementation considered too hard or risky vs perceived utility? If someone implemented type subtraction or negation (and it didn't break anything!) would that influence the decision to include the feature? Would a PR be welcome (asking for a friend ;) )? |
@adrianstephens My guess is that implementing negated types in a duck-typed language like TypeScript would be quite challenging. Since TypeScript allows structural typing, an object of one type can often be used as another type if they share the same attributes—even if their meanings are entirely different. // A and B are distinct interfaces but have identical attributes
const fn = (a: A, callback: (notA: ~A) => void) => {
const b: B = a; // Structurally identical, so assignment is allowed
callback(b); // This is valid
callback(a); // This is an error
};
// The issue becomes even more problematic when implicit type conversions occur
const fn2 = (b: B, callback: (notA: ~A) => void) => {
callback(b); // Accepted
};
const a: A = getA();
fn2(a, (notA: ~A) => console.log('not A', notA)); // Unexpectedly valid
// Ambiguity arises with inheritance and type intersections
interface C extends A, B {}
// Alternatively, C could be defined as: type C = A & B
function getC(): C { ... }
const myCallback: (notA: ~A) => void = ...
const c1 = getC();
myCallback(c1); // Error
const c2: B = getC();
myCallback(c2); // Allowed These cases could pose challenges for those looking to implement negated types, depending on how strictly they need to be enforced. The validity of ~A depends not only on the structure of A but also on how TypeScript resolves assignments and type relations dynamically. One potential solution would be introducing nominal types (see: #202). If nominal typing were supported, negated types might be more feasible—at least for explicitly nominal types—since they wouldn't rely purely on structural compatibility. |
@lucasbasquerotto Without thinking about it too hard, it seems to me that if you have a function in your transpiler that determines if one type is assignable to another (say), you can just negate the result for negated types. In other words, just think of the type checker as working on abstract things that obey boolean algebra. This makes it quite unambiguous - ~A would match anything that A doesn't by the existing rules. So if an instance of B could be passed to a parameter of type A, then it would not match ~A. I think type subtraction is a bit clearer conceptually, so that's what I tried to implement; but negation would probably've been simpler. |
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.Another example
The text was updated successfully, but these errors were encountered: