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

Why is never assignable to every type? #28982

Closed
mjomble opened this issue Dec 12, 2018 · 16 comments
Closed

Why is never assignable to every type? #28982

mjomble opened this issue Dec 12, 2018 · 16 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@mjomble
Copy link

mjomble commented Dec 12, 2018

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

The never type is a subtype of, and assignable to, every type

but doesn't mention why.

Intuitively, I would expect code like this to fail:

const useString = (str: string) => console.log('This is definitely a string:', str)
const useNever = (not_a_string: never) => useString(not_a_string)

but there are no errors, because any never value is considered a valid string.

Is this intentional? If yes, then why? :)

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Dec 12, 2018

The never type is meant for functions that never return.

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


This function returns number|never

function russianRoulette () : never|number {
    const r = Math.random();
    if (r < 1/6) {
        throw new Error("You are dead");
    } else {
        return r;
    }
}

However, if russianRoulette() does return a value, it'll return a value of type number.

//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 never|number is assignable to a variable of type number.


An intuitive way to think about assignability is to think about sets and subsets.

The set of {1,2,3} is a subset of the set of numbers.

Therefore, the following is allowed,

declare const _123 : 1|2|3;
const n : number = _123; //OK

The set of {1,2,3} is not a subset of the set {2,3,4}.

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 nevers?

What values are of type never? Well... No values are of type never.
The set of never is an empty set. It has no values. No elements.

And the empty set is a subset of... Every other set. The set of numbers. The set of strings. Etc.


This is also why never|number is the same type as number.
And also why never|<any-other-type> is the same as <any-other-type>.

Since, given a set S, the union of S and the empty set is just S


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

@weswigham weswigham added the Question An issue which isn't directly actionable in code label Dec 12, 2018
@fatcerberus
Copy link

In terms of type theory, never is the “bottom type” - it is assignable to everything (in principle a “subclass of every possible type” though I don’t like this description as it requires too many mental gymnastics), in contrast to the “top type” unknown which is assignable from everything.

(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 any as a type: it’s unsound because it acts as both a top and bottom type simultaneously.

@mjomble
Copy link
Author

mjomble commented Dec 12, 2018

These are very good answers.
I also finally got a proper answer on SO (probably thanks to this issue) that mentions the logical principle of explosion.

And in theory, this all makes sense now.

...but... :)

In practice, things like this can happen: lib.dom.d.ts contains declare const name: never;

Which means that any TypeScript code targeting a browser has a global "impossible variable" with a very common name. Namely, name.

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 name variable, then removed it and expected TypeScript to give you errors about any remaining references to it. But all those practically broken references are still theoretically valid, because never is everything :)

This has resulted in actual bugs for me on several occasions. So far, my workaround has been "Never use name as a name". But I'm wondering if there are better solutions for this.

Maybe the global name could be declared differently? As a string? Or unknown?

Maybe disallow using variables of type never where variables of some other type are expected? In theory, this would be wrong. But in practice, maybe the benefits would outweigh the costs?

Alternatively, I guess a TSLint rule could be created that checks for this.

@mjomble
Copy link
Author

mjomble commented Dec 12, 2018

Maybe the global name could be declared differently? As a string? Or unknown?

I now looked into this a bit and found some older issues.

Most of them are pointing to #15424 (comment)

by making it never at least you will get an error and you will go back and check the source of the declaration.

However, as has been pointed out in some of the other issues, you do not actually get an error from using a never.

@fatcerberus
Copy link

Seems weird that there should be something explicitly declared as never - given the nature of the type it should only ever be inferred, it’s very unusual to declare something as not having any possible values. If it has no legal values then by definition it doesn’t exist (like the case of a function that always throws—it has no return value as it doesn’t return)

@fatcerberus
Copy link

I guess logically, a type which can be neither assigned “from” nor “to” would be the exact opposite of any, which is to say, none. Sadly there’s no way to model that in the type system currently.

@AnyhowStep
Copy link
Contributor

type none = number & string & boolean? =P

Would be pretty difficult to accidentally assign anything to and from this type by accident =)

@fatcerberus
Copy link

Actually I don’t think that works - the types have no overlap so I believe the type system resolves the intersection to never, i.e. the empty set. And then we’re back to square one. 😄

@jcalz
Copy link
Contributor

jcalz commented Dec 13, 2018

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 declare const name: invalid; to itself be an error before you even get a chance to call a function with name as a parameter.

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Dec 13, 2018

@fatcerberus
The intersection of those primitives doesn't actually get reduced to the never type, though.

But it is a terrible hack.

@jcalz
Oh, I've seen that proposal. I would really love to have that, too.

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

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 foo(42), you should see a compile time error saying that 42 does not extend the type 42 & ["You cannot use", 42, "as an argument"]

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

Playground

@jack-williams
Copy link
Collaborator

jack-williams commented Dec 13, 2018

The any type isn't really like bottom for a good reason, it's better to think of it as the unknown for types. If unknown is the set of all values then any is the set of all types. The dual to any is the gradual bottom type, which is the set of no types.

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 T exetends U ? A : bot is not related to itself.

I think you would probably want a one-branched conditional type T extends U ? A that resolves to bottom if T or A is instantiated in such a way that the condition fails. T extends U ? A would not be related T extends U ? A : never.

@mjomble
Copy link
Author

mjomble commented Dec 13, 2018

The intersection of those primitives doesn't actually get reduced to the never type, though.

Actually, number & string & boolean does, but some similar intersections don't. I tested a few variations, the names indicate the resolved type (as of the version active in the Playground today):

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

Playground link

@jack-williams
Copy link
Collaborator

@mjomble The explanation there is because boolean is a union type true | false, and empty intersections only get simplified if they appear within a union. For example:

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 never), so they don't function like this none type.

@Nathan-Fenner
Copy link
Contributor

If you want to collapse "impossible" intersections to never, union them with never:

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 & "brand" for "branding" their types (fake nominalism) and the people who wanted these to collapse to never for convenience.

@fatcerberus
Copy link

I think we’ve lost sight of the original goal of this issue - the issue was specifically that never is assignable to everything so the global name being declared as never doesn’t actually prevent all errors. It prevents it being assigned to, but doesn’t catch cases where it’s used in an expression.

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 never, it will nonetheless act like never. Under structural typing, any type which is effectively an empty set is unambiguously a subtype of everything.

@typescript-bot
Copy link
Collaborator

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

8 participants