microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.02k stars 12.49k forks source link

Negated types #4196

Open zpdDG4gta8XKpMCd opened 9 years ago

zpdDG4gta8XKpMCd commented 9 years ago

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 commented 8 years ago

It is interesting, it is possible to achieve somehow?

siegebell commented 8 years ago

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 commented 6 years ago

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 commented 6 years ago

Do negated types rely on completeness for type-checking?

zpdDG4gta8XKpMCd commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago
// 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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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.

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

Sorry if that's not clear!

SalathielGenese commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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

SalathielGenese commented 6 years ago

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

zheeeng commented 6 years ago

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 commented 6 years ago
export type NotUndefined = !undefined;

would be extremely useful IMO

RyanCavanaugh commented 6 years ago

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 commented 6 years ago

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

RyanCavanaugh commented 6 years ago

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

What mistake?

jpike88 commented 6 years ago

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 commented 6 years ago

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

jpike88 commented 6 years ago

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 commented 6 years ago

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:

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

RyanCavanaugh commented 6 years ago

What you're describing is just the ButNot type above, but with the ? removed

jack-williams commented 6 years ago

I don't know if that reply was for me, but that was the intention of my post. There doesn't seem to be anything 'logically problematic' with Animal && !Dog or ButNot<Animal, Dog>.

jvanbruegge commented 6 years ago

I would suggest a backslash as syntax, as this is also what the set operation looks like. But anyways, this would be nice to have, because AFAICT currently this is not expressable in typescript:

type Config = {
    foo: number;
    bar: number;
    [k in (string \ ("foo" | "bar"))]: string;
};
dead-claudia commented 5 years ago

@RyanCavanaugh Could this be reconsidered, as an alternative to awaited/promised for typing promises? Here's how you could properly type native promises with this (this addresses concerns listed here):

// Note: `!T` means "all types but T"
// - `!unknown` = `never`
// - `!never` = `unknown`
interface PromiseLike<T, E = Error> {
    then(onResolve: (value: T) => any, onReject: (error: E) => any): any;
}

interface PromiseLikeCoerce<T extends !PromiseLike<any>, E = Error>
    extends PromiseLike<PromiseCoercible<T, E>, E> {}
type PromiseCoercible<T extends !PromiseLike<any>, E = Error> =
    T | PromiseLikeCoerce<T, E>;

interface PromiseConstructor {
    resolve<T extends !PromiseLike<any>>(value: PromiseCoercible<T>): Promise<T, never>;
    reject<E = Error>(value: E): Promise<never, E>;
    all<T extends Array<!PromiseLike<any>>, E = Error>(
        values: (
            {[I in keyof T]: PromiseCoercible<T[I]>} |
            Iterable<Await<T[number]>>
        )
    ): Promise<T, E>;
    race<T extends !PromiseLike<any>, E = Error>(
        values: Iterable<PromiseCoercible<T>>
    ): Promise<T, E>;
}

interface Promise<T extends !PromiseLike<any>, E = Error> {
    then(onResolve?: !Function, onReject?: !Function): Promise<T, E>;
    catch(onReject?: !Function): Promise<T, E>;
    then<U, F = E>(
        onResolve: (value: AwaitValue<T>) => AwaitValue<U, F>,
        onReject?: !Function,
    ): Promise<U, E | F>;
    then<U, F = E>(
        onResolve: !Function,
        onReject: (error: E) => AwaitValue<U, F>,
    ): Promise<T | U, F>;
    then<U, F = E>(
        onResolve: (value: AwaitValue<T>) => AwaitValue<U, F>,
        onReject: (error: E) => AwaitValue<U, F>,
    ): Promise<U, F>;
    catch<U, F = E>(
        onReject: (error: E) => AwaitValue<U, F>,
    ): Promise<T | U, F>;
    finally(onSettled: () => PromiseCoercible<any, any>): Promise<T, E>;
}

Note that any here has to be used since TS only has concrete types + any + never.


Technically, you could type it with only conditional types...

...but it'd get very awkward very fast, and it'd be a bit counter-intuitive and a really ugly hack. It also doesn't assert the invariant of promise resolutions never containing promises.

Link to playground%3A%20any%3B%0D%0A%7D%0D%0A%0D%0Atype%20AwaitValue%3CT%2C%20E%20%3D%20Error%3E%20%3D%20%7B%0D%0A%20%20%20%200%3A%20T%3B%0D%0A%20%20%20%201%3A%20T%20extends%20PromiseLike%3Cinfer%20U%2C%20any%3E%20%3F%20AwaitValue%3CU%2C%20E%3E%20%3A%20never%3B%0D%0A%7D%5BT%20extends%20PromiseLike%3Cany%2C%20any%3E%20%3F%201%20%3A%200%5D%3B%0D%0A%0D%0Atype%20AwaitPromise%3CT%2C%20E%20%3D%20Error%3E%20%3D%20%7B%0D%0A%20%20%20%200%3A%20Promise%3CT%2C%20E%3E%3B%0D%0A%20%20%20%201%3A%20T%20extends%20PromiseLike%3Cinfer%20U%2C%20infer%20F%3E%20%3F%20AwaitPromise%3CU%2C%20E%20%7C%20F%3E%20%3A%20never%3B%0D%0A%7D%5BT%20extends%20PromiseLike%3Cany%2C%20any%3E%20%3F%201%20%3A%200%5D%3B%0D%0A%0D%0Ainterface%20PromiseLikeCoerce%3CT%2C%20E%20%3D%20Error%3E%0D%0A%09extends%20PromiseLike%3CPromiseCoercible%3CT%2C%20E%3E%2C%20E%3E%20%7B%7D%0D%0Atype%20PromiseCoercible%3CT%2C%20E%20%3D%20Error%3E%20%3D%20T%20%7C%20PromiseLikeCoerce%3CT%2C%20E%3E%3B%0D%0A%0D%0Ainterface%20PromiseConstructor%20%7B%0D%0A%09resolve%3CT%3E(value%3A%20T)%3A%20AwaitPromise%3CT%3E%3B%0D%0A%09reject%3CE%20%3D%20Error%3E(value%3A%20E)%3A%20Promise%3Cnever%2C%20E%3E%3B%0D%0A%09all%3CT%20extends%20%5Bany%2C%20any%5D%5B%5D%3E(%0D%0A%09%09values%3A%20(%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20Pick%3CT%2C%20Exclude%3Ckeyof%20T%2C%20number%3E%3E%20%26%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%7B%5BI%20in%20number%20%26%20keyof%20T%5D%3A%20PromiseCoercible%3CT%5BI%5D%5B0%5D%2C%20T%5BI%5D%5B1%5D%3E%7D%20%7C%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20Iterable%3CPromiseCoercible%3CT%5Bnumber%5D%3E%3E%0D%0A%20%20%20%20%20%20%20%20)%0D%0A%09)%3A%20Promise%3C%0D%0A%20%20%20%20%20%20%20%20Pick%3CT%2C%20Exclude%3Ckeyof%20T%2C%20number%3E%3E%20%26%0D%0A%20%20%20%20%20%20%20%20%7B%5BI%20in%20number%20%26%20keyof%20T%5D%3A%20AwaitValue%3CT%5BI%5D%5B0%5D%2C%20T%5BI%5D%5B1%5D%3E%7D%2C%0D%0A%20%20%20%20%20%20%20%20T%5Bnumber%5D%5B1%5D%0D%0A%20%20%20%20%3E%3B%0D%0A%09race%3CT%2C%20E%20%3D%20Error%3E(values%3A%20Iterable%3CT%3E)%3A%20AwaitPromise%3CT%2C%20E%3E%3B%0D%0A%7D%0D%0A%0D%0Ainterface%20Promise%3CT%2C%20E%20%3D%20Error%3E%20%7B%0D%0A%09then%3CU%2C%20F%20%3D%20E%3E(%0D%0A%09%09onResolve%3A%20(value%3A%20AwaitValue%3CT%3E)%20%3D%3E%20AwaitValue%3CU%2C%20F%3E%2C%0D%0A%09%09onReject%3A%20(error%3A%20E)%20%3D%3E%20AwaitValue%3CU%2C%20F%3E%2C%0D%0A%09)%3A%20Promise%3CU%2C%20F%3E%3B%0D%0A%09catch%3CU%2C%20F%20%3D%20E%3E(%0D%0A%09%09onReject%3A%20(error%3A%20E)%20%3D%3E%20AwaitValue%3CU%2C%20F%3E%2C%0D%0A%09)%3A%20Promise%3CT%20%7C%20U%2C%20F%3E%3B%0D%0A%09finally(onSettled%3A%20()%20%3D%3E%20AwaitValue%3Cany%3E)%3A%20Promise%3CT%2C%20E%3E%3B%0D%0A%7D)

webhacking commented 5 years ago

Checkout 3.5 https://github.com/Microsoft/TypeScript/issues/30555

dead-claudia commented 5 years ago

Could this be re-opened in light of #29317?

mchccn commented 2 years ago

I want this feature in some way or another, maybe even as a marker type like ThisType.

I have a "language" for matching tree-like structures:

{
    type: "NOT_NUMBER";
    value: not number;
}

In this language of mine, I support not, which matches all types but its operand. So not string and not number would disallow strings and numbers.

I've got most of this excellently (over engineered to hell) typed:

fantasyLibrary.match(compiledExpression, myTree, (match) => {
    // match is of type { type: "NOT_NUMBER"; value: unknown; }
});

The match parameter is as close as possible to what the original source expression describes. However, because TypeScript doesn't have any way of representing the negation of types, I am forced to type anything that uses not as unknown, which isn't as great as I would've liked it to be.

You can see the actual file here, on line 3, where I have no better type to give other than unknown :(

If this was a thing in TypeScript, I would be able to provide even more accurate types in the callback of my library function.

Pyrolistical commented 2 years ago

I think the most obvious use-case is being able to define a non-empty string type:

type NonEmptyString = string & !''

Exclude<string, ''> doesn't work because Exclude is only for union types.

For my personal use-case, I wish to write an assertion function that asserts a value is a non-empty string.

function assertNonEmptyString(value: unknown): asserts value is string & !'' {
  if (typeof value === 'string' && value !== '') {
    return
  } else {
    throw new Error(..)
  }
}
HolgerJeromin commented 2 years ago

@Pyrolistical Typescript is mostly about types of variables (and other things) and not about its content.. ref NaN discussion: https://github.com/microsoft/TypeScript/issues/28682#issuecomment-707142417

Pyrolistical commented 2 years ago

@HolgerJeromin but typescript does allow string literal types. How is this different?

thw0rted commented 2 years ago

Not just string literals -- I can think of a number of places where I would find Exclude<number, 0> helpful. Similar to the post above, I'd like to just have some way to say "Type X, but not falsy".

HolgerJeromin commented 2 years ago

but typescript does allow string literal types. How is this different?

They are kind of enums. So with them we have a positive list (this "few" values), but right now no negative list (all but not these values). The type "auto" | string gets string.

darrylnoakes commented 2 years ago

Would this work with template literal types? E.g. to exclude certain substrings from a string. (I'm assuming it would.) If so, I want this even more than before!

// Error: Type '"Hello, foo!"' is not assignable to type 'not `${string}foo${string}`'. ts(2322)
const withoutFoo: not `${string}foo${string}` = "Hello, foo!";

// And with generics:
type NoSubstring<substr extends string> = not `${string}${substr}${string}`;
const withoutFoo: NoSubstring<"foo"> = "Hello, foo!";
typescript-bot commented 1 year ago

This issue has been marked as "Declined" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

douglasg14b commented 1 year ago

"No Recent Activity"

Yet another dead link in a bunch of other issues >_>

Issue fragmentation and lack of coherency is getting to be a problem on large, active, repos

KenjiTakahashi commented 1 year ago

I wonder how far are we from people creating bots that create artificial activity so that bots detecting no activity do not close relevant issues?

truemogician commented 1 year ago

Being mentioned in other issues or pull requests should also be considered as "activity".

RyanCavanaugh commented 1 year ago

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

dead-claudia commented 1 year ago

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

jakebailey commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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

ljharb commented 1 year ago

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

thw0rted commented 1 year ago

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 commented 1 year ago

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

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

yamiteru commented 1 year ago

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 commented 10 months ago

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?