3

When I use a union type for which one of the type is any, the TypeScript editor does not seem to resolve the type properly. I have the following contrived example:

interface ITest {
    name: string;
    age: number;
}

interface ITest2 {
    last: string;
    dob: number;
}

function isGood(input: any) : input is ITest{
    return 'name' in input && 'age' in input;
}

function doIt(){
    var data : ITest2 | ITest  = {
        last: 'Test', dob: 123
    }

    var data2 : any | ITest = {
        name: 'else', age: 45
    }

    if(isGood(data)){
        alert(`name:${data.name}, age:${data.age}`); // data shows ITest
    }

    if(isGood(data2)){
        alert(`name:${data2.name}, age:${data2.age}`); // data2 shows any
    }
}

doIt();

The code does execute properly, but the design time experience is not what I would expect (both in VS Code - 1.4.0 and in the TypeScript playground - 1.8).

As expected, the following line infers that data is of type ITest based on the type guard.

alert(`name:${data.name}, age:${data.age}`); // data shows ITest

However, the second part (with data2) does not infer the right type and is always any:

alert(`name:${data2.name}, age:${data2.age}`); // data2 shows any

In the above line, I would expect intellisense to know that data2 is of type ITest just like it did for data previously.

I have tried swapping the 2 types in the union declaration but it did not change the outcome.

Am I missing something?

Use case

The real use case I am facing is that I get a response from some HTTP call. Right off the wire, it has to be of type any since the HTTP clients know nothing of our data types.

However, I want to to write a user-defined type guard that checks for the properties we want off that json object, then extract only the data we need and returns it.

Eric Liprandi
  • 5,324
  • 2
  • 49
  • 68
  • My guess is that the use of `any` makes the compiler ["pass through compile-time checks"](https://www.typescriptlang.org/docs/handbook/basic-types.html#any). – Nitzan Tomer Aug 30 '16 at 18:51
  • FWIW, came across [this other SO post](http://stackoverflow.com/questions/18961203/typescript-any-vs-object) post that had some great details. Had moved on from this, but figured I'd link the 2. – Eric Liprandi Sep 22 '16 at 21:01

1 Answers1

1

It widens any type to include the type guard. any is already wider than ITest, so it picks any. If you change it to a more narrow type, it correctly infers ITest:

var data2 : {} | ITest = {
    name: 'else', age: 45
}

The reason this happens, is because any is the widest type imaginable, it is every other type at once, so any | ITest doesn't really make sense, that's just the same as any.

Edit: I'd also like to point out that simply removing the type works fine, even if implicit any is turned off. Not sure if this is helpful. Example:

var data2 = {
    name: 'else', age: 45
}

Second edit: Slightly rewrote the answer to be more clear, as the comments indicate, there was some confusion.

Simon Meskens
  • 928
  • 1
  • 6
  • 12
  • The first part of your answer is defiantly wrong, as if for example `ITest extends BaseTest` and then `data2: BaseTest | ITest` then it will work just fine, so `"it chooses the widest type that fits your criteria"` isn't the right reason. You're right that `any | ITest` makes little sense, but the question is why does the `any` bypasses the type guard. – Nitzan Tomer Aug 30 '16 at 18:48
  • If `ITest extends BaseTest` then ITest is the wider of the two, as it extends BaseTest and it picks ITest. I don't see how that example is wrong? Also, note the first `data` is not inferred as `ITest` either, it's inferred as `ITest&ITest2`, again, because it widens this type as much as possible. I'm fairly certain my answer is correct. – Simon Meskens Aug 30 '16 at 18:54
  • With the `BaseTest` and `ITest` example, it will work in both ways. That is if you have a safe guard function for `input is ITest` and one for `input is BaseTest` then it will work with both of them. – Nitzan Tomer Aug 30 '16 at 18:58
  • Of course it will, that still doesn't mean my answer is not correct, it absolutely is. It will widen any type until it satisfies the type guard. `any` satisfies any typeguard, so it picks `any`. `{}` doesn't satisfy any type guard, so it widens it to `ITest`. If the type is of type `A` already, it will widen to `A&ITest`. – Simon Meskens Aug 30 '16 at 19:02
  • You're saying that if `data2: any | ITest` then it will choose `any` because every `ITest` is any. So using the same exact logic if `data2: BaseTest | ITest` should choose `BaseTest` because every `ITest` is also a `BaseTest`. But that's not the case. – Nitzan Tomer Aug 30 '16 at 19:07
  • That's not what I said at all. It will choose `any` because every `any` is an `ITest`. It will not pick `BaseTest` because not every `BaseTest` is `ITest`. – Simon Meskens Aug 30 '16 at 19:10
  • A function that expects `param: any` will be satisfied with an instance of `ITest` because every `ITest` is an `any`. But a function that expects `ITest` won't be satisfied with `any` because `any` is not an `ITest`. a `string` is also `any`, but `any` is not a `string`. The reason it might seems so is (as I wrote in a comment to the OP) is that using `any` bypasses the type checking (that's my guess at least) – Nitzan Tomer Aug 30 '16 at 19:22
  • That is just patently incorrect. A function that expects a `string` will accept a wider type, including `any`. – Simon Meskens Aug 30 '16 at 19:26
  • `any` does not bypass type checking, it is simply the opposite of a bottom type, a type that is every other type. It's clear your knowledge in this field is rather shallow, I advise you to move further discussion to chat. – Simon Meskens Aug 30 '16 at 19:29
  • @SimonMeskens, I think your `{} | ITest` approach seems to make sense. However, I still think this behavior is not intuitive (and wrong?). For the foreseeable future, we will be dealing with `any` coming out of APIs. What would be the proper way to *convert* (not just cast, but verify) `any` to a desired type? – Eric Liprandi Aug 30 '16 at 19:51
  • You're right, that was my mistake, as a function expecting a string will accept an any, but because, as i've said, using any removes type checking. Check the docs, it says on any that: "You might expect Object to play a similar role, as it does in other languages". Everything is `any`, but not the other way around. What you're talking about is type `every` (in theory, as it does not exist). – Nitzan Tomer Aug 30 '16 at 20:00
  • @EricLiprandi, the behavior is not intuitive, but it is correct, type theory wise. You are mixing up what `any` is supposed to do with a bottom type, such as `{}`. My answer is completely compiler-verified, it doesn't cast any object and gives the correct behavior. Never ever accept something coming out of an API as `any`, and if it really is so, just immediately do this: `let data = API.getThing() as {};`. Note that the `as` keyword is not a cast, it will compiler-verify and complain. – Simon Meskens Aug 30 '16 at 20:33
  • 1
    @EricLiprandi, the advantage of casting everything that comes in to `{}` is that you make no assumptions. You basically type the incoming variable as "all I know for sure is this is an object". Any other assumptions will need to be gained through duck typing. I assume this is the behavior you want? Note that `{}` does accept `null` or `undefined`, so you will also need to check for those. – Simon Meskens Aug 30 '16 at 20:44