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

Type logical operators "and", "or" and "not" in extends clauses for mapped types #31579

Open
5 tasks done
inad9300 opened this issue May 24, 2019 · 8 comments
Open
5 tasks done
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@inad9300
Copy link

inad9300 commented May 24, 2019

Search Terms

Pretty much the ones in the title...

Suggestion

I would like the ability to use logical operators for types.

I am aware that not is already planned as a feature, but I would like to make sure that this gets further extended to other logical operators, and I couldn't find any hints that this is in your minds already.

Use Cases

Type composition, better readability...

Examples

Current tricks to get to the desired behavior:

type Not<T extends boolean> = T extends true ? false : true

type Or<A extends boolean, B extends boolean> = A extends true
    ? true
    : B extends true
        ? true
        : false

type Or3<A extends boolean, B extends boolean, C extends boolean> = Or<A, Or<B, C>>

type And<A extends boolean, B extends boolean> = A extends true
    ? B extends true
        ? true
        : false
    : false

A few arbitrary use cases:

type Primitive = boolean | number | string | symbol | null | undefined | void
type IsA<T, E> = T extends E ? true : false

type IsIndexSignature<P> = Or<IsA<string, P>, IsA<number, P>>

type IsCallback<F extends Function> = F extends (...args: any[]) => any
    ? And<Not<IsA<Parameters<F>[0], Primitive>>, IsA<Parameters<F>[0], Event>> extends true
        ? true
        : false
    : false

All together in the Playground: here

Desired syntactic sugar to write the same:

type IsIndexSignature<P> = IsA<string, P> or IsA<number, P>

type IsCallback<F extends Function> = F extends (...args: any[]) => any
    ? not IsA<Parameters<F>[0], Primitive> and IsA<Parameters<F>[0], Event> extends true
        ? true
        : false
    : false

It would make the most sense to accompany this with better support for boolean checks in type definitions. That is, to allow to use the ternary operator directly without the constant need for extends true everywhere. Possibly, a new sort of "boolean type declaration" could be introduced, as to avoid having to propagate the boolean value all the way. For example, it should be possible to define KnownKeys (not full implementation here) like this:

type KnownKeys<T> = {
    [P in keyof T]: IsIndexSignature<P> ? never : T[P]
}

Without the need to do:

type KnownKeys<T> = {
    [P in keyof T]: IsIndexSignature<P> extends true ? never : T[P]
}

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@MartinJohns
Copy link
Contributor

I don't think introducing more keywords and additional syntax for something that works well with existing tools already is worthwhile.

@inad9300
Copy link
Author

inad9300 commented May 24, 2019

I don't think introducing more keywords and additional syntax for something that works well with existing tools already is worthwhile.

Except it doesn't work well... It is hard to write, hard to read, error prone, and at the same time something quite fundamental, which projects will keep duplicating on and on.

@jcalz
Copy link
Contributor

jcalz commented May 24, 2019

I've more or less switched to using unknown and never instead of true and false for this sort of thing. It has the added benefit of letting you use & for "and" and | for "or". If not ever lands, you can use that; for now you need a generic type alias like type Not<T> = [T] extends [never] ? unknown : never or maybe type Not<T> = unknown extends T ? never : unknown (depending on what you want to see happen with Not<123>).

After that the only thing missing is the abbreviated conditional type syntax C ? Y : N which stands for something like [C] extends [never] ? N : Y or maybe unknown extends C ? Y : N (depending on what you want to see happen with 123 ? Y : N). I wouldn't hold my breath waiting for that to make it into the language, so I guess the best you can do there is a generic type alias also: type Cond<C, Y=unknown, N=never> = [C] extends [never] ? N : Y.

@inad9300
Copy link
Author

inad9300 commented May 25, 2019

@jcalz could you illustrate your ideas by applying them to the examples above? For instance, to IsIndexSignature, perhaps explaining it a bit? It is quite tricky to become certain that | will equate to a logical or by somehow using never and unknown.

I am curious, because I want to have such a feature today if this is possible, but it is most definitely a hack that I don't wish anybody to be forced to use for what would have otherwise been a very easy task.

@jcalz
Copy link
Contributor

jcalz commented May 25, 2019

I suppose "hack" is in the eye of the beholder, here. While the boolean literal types true and false have evocative names, they just represent the types of boolean expressions that exist at runtime, they don't behave like booleans at the type level.

On the other hand, unknown and never are TypeScript's names for the top and bottom types, respectively, which behave much more like booleans at the type level, which is what you're asking for.

The correspondence comes from equating a type to the statement that some value can be assigned to the type:

Propositional Logic Type System Correspondence
T/true/⊤ unknown (top) value can be assigned to unknown? true.
F/false/⊥ never (bottom) value can be assigned to never? false.
and/∧ & (intersection) value can be assigned to A & B iff it can to A and B.
or/∨ | (union) value can be assigned to A | B iff it can to either A or B.
not/¬ not* (complement) value can be assigned to not A iff it is not to A.

* well, if we get negated types

Hopefully that seems less arbitrary. Of course, there is no not yet, and propositional logic doesn't usually have a primitive symbol for a ternary operator, so we'll need our own Not<T> and Cond<T,Y,N>, as I said before:


So, let's look at your example:

type Primitive = boolean | number | string | symbol | null | undefined | void
type IsA<T, E> = T extends E ? unknown : never;

type Cond<C, Y=unknown, N=never> = [C] extends [never] ? N : Y
type IsIndexSignature<P> = IsA<string, P> | IsA<number, P>

type Yes = IsIndexSignature<number> // (never | unknown) = unknown
type No = IsIndexSignature<boolean> // (never | never) = never

Does that make more sense?

@inad9300
Copy link
Author

inad9300 commented May 27, 2019

Hmm... Sort of. I have tried to replace in a bigger example more or less every occurrence of true with unknown, false with never, Or<T> with | and so on, and I failed miserably. The thing is, previously IsIndexSignature<T> was easily testable for use in bigger types via extends true ?. Now this flexibility is lost, and I am (I think) forced to test with extends never ? (or extends [never] ?? I didn't really get why wrapping in an array is necessary...), since everything extends unknown, and thus reverse all the conditionals (quite a few).

The bottom line is, I do not believe your proposal is a reasonable one. Hack is not so much in the eye of the beholder, I would say. This arrangement of tricks is by any metric harder to understand than "regular binary logic" -- in my opinion, to the point where the pain is too high and making mistakes is significantly easier.

@fatcerberus
Copy link

fatcerberus commented May 28, 2019

I didn't really get why wrapping in an array is necessary...

If you mean this pattern: [T] extends [U] ? Y : N - it's to turn off union distribution (see section Distributive conditional types). Generic conditional types map over the individual members of a union so if we don't want that to happen, we turn the union of types into a "tuple-of-union" which is treated as one atomic type. You could replace [T] which pretty much any other covariant generic like Promise<T> and it would work the same way.

@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels May 28, 2019
@yume-chan
Copy link
Contributor

It's sly that TypeScript Handbook describes Conditional Types as

A conditional type selects one of two possible types based on a condition expressed as a type relationship test:

T extends U ? X : Y

The type above means when T is assignable to U the type is X, otherwise the type is Y.

(from https://www.typescriptlang.org/docs/handbook/advanced-types.html#conditional-types)

It never says Conditional Types will resolve to true or false, so if we want to make specific the result of the condition, any binary values can work, unknown and never are definitely ok.


But not true/false

Everyone knows that at runtime the opposite of true is false, and vice versa (except NaN, of course).

But in negated types, not true means "anything not assignable to true" and not false means "anything not assignable to false".

So for example string is not true and not false at the same time, what will string ? true: false resolve to?


However I have to admit that using unknown and never is way more less intuitive.


Originally I came here because I found a super cool blog post (in Chinese) that uses only the type system to build a script parser.

image
(Playground link)

To chain multiple expressions, the author has to heavily use Conditional Types. So I wonder is there any proposal to add logical operators to the Conditional Types, as in JavaScript we can use &&.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

6 participants