Search code examples
reactjstypescripttsx

TypeScript can no longer check the existence of properties correctly as soon as checks are outsourced to a function?


I use the following type as property for my component:

type MapProps = {
    standalone: true
} | {
    standalone: false
    inContextOf: Nullable<Artifact | ExcavationSite>
    fieldSetter: UseFormSetValue<FieldValue<any>>

}

Further down, the following checks are performed:

if ((props.standalone || !props.standalone && !props.inContextOf) || props.inContextOf && !IsEntityArtifact(props.inContextOf))
                return;


if (props.inContextOf?.Latitude == 0.0 && props.inContextOf.Longitude == 0.0)
                return;


            const newPoint = {
                type: "Feature",
                geometry: {
                    type: "Point",
                    coordinates: [props.inContextOf!.Longitude, props.inContextOf!.Latitude]
                }
            };

Everything is working as expected with this implementation, however as soon as i move those checks to a function like this

const CheckReturnScenarios = (): boolean => {

        if ((props.standalone || !props.standalone && !props.inContextOf) || props.inContextOf && !IsEntityArtifact(props.inContextOf))
            return true;


        if (props.inContextOf?.Latitude == 0.0 && props.inContextOf.Longitude == 0.0)
            return true;

        return false;
    };

and I use it like this

    if (CheckReturnScenarios())
                return;

            const newPoint = {
                type: "Feature",
                geometry: {
                    type: "Point",
                    coordinates: [props.inContextOf!.Longitude, props.inContextOf!.Latitude]
                }
            };

I do get the following error: TS2339: Property inContextOf does not exist on type MapProps Property inContextOf does not exist on type { standalone: true; }

I'm wondering why it's suddenly no longer possible for TypeScript to determine that the properties being accessed must exist here, because otherwise the check performed above would abort the function.

Would be nice if someone could shed some light on this.

Thank you!

Edit:

The entire component where the error occurs:

import { FC, useState, useEffect } from 'react';

type Person = {
  Name: string;
  Age: number;
  Kind: number;
};

type Product = {
  Title: string;
  Description: string;
  Kind: number;
};

type ComponentProps =
  | {
      standalone: true;
    }
  | {
      standalone: false;
      inContextOf: Nullable<Person | Product>;
      fieldSetter: Function;
    };

export const MyTestComponent: FC<ComponentProps> = (props) => {
  //#region Constants

  //#endregion

  //#region States

  //#endregion

  //#region Hooks
  useEffect(() => {
    Initialize();
  }, []);
  //#endregion

  //#region Functions

  const CheckAbortScenarios = (): boolean => {
    if (
      props.standalone ||
      (!props.standalone && !props.inContextOf) ||
      (props.inContextOf && !IsEntityPerson(props.inContextOf))
    )
      return true;

    if (props.inContextOf?.Kind == 0.0) return true;

    return false;
  };

  function IsEntityPerson(entity: Person | Product): entity is Person {
    return (entity as Person).Name !== undefined;
  }

  const Initialize = (): void => {
    if (CheckAbortScenarios()) return;

    /*The error happens here*/
    const kind = props.inContextOf!.Kind;
  };
  //#endregion

  //#region Render

  return <div></div>;

  //#endregion
};


Solution

  • Since you're moving the type checking logic to a separate function, you're looking for the type guarding is operator.

    In the code that you showed at the bottom you are already using it for the IsEntityPerson() function.


    Revisiting your ComponentProps type, we should first separate each case in the union into separate types. I actually would go as far as replacing Nullable<Person | Product> logic into another union where inContextOf could be null or Person | Product, as such:

    type SubTypeA = {
      standalone: true
    }
    type SubTypeB = {
      standalone: false
      inContextOf: null
      fieldSetter: Function
    }
    type SubTypeC = {
      standalone: false
      inContextOf: Person | Product
      fieldSetter: Function
    }
    
    type ComponentProps = SubTypeA | SubTypeB | SubTypeC
    

    Now, your type guard function should receive the variable that needs type checking as an argument and its return type should be a argument is MyType. This basically discrimates argument into the type MyType if the function's return is true, otherwise it turns into Exclude<typeof argument, MyType>.

    Given this, you should probably separate this type guard into two functions:

    const CheckSubtypeA = (props: ComponentProps): props is SubTypeA => {
      return props.standalone
    }
    const CheckSubtypeB = (props: ComponentProps): props is SubTypeB => {
      return (
        CheckSubtypeA(props) ||
        (!props.standalone && !props.inContextOf) ||
        (props.inContextOf && !IsEntityPerson(props.inContextOf)) ||
        props.inContextOf?.Kind == 0.0
      )
    }
    

    And finally, inside your Initialize function:

    const Initialize = (): void => {
      if (CheckSubtypeA(props) || CheckSubtypeB(props)) return
    
      const kind = props.inContextOf.Kind // no longer errors and no need for a null assert (!)
    }