0

when I define (in typescript) custom type, it correctly whisper, when i assign to variable of that type. For example in my code below, when I define variable "error", typescript correctly whisper, that I must add "message" or "code" (as I defined type IErrorType, that it must have at least one of that). But when I use it in function parameter, like in my "printError()", that in function it gives me error: "Property 'message' does not exist on type 'IErrorType'."

But it exists! How can I solve it? I need type in which you must type at least one property from one interface or from another (or from both)...My code is minimal working example, so you can run it...

type IErrorType = IErrorCode | IErrorMessage;

interface IErrorCode {
    code: number;
}

interface IErrorMessage {
    message: string;
}

let error: IErrorType = {
    code: 5
}
let error2: IErrorType = {
    message: "Error!!!"
}
let error3: IErrorType = {
    message: "Error 7!!!",
    code: 7
}

function printError(err: IErrorType){
    if(err && err.message){
        console.log(err.message);
    }
}

printError(error3);
printError(error2);
printError(error);

Thanks!

Petr Marek
  • 595
  • 5
  • 19

2 Answers2

0

Since not all of the types in the union have a .message property on them, typescript will not let you access err.message. Instead, you should use the in operator to check whether message exists before you try to access it:

function printError(err: IErrorType) {
  if ('message' in err) {
    console.log(err.message);
  }
}

Playground link


EDIT: if you want to change your types, here are some options. If you want to be able to use err.message without checking what it is, then .message needs to be on the type.

The following types would require that you either have code, or you have message, but not both.

type IErrorType = IErrorCode | IErrorMessage;

interface IErrorCode {
    code: number;
    message?: never;
}

interface IErrorMessage {
    code?: never;
    message: string;
}

function printError(err: IErrorType) {
  console.log(err.message);
}

If you want to allow both, then take one of the nevers and turn it into the actual type (while still keeping it optional):

type IErrorType = IErrorCode | IErrorMessage;

interface IErrorCode {
    code: number;
    message?: never;
}

interface IErrorMessage {
    code?: number; // <---- now an optional number
    message: string;
}
Nicholas Tower
  • 72,740
  • 7
  • 86
  • 98
  • Ok, it works. But is there way to print err.message without that "in" check? Never mind if I must edit my types definition... But I want to have condition, that any of properties (code/message) must be specified. And the in function if that property will not specified it should just print undefined...Is it possible to do that? – Petr Marek Apr 18 '21 at 14:45
  • I've added some options that would allow you to skip the check, because the properties are now part of the type (albeit optional properties) – Nicholas Tower Apr 18 '21 at 14:53
0

There is alternative solution:


// credits goes to https://stackoverflow.com/questions/65805600/type-union-not-checking-for-excess-properties#answer-65805753
type UnionKeys<T> = T extends T ? keyof T : never;
type StrictUnionHelper<T, TAll> =
  T extends any
  ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;

type StrictUnion<T> = StrictUnionHelper<T, T>

type IErrorType = StrictUnion<IErrorCode | IErrorMessage> | IErrorMessage & IErrorMessage;

interface IErrorCode {
  code: number;
}

interface IErrorMessage {
  message: string;
}

let error: IErrorType = {
  code: 5
}
let error2: IErrorType = {
  message: "Error!!!"
}
let error3: IErrorType = {
  message: "Error 7!!!",
  code: 7
}

function printError(err: IErrorType) {
  if (err && err.message) {
    console.log(err.message);
  }
}

printError(error3);
printError(error2);
printError(error);

Playground