javascripttypescriptprimitive

JS has object wrappers to let me check primitive's "property" exists or not, how do I let TS work like that?


The return value of XMLValidator.validate is either true or ValidationError (which is not exactly correct, check my update), which has err property.

validate(  xmlData: string,  options?: validationOptionsOptional): true | ValidationError

Because javascript uses object wrappers for boolean primitive I can just check whether the err property exists or not to see if the validation succeeds.

const result = XMLValidator.validate(message)
if (result.err) { // How do I make it work in ts?
   console.log(`invalid XML)
} else {
  console.log(`XML)
}

But typescript won't let me do that, it will complain Property 'err' does not exist on type 'true'

I don't like the idea of checking the return type first because I feel it is wordy and there is no return value type definition actually (check my update). How do I write the ts code here as concise as my js code?

--- update ---

I further check validator.js code

const isValid = validateAttributeString(attrStr, options);
if (isValid !== true) {
   return getErrorObject(...);
}
...

function getErrorObject(code, message, lineNumber) {
  return {
    err: {
      code: code,
      msg: message,
      line: lineNumber.line || lineNumber,
      col: lineNumber.col,
    },
  };
}

So it seems that I can only do if (typeof result == "boolean") here but I hope there is any "general" solution to my question.


Solution

  • You're right that TypeScript won't let you access a property that it doesn't know can exist on a variable's type. If that type is a union, then the property must exist on all members of that union before TypeScript will let you access it.

    But it is possible to narrow the type of a variable through various methods. For example, you could first check if the value is true, and if it's not then TypeScript will narrow the union down to just ValidationError.

    Normally, another option would be to use the in operator, but in this case the union doesn't only contain object types so TypeScript won't allow it.

    You could also define a custom type guard (or use one provided by the library, if one exists), but for a simple case like this that seems like it's probably a bit too much work for what you want.

    As the TypeScript documentation I linked to details, there are other ways of narrowing types too. Checking the result of the typeof operator, like you've mentioned, is another one of those methods.

    This example just declares that a typeguard exists, rather than implementing one, but here's how you could use each of those approaches to narrow the type of your result variable:

    declare type ValidationError = { err: unknown };
    declare function isValidationError(val: unknown): val is ValidationError;
    
    declare const XMLValidator: {
      validate(message: unknown): true | ValidationError
    };
    
    const message = 'test message';
    
    const result = XMLValidator.validate(message)
    
    // You can check first if the result is `true`
    if (result === true) {
      // Handle success
    } else {
      // Then the `else` branch knows it much be a `ValidationError`
      console.error(result.err);
    }
    
    // Normally allowed, but not in this case. Error:
    // Type 'true | ValidationError' is not assignable to type 'object'
    if ('err' in result) {
      console.error(result.err)
    }
    
    if (isValidationError(result)) {
      console.error(result.err);
    }
    

    TypeScript Playground


    Though they're usually best avoided, another option available to you is to use a type assertion via TypeScript's as keyword.

    The reason why these are best avoided is because you're telling TypeScript to treat a type as though it were something else. So you can compromise type safety by doing this.

    console.error((result as ValidationError).err);
    

    However, in cases where you understand what's going on better than the TypeScript compiler does, this is the tool you can use to provide more information to it.

    Personally, I find it's a helpful approach to always leave a comment explaining why a type assertion has been used, mentioning any assumptions that its safety is based on.

    Also, in this case, using a type assertion to check that property doesn't narrow the type of the result variable, so this approach might not be exactly what you need.