TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

Negated types

Open zpdDG4gta8XKpMCd opened this issue 10 years ago • 62 comments

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

zpdDG4gta8XKpMCd avatar Aug 06 '15 20:08 zpdDG4gta8XKpMCd

It is interesting, it is possible to achieve somehow?

wclr avatar Sep 05 '16 10:09 wclr

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
} 

siegebell avatar Nov 11 '16 17:11 siegebell

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


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

SalathielGenese avatar Mar 09 '18 22:03 SalathielGenese

Do negated types rely on completeness for type-checking?

jack-williams avatar Mar 09 '18 22:03 jack-williams

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.

zpdDG4gta8XKpMCd avatar Mar 09 '18 22:03 zpdDG4gta8XKpMCd

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?

jack-williams avatar Mar 09 '18 22:03 jack-williams

// 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

SalathielGenese avatar Mar 09 '18 22:03 SalathielGenese

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 avatar Mar 09 '18 22:03 zpdDG4gta8XKpMCd

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

zpdDG4gta8XKpMCd avatar Mar 09 '18 23:03 zpdDG4gta8XKpMCd

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!

jack-williams avatar Mar 09 '18 23:03 jack-williams

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

I think you meant A not assignable to C

SalathielGenese avatar Mar 09 '18 23:03 SalathielGenese

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

zpdDG4gta8XKpMCd avatar Mar 09 '18 23:03 zpdDG4gta8XKpMCd

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

SalathielGenese avatar Mar 09 '18 23:03 SalathielGenese

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

SalathielGenese avatar Mar 09 '18 23:03 SalathielGenese

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.

zheeeng avatar Apr 11 '18 06:04 zheeeng

export type NotUndefined = !undefined;

would be extremely useful IMO

the1mills avatar Jul 17 '18 23:07 the1mills

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

RyanCavanaugh avatar Aug 15 '18 05:08 RyanCavanaugh

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

jpike88 avatar Aug 28 '18 06:08 jpike88

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

What mistake?

RyanCavanaugh avatar Aug 28 '18 16:08 RyanCavanaugh

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

jpike88 avatar Aug 29 '18 02:08 jpike88

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

ORESoftware avatar Aug 29 '18 07:08 ORESoftware

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?

jpike88 avatar Aug 29 '18 08:08 jpike88

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.

jack-williams avatar Aug 29 '18 13:08 jack-williams

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

RyanCavanaugh avatar Aug 29 '18 20:08 RyanCavanaugh

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>.

jack-williams avatar Aug 29 '18 22:08 jack-williams

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;
};

jvanbruegge avatar Oct 30 '18 17:10 jvanbruegge

@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

dead-claudia avatar Nov 26 '18 00:11 dead-claudia

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

webhacking avatar Apr 22 '19 04:04 webhacking

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

dead-claudia avatar Apr 22 '19 16:04 dead-claudia

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.

k-tten avatar May 28 '22 19:05 k-tten