-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Why is never assignable to every type? #28982
Comments
The A function that throws an exception is a function that never returns. function throws () : never {
throw new Error("I am an error");
} A function that gets stuck in an infinite loop also has a return type of This function returns function russianRoulette () : never|number {
const r = Math.random();
if (r < 1/6) {
throw new Error("You are dead");
} else {
return r;
}
} However, if //Inferred type of `f` is `number`
const f = russianRoulette(); This also works, const f : number = russianRoulette(); So, we can see that a return value of type An intuitive way to think about assignability is to think about sets and subsets. The set of Therefore, the following is allowed, declare const _123 : 1|2|3;
const n : number = _123; //OK The set of Therefore, the following is not allowed, declare const _123 : 1|2|3;
const _234 : 2|3|4 = _123; //NOT OK So... What is the set of What values are of type And the empty set is a subset of... Every other set. The set of This is also why Since, given a set const nvr1: never = 3; //NOT OK
const nvr2: never = "hello, world"; //NOT OK Being the empty set, no values are assignable to a variable of type |
In terms of type theory, (Personally I found the top/bottom distinction to be weird - intuitively I expect the meanings of “top” and “bottom” to be reversed) Incidentally this highlights the problem with |
These are very good answers. And in theory, this all makes sense now. ...but... :) In practice, things like this can happen: Which means that any TypeScript code targeting a browser has a global "impossible variable" with a very common name. Namely, And due to the principle of explosion, if an impossible variable exists, everything false might as well be true :) For a more concrete example, let's say a developer makes a mistake like this: const useStringArray = (strArr: string[]) => strArr.join(',')
const names = ['Foo', 'Bar']
useStringArray(name) // Meant to use names This is valid TypeScript, but throws an error at runtime. Or perhaps you had a local This has resulted in actual bugs for me on several occasions. So far, my workaround has been "Never use Maybe the global Maybe disallow using variables of type Alternatively, I guess a TSLint rule could be created that checks for this. |
I now looked into this a bit and found some older issues. Most of them are pointing to #15424 (comment)
However, as has been pointed out in some of the other issues, you do not actually get an error from using a |
Seems weird that there should be something explicitly declared as |
I guess logically, a type which can be neither assigned “from” nor “to” would be the exact opposite of |
Would be pretty difficult to accidentally assign anything to and from this type by accident =) |
Actually I don’t think that works - the types have no overlap so I believe the type system resolves the intersection to |
What we're calling "none" here is, I think, proposed as "invalid" in #23689. It's hard to say how it would work in practice, since I'd expect |
@fatcerberus But it is a terrible hack. @jcalz But I've found a workaround that works for my use-case 90% of the time. Make the error checks part of the parameter of the method/function. And (ab)use the Something like, declare function foo<T extends number> (
n : (
T &
(
T extends 42|69|1337 ?
["You cannot use", T, "as an argument"] :
unknown
)
)
) : string (On my phone so I can't verify this is correct) So, if you call As your conditions/types get more complicated, your error messages start getting longer and uglier, too, though. [EDIT] Here's a longer example, type ErrorCheck<T extends number> = (
Extract<42|69|1337, T> extends never ?
unknown :
["You cannot use", T, "as an argument"]
);
declare function foo<T extends number>(
n : T & ErrorCheck<T>
): string
/*
Argument of type '42' is not assignable to parameter of type '42 & ["You cannot use", 42, "as an argument"]'.
Type '42' is not assignable to type '["You cannot use", 42, "as an argument"]'.
*/
foo(42);
//OK
foo(32);
/*
Argument of type 'number' is not assignable to parameter of type 'number & ["You cannot use", number, "as an argument"]'.
Type 'number' is not assignable to type '["You cannot use", number, "as an argument"]'.
*/
declare const n: number;
foo(n);
declare const n2: 42 | 69 | 78;
//Long, ugly, error message
foo(n2);
///// Chaining calls/Generics
function bar<T extends number>(n: T) {
//NOT OK; Long, ugly, error message
return foo(n);
}
function baz<T extends number>(n: T & ErrorCheck<T>) {
//Still NOT OK; Long, ugly, error message
return foo(n);
}
function buzz<T extends number>(n: T & ErrorCheck<T>) {
//OK!
return foo<T>(n)
} |
The The trait of the gradual bottom type is that no relation is defined for it, even reflexive relations, which is why it doesn't really work as you want for conditional types. The type I think you would probably want a one-branched conditional type |
Actually, type Never1 = number & string & boolean
type Never2 = number & boolean
type Never3 = string & boolean
type NotNever1 = string & number
type NotNever2 = number & bigint
type NotNever3 = string & void
type Null1 = string & null
type Null2 = number & null
type Undefined1 = number & undefined
type Undefined2 = boolean & undefined |
@mjomble The explanation there is because number & string & boolean ==>
number & string & (true | false) ==>
(number & string & true) | (number & string & false) ==>
never | never ==>
never The point is moot though because those intersection types are assignable to lots of things from the rules for intersections alone (independently of whether they denote |
If you want to collapse "impossible" intersections to Original: type Never1 = number & string & boolean
type Never2 = number & boolean
type Never3 = string & boolean
type NotNever1 = string & number
type NotNever2 = number & bigint
type NotNever3 = string & void
type Null1 = string & null
type Null2 = number & null
type Undefined1 = number & undefined
type Undefined2 = boolean & undefined All never: type Never1 = number & string & boolean | never
type Never2 = number & boolean | never
type Never3 = string & boolean | never
type NotNever1 = string & number | never
type NotNever2 = number & bigint | never
type NotNever3 = string & void | never
type Null1 = string & null | never
type Null2 = number & null | never
type Undefined1 = number & undefined | never
type Undefined2 = boolean & undefined | never This was decided as a compromise between the people who used |
I think we’ve lost sight of the original goal of this issue - the issue was specifically that The problem here is that there’s no type that means “you can’t assign to OR from it”—even if you make an impossible intersection that doesn’t collapse to |
This issue has been marked as 'Question' and has seen no recent activity. It has been automatically closed for house-keeping purposes. If you're still waiting on a response, questions are usually better suited to stackoverflow. |
I understand that the issue tracker is not for questions, which is why I've already asked this on StackOverflow:
https://stackoverflow.com/questions/53540282/why-is-never-assignable-to-every-type
But as I haven't gotten an answer to the main question in almost two weeks, I thought I'd try here as well:
The TypeScript documentation says that
but doesn't mention why.
Intuitively, I would expect code like this to fail:
but there are no errors, because any
never
value is considered a valid string.Is this intentional? If yes, then why? :)
The text was updated successfully, but these errors were encountered: