-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Intersection of conditional types, acting on a generic type, with another type doesn't narrow the generic type #57246
Comments
|
It's actually not true that type NonFunction<A> = A extends Function ? never : A;
type Fun = () => void;
type Obj = { foo: string };
type MoreFun = NonFunction<Obj> & Fun;
declare const foo: MoreFun;
foo.foo; // ok
foo(); // also ok Footnotes
|
@fatcerberus Can you give me a real life example of how you would construct a value of type |
function foo() {
console.log("you fooey bard!")
}
foo.foo = "foo"; All functions are objects in JavaScript. |
@fatcerberus I understand that. In fact, I wrote about this very issue in the Additional information about the issue section.
However, that's what I think is wrong. I think that in your example Note that I have no issue with the type NonFunction<Obj> & Fun
= (Obj & !Function) & Fun // by definition
= Obj & (!Function & Fun) // associativity
= Obj & never // law of non-contradiction
= never // annihilation |
It's not practical for every possible non-inhabitable type to be simplified to |
But that's the thing - those are the same type. Type aliases are just type-functions - |
If we had negated types, you could just write the latter directly, but I disagree that that's what the conditional type you wrote should simplify to. Ternary expressions aren't retroactive at runtime, so I wouldn't expect the typespace equivalent to be either. |
Yes, The type By simplifying
True, if we had negated types then I could define type NonFunction<A> = A & !Function; However, I disagree with the second part of your statement. A conditional type of the form First, we need to understand what Next, the Putting it all together, the conditional type At this point, we can use the modus ponens rule to infer that Finally, Now, you might be think that there's some flaw in my argument because I'm treating types as propositions. After all, types are not the same as propositions. But, the CurryβHoward isomorhpism is proof that types and propositions are indeed equivalent. It's one of the seminal theorems of computer science. In conclusion, |
I understand that the solution this for this problem might take a lot of work to implement, and therefore it's not something that you want to solve. However, this is indeed a defect of the type system as I demonstrated using propositional logic above. Hence, I don't think the "Not a Defect" label is justified here. To be fair, I can easily solve this problem using type guards. type Thunk<A> = () => A;
type NonFunction<A> = A extends Function ? never : A;
type Expr<A> = Thunk<A> | NonFunction<A>;
const isThunk = <A>(expr: Expr<A>): expr is Thunk<A> =>
typeof expr === "function";
const evaluate = <A>(expr: Expr<A>): A =>
isThunk(expr) ? expr() : expr; However, I expected TypeScript to correctly narrow the type of |
That would make the conditional type retroactive, which, again, is not how they work. That would be like writing a function: function nonString(x) {
// let's assume throw expressions are available for brevity
return typeof x !== 'string' ? x : throw Error("no strings allowed!");
} and then expecting it to throw if, at any point, A conditional type is a computation done on a type at compile time, not a new kind of "dynamic type". |
The problem literally isn't computable. It's not for lack of desire to compute BB(12) that I haven't. |
I don't understand what you mean by "retroactive". Could you please provide a definition for this term? This is the second time you've used this term and I'm honestly confused as to what you're trying to convey. Also, could you please elucidate the following statement?
Again, I don't understand what you mean by "retroactive" and hence I don't understand what "typespace equivalent" means in this sentence either.
Your analogy is wrong. If you want to view types as values then you should reify them as predicates. The predicate takes any value, and returns // Analogous to the type `unknown`.
const unknown = () => true;
// Analogous to the type `Function`.
const func = (x) => typeof x === "function";
// Analogous to the type `NonFunction<A> = A & !Function`.
const nonFunc = (t) => (x) => t(x) && !func(x);
// Analogous to the type `A & B`.
const intersect = (t1, t2) => (x) => t1(x) && t2(x);
// Analogous to the type `NonFunction<A> & Function`.
const nonFuncAndFunc = (t) => intersect(nonFunc(t), func);
// Analogous to the type `never`.
const absurd = nonFuncAndFunc(unknown);
console.log(absurd(42)); // false
console.log(absurd(() => 42)); // false As you can see, the Also note that the Anyway, you might have noticed that I defined
So is the algorithm I have in mind to simplify |
Interesting, so you're telling me that this particular problem is undecidable? I would love to understand why. Would you explain it to me?
What is "BB(12)"? |
Retroactive is probably the wrong term. But basically you wrote a ternary expression (a conditional type) and now expect it to remain live after it's already been computed, i.e. you expect the type-level equivalent of
|
I don't understand what you mean by "remain live". Could you please elucidate? Also, your
I agree. In fact, in my previous comment I indeed reified // Analogous to the type `NonFunction<A> = A & !Function`.
const nonFunc = (t) => (x) => t(x) && !func(x);
// ^^^ ^^^^^^^^^^^^^^^^^^^^^^^
// | |
// input type output type
The algorithm I have in mind also does a very simple computation. In fact, let me demonstrate what I have in mind. type Example<A> = A extends string ? { foo: A } | { bar: A; baz: A };
type Foo = Example<"a" | "b" | "c">; // Simplifies to { foo: "a" } | { foo: "b" } | { foo: "c" }
type Bar = Example<"bar" | number>; // Simplifies to { foo: "bar" } | { bar: number & !string; baz: number & !string }
type Baz = Example<unknown>; // Simplifies to { bar: unknown & !string; baz: unknown & !string } Here are the key points of this algorithm.
In the above example, As you can see, this is a purely compile-time computation and it works exactly like you would expect a function on types to work.
That's what is wrong, and that's what I want to change.
I think I've sufficiently demonstrated that I'm not operating under such a premise. |
It can't be narrowed down to this type, because types like Based on the types support currently existing this is working as intended / not a defect. Undesirable? Sure. But not a bug. |
This is precisely what I wrote in my original post.
In fact, if we had negation types then as you pointed out we could simply define Anyway, now that we agree on something, could you at least tell me what are your thoughts on narrowing in the false branch of conditional types if negated types were added to TypeScript?
Logically, it is a defect. You wouldn't expect the true branch of the following const conditionA = true;
const conditionB = true;
if (conditionA && !conditionB) {
console.log(`
You wouldn't expect this log to be printed
because conditionB is not false. Hence, if
this log's printed that means that there's
a defect in the JavaScript runtime. In the
same way NonFunction<A> should evaluate to
A & !Function. If it evaluated to simply A
then it should be regarded a major defect.
`);
} After this discussion, I'm convinced that not having negation types in TypeScript is indeed a defect. It's like a baby born without hearing. Deafness is certainly a defect. Similarly, not having negation types in TypeScript is also a defect. Anyway, I'm closing this issue as a duplicate of #4196. |
@MartinJohns Sorry, I mistook you for @fatcerberus. Anyway, it seems like I'm not the first person to face this exact issue. See #29317. In particular, the following paragraph sums up my experience perfectly.
Emphasis mine. |
I'm really not sure how you can see type NonFunction<T> = T extends Function ? never : T; and interpret that as anything other than typefun NonFunction(T: Type) {
if (T assignsto Function)
return never;
else
return T;
} because that's exactly what that type means. It's literally an identity function if you pass it anything that's not currently assignable to |
@fatcerberus I'm really not sure how you can see typefun NonFunction(T: Type) {
if (T assignsto Function)
return never;
else
return T;
} and not realize that
I lot of people besides myself would disagree with you on that. Just look at the number of ππΌ, π, β€οΈ, and π emojis on #29317. And, not to mention when you view types as propositions, which is a fundamental theorem of computer science, it becomes obvious that
I concur. The type returned by This is the kind of simplification that TypeScript does often. For example, |
I think I might see where you're coming from here; you envision the tl;dr is that, in general, |
Hi, just wanted to point out that negated types are absent in virtually all other languages as well, making all other languages defective as well - if not more defective than TS. |
@somebody1234 Other languages don't have conditional types like TypeScript does. So, no that analogy doesn't apply. |
More concretely: type NonDog<T> = T extends Dog ? never : T;
type Test = NonDog<Animal>; You asked whether |
@fatcerberus Let me formalize what you're saying so that I understand it.
That makes sense to me. I don't see any problem here. I certainly don't see how the proposed narrowing is wrong.
Why would you even care if
So, it makes perfect sense to set
You don't make any sense. If
|
In fact you are. |
I literally don't see the check for type Animal = Cat | Dog | Horse;
type NotDog = Animal & !Dog; // equivalent to Cat | Horse
declare const cat: Cat;
const notDog: NotDog = cat;
const animal: Animal = notDog; |
We're not concluding that |
You're arguing that |
Yes, type Cat = "cat";
type Dog = "dog";
type Horse = "horse";
type Animal = Cat | Dog | Horse;
type NonDog<T> = T extends Dog ? never : T;
const dog: Dog = "dog";
const nonDog: NonDog<Animal> = dog;
// ^^^^^^
// Type '"dog"' is not assignable to type '"cat" | "horse"'. (2322)
What does that have to do with anything?
I haven't. You literally can't assign a |
@aaditmshah counterexample: type Cat = { cat: true };
type Dog = { dog: true };
type Horse = { horse: true };
type Animal = Cat | Dog | Horse;
type NonDog<T> = T extends Dog ? never : T;
const dog = { dog: true, horse: true } satisfies Animal & Dog;
const nonDog: NonDog<Animal> = dog; |
@somebody1234 Your counterexample has errors. type Cat = { cat: true };
type Dog = { dog: true };
type Horse = { horse: true };
type Animal = Cat | Dog | Horse;
type NonDog<T> = T extends Dog ? never : T;
const dog: Dog = { dog: true, horse: true };
// ^^^^^
// Object literal may only specify known properties, and 'horse' does not exist in type 'Dog'. (2353)
const nonDog: NonDog<Animal> = dog;
// ^^^^^^
// Type 'Dog' is not assignable to type 'Cat | Horse'.(2322) |
Listen, as much as I love to argue with random strangers on the internet, I'm going to have to withdraw from this conversation now. It's pointless and unhealthy. Feel free to keep commenting, but I'm not going to reply. In fact, I'm unsubscribing from this thread. I already closed this issue because it's a duplicate of #4196. Have a great day folks. |
@aaditmshah it doesn't have errors, you modified the code so that it produced an error |
hmm... i guess the idea doesn't quite check out |
@aaditmshah Here is a proper incorrect case: type Cat = { animal: true; cat: true };
type Dog = { animal: true; dog: true };
type Horse = { animal: true; horse: true };
type Animal = { animal: true };
type NonDog<T> = T extends Dog ? never : T;
const dog: Dog = { animal: true, dog: true };
const animal: Animal = dog;
const nonDog: NonDog<Animal> = animal; |
For arbitrary |
π Search Terms
π Version & Regression Information
This is the behavior in every version I tried, and I reviewed the FAQ for entries about narrowing.
β― Playground Link
https://www.typescriptlang.org/play?target=99&jsx=0#code/C4TwDgpgBAKgFgVwHYGsA8BBAfFAvFACgEo8cMBuAKEtEigDkB7JAMWQGNgBLZzHfDFAgAPYBCQATAM5Q2STjyRQA-FCQQAbhABOUAFxQK1WtACiwsNr55YiVNYA+DZnIW9sVSu2ZTgQjQCGADYIAWI2fAQilgbmlnxEBoK4WJRQUCaMAGZCFrq4BVAARFkc3MxFKrmWxPrV2lTpAPRN6W3tHZ1dAHq93WlQLVAAtKNj4xOTU9Mzs3PzUwND8Fwy0doQUlKKUKtqjH7swUEBAEZBEAB0S63pTH7HUN5IvlzACOLAMtkZ4NAA5PBkOhsFAnAQmKwyoprAAyWTQ5hEf5QAIbJ7HM4Xa7NW5tGB-KD-SGucpIOEI+RklFwAIyJCMDFBIJQbYAcyQYQQGykl0IACYAMwAFgAnEQbiMFtKZbK5TNJQQwGiAgBbCBibQkdYGIH2UHgkmI8mg+GkxQS3FS+U2212iaUIA
π» Code
π Actual behavior
The parameter
expr
, which has the typeExpr<A>
, is being narrowed by thetypeof expr === "function"
condition. When the condition istrue
, the type ofexpr
is narrowed toThunk<A> | (NonFunction<A> & Function)
. When the condition isfalse
, the type ofexpr
is narrowed toNonFunction<A>
.π Expected behavior
When the condition is
true
, the type ofexpr
should be simplified to justThunk<A>
.The challenging step to understand is step number 4 where we invoke the law of non-contradiction to simplify the type
A & Function
tonever
. The law of non-contradiction states that some propositionP
and its negation can't both be true. In our case, the proposition isFunction
. SinceA & Function
is in the else branch of the conditionalA extends Function
, it implies thatA
is not a function. Hence,A
andFunction
are contradictory. Thus, by the law of non-contradiction we should be able to simplify it tonever
.In plain English,
NonFunction<A> & Function
is a contradiction. A type can't both be aFunction
and aNonFunction<A>
at the same time. Hence, the type checker should simplifyNonFunction<A> & Function
tonever
.At the very least,
NonFunction<A> & Function
should be callable. We shouldn't get ats2349
error.Additional information about the issue
I understand that as TypeScript is currently implemented,
NonFunction<A> & Function
doesn't simplify tonever
for all possible typesA
. For example, consider the scenario whenA
isunknown
.This seems to be because in step number 2 we're simplifying
unknown extends Function ? never : unknown
to justunknown
. Instead, we should simplify it tounknown & !Function
where the!
denotes negation, i.e. an unknown value which is not a function. Then we can apply the law of non-contradiction to get the correct type.At the very least, this seems to suggest the need for some sort of negation type.
The text was updated successfully, but these errors were encountered: