Skip to content

Type assertion considers empty object compatible with any interface #25539

Closed

Description

TypeScript Version: 3.0.0-dev.20180707
2.9.2

Search Terms: empty object compatible with any interface
type assertion empty object

I originally thought that as acted as a hard cast operator, ignoring types. But then someone told me that it can be used to check types. And, sure enough, the expression ({x:24} as IPerson) will result in TS2352. However, ({} as IPerson) will yield IPerson even if IPerson has required properties. This behavior seems inconsistent with itself and I could not find anything obvious in the FAQ about it.

Code

interface IPerson {
  name:string;
}

function myFunc(person:IPerson) {
  console.log(`name is ${person.name.length} characters long`);
}

const person = {} as IPerson;
myFunc(person);

Expected behavior:

 $ ./node_modules/.bin/tsc
index.ts:9:16 - error TS2352: Type '{}' cannot be converted to type 'IPerson'.
  Property 'name' is missing in type '{}'.

9 const person = {} as IPerson;
                 ~~~~~~~~~~~~~

Actual behavior:

$ ./node_modules/.bin/tsc
$ node ./build/dist/index.js
/tmp/.private/ohnobinki/typescript3-play/build/dist/index.js:3
    console.log(`name is ${person.name.length} characters long`);
                                       ^

TypeError: Cannot read property 'length' of undefined
    at myFunc (/tmp/.private/ohnobinki/typescript3-play/build/dist/index.js:3:40)
    at Object.<anonymous> (/tmp/.private/ohnobinki/typescript3-play/build/dist/index.js:6:1)
    at Module._compile (module.js:652:30)
    at Object.Module._extensions..js (module.js:663:10)
    at Module.load (module.js:565:32)
    at tryModuleLoad (module.js:505:12)
    at Function.Module._load (module.js:497:3)
    at Function.Module.runMain (module.js:693:10)
    at startup (bootstrap_node.js:188:16)
    at bootstrap_node.js:609:3

Playground Link: http://www.typescriptlang.org/play/#src=interface%20IPerson%20%7B%0D%0A%20%20name%3Astring%3B%0D%0A%7D%0D%0A%0D%0Afunction%20myFunc(person%3AIPerson)%20%7B%0D%0A%20%20console.log(%60name%20is%20%24%7Bperson.name.length%7D%20characters%20long%60)%3B%0D%0A%7D%0D%0A%0D%0Aconst%20person%20%3D%20%7B%7D%20as%20IPerson%3B%0D%0AmyFunc(person)%3B%0D%0A

Related Issues:

I found FAQ entry “Why are all types assignable to empty interfaces?”, but that situation has an empty interface and an object with anything in it rather than the other way around.

Activity

  1. mattmccutchen commented on Jul 10, 2018

    @mattmccutchen
    Contributor

    as can be used to cast to a supertype or a subtype, but you get TS2352 if the target type is neither a supertype nor a subtype of the original type, because that often (but not always) indicates a mistake. See the language specification (which is seriously out of date in many places, but this behavior has been around long enough to be covered).

    Want to turn this issue into a feature request for a better error message? That would fit well with the work on better error messages in TypeScript 3.0.

  2. added a commit that references this issue on Jul 10, 2018
    bd1ab26
  3. RyanCavanaugh commented on Jul 10, 2018

    @RyanCavanaugh
    Member

    The OP is working as intended; see issues like #7531 and #9312.

    Regarding phrasing, in TypeScript 1.8 we issued the error

    TS2352: Neither type '{ m: number; }' nor type '{ s: string; }' is assignable to the other.

    But then had to change it to the more generic phrasing because we switched to a different rule for when an assertion is allowed. The proposed text in the PR isn't right either, as the actual relationship is "relatability"; this is a more loose rule than assignability as it allows this:

    var x: string | number;
    var y: number | Date;
    x as typeof y; // OK
    

    even though x and y are not subtypes of or assignable to/from each other.

    I think there's likely a better message than the current

    TS2352: Type '{ m: number; }' cannot be converted to type '{ s: string; }'.

    but I don't know what it is. We need to communicate to the developer that the two types appear to be completely unrelated, but don't have much space to communicate what our definition of "related" is.

  4. mattmccutchen commented on Jul 10, 2018

    @mattmccutchen
    Contributor

    Thanks RyanCavanaugh for looking at this!

    We need to communicate to the developer that the two types appear to be completely unrelated, but don't have much space to communicate what our definition of "related" is.

    I don't think we need to communicate the whole definition. Even giving the user an idea of what is going on so we don't get more issues filed like this one will be an improvement on the status quo. How about just what you said?

    The asserted type '{1}' appears completely unrelated to the original type '{0}'. If this is intentional, convert to 'unknown' first.

  5. RyanCavanaugh commented on Jul 10, 2018

    @RyanCavanaugh
    Member

    Just writing the options down next to each other:

    • x cannot be converted to y
      • Because z
    • x appears completely unrelated to y
      • Because z

    It's not immediately apparent why someone confused by one wouldn't be confused by the other? Trying to see it through OP's eyes, {} is just as much "completely unrelated" to IPerson as it is "not convertable to".

    We don't want to churn the error messages too much (it causes search keyword rot) so I'd like any new message to be identifiably better than the status quo.

  6. mattmccutchen commented on Jul 10, 2018

    @mattmccutchen
    Contributor

    AIUI, OP's incorrect belief was that type assertions allowed upcasts only, and the "Because z" reinforces this belief (!) by reporting how the assertion fails to be an upcast rather than how it fails to be a downcast. To me, the "completely unrelated" message clearly indicates that the rule is something other than upcasts only, while "not convertable" just says the assertion doesn't follow the rule, whatever it is. If I saw that {x:24} as IPerson is an error and {} as IPerson is not, I'd assume "completely unrelated" must have some definition consistent with those observations and not file an issue. So I think this change addresses at least OP's misunderstanding, though I won't claim it addresses every conceivable misunderstanding. It's unfortunate that the "Because z" explains only one direction of the failure, but that's less trivial to fix; I could try to fix it if you give me an idea how.

    I don't want to start a long debate that consumes time that could be better spent on other things, so I'm fine if you want to close the issue and PR and not pursue this now.

  7. RyanCavanaugh commented on Jul 10, 2018

    @RyanCavanaugh
    Member

    I think you're right about the bidirectionality. "completely unrelated" is sort of informal; maybe it should be something like

    Types 'x' and 'y' are not related to each other.

    Daniel Rosenwasser (@DanielRosenwasser) thoughts?

  8. binki commented on Jul 10, 2018

    @binki
    Author

    Ah, I see I misunderstood the functionality of as because I tried to guess what its functionality was given the error messages I could get it to produce in a set of experiments which did not include pure widening. I found the operator quite confusing because I could sort of see it being used to add static type checking by narrowing types sometimes and bypassing static type checking widening them other times (mainly when casting to any which I thought was a special case). I wish there were one-way operators so that I could remove this ambiguity from my code and that as was never defined the way it is, but it’s way too late to change that.

    I guess that to get the functionality I want, there is no built-in expression style operator. I am required to declare a variable or call a function to get what I want which actually ends up being similar to the situation in C#:

    function staticCast<T>(value:T):T {
      return value;
    }

    If the error message is made less suggestive, that would be helpful. I think if it said something that made it obvious that the compiler tried both directions that would help. For example, “{ blah; x } can neither be narrowed or widened to IPerson because neither type is assignable to the other”. The “Types 'x' and 'y' are not related to each other” from Ryan Cavanaugh (@RyanCavanaugh) is also clear.

  9. mattmccutchen commented on Jul 10, 2018

    @mattmccutchen
    Contributor

    Nathan Phillip Brink (@binki) FWIW, I think you're using the terms "narrowing" and "widening" backwards to how they are used in TypeScript (downcast and upcast respectively).

    I'll wait for further guidance from Ryan Cavanaugh (@RyanCavanaugh) or Daniel Rosenwasser (@DanielRosenwasser) before updating the PR.

  10. binki commented on Jul 10, 2018

    @binki
    Author

    Matt McCutchen (@mattmccutchen) Thanks for the correction. It looks like I was thinking that “more strictly specified” meant wider for objects because there are more keys when it’s actually narrower because it is more specified and fewer other can be assigned to it. Hopefully I keep that straight in the future.

  11. added 3 commits that reference this issue on Jul 17, 2018
    1c675dd
    f1fabd8
    aea6196
  12. typescript-bot commented on Jul 25, 2018

    @typescript-bot
    Contributor

    Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

  13. added a commit that references this issue on Jul 25, 2018
    da64479
  14. locked as resolved and limited conversation to collaborators on Oct 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Working as IntendedThe behavior described is the intended behavior; this is not a bug

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions