Noby Fujioka
Noby Fujioka

Reputation: 1824

React Functional component with Typescript: Typescript does not understand changing property types

I am passing a prop which can be string or an object to a react functional component, an example below. It works without typescript. But, when I add typescript, it does not seem to understand that the prop (e.g. helpMessage below) can be either string or an object.

I get this error TS2339: Property 'message' does not exist on type 'string | HelpMessage'.   Property 'message' does not exist on type 'string'.

import React, { FC } from 'react';

interface HelpMessage {
  headerText?: string;
  footerText?: string;
  message?: string;
}

interface FieldProps {
  name: string;
  helpMessage?: string | HelpMessage;
}

const ConditionalPropsComponent: FC<FieldProps> = ({ name, helpMessage }) => {
  const helpMessageIsString = typeof helpMessage === 'string';
  const helpText =
    helpMessage && helpMessageIsString ? helpMessage : helpMessage?.message;

  return (
    <div>
      {helpText && !helpMessageIsString && (
        <div>
          <p>{helpMessage?.headerText}</p>
          <p>{helpMessage?.message}</p>
          <p>{helpMessage?.footerText}</p>
        </div>
      )}
      {helpText && helpMessageIsString && <p>{helpText}</p>}
    </div>
  );
};

export default ConditionalPropsComponent;

The error appears where the red wiggly lines are in the screenshot below:

enter image description here

I created a code sandbox, so that you can see the issue. https://codesandbox.io/s/github/nfabacus/React-typescript-dynamic-type-issue/tree/main/?file=/src/index.tsx:217-226

Basically, extracting the condition to const helpMessageIsString = typeof helpMessage === 'string'; throws the type error, but if I replace every one of them with typeof helpMessage === 'string', error disappears, but this is repetitive..

also, below works but feels hacky - helpMessage is either HelpMessage or undefined. When I pass string, it is neither the HelpMessage object or undefined, but typescript does not complain..which is strange.

interface HelpMessage {
  headerText?: string;
  footerText?: string;
  message?: string;
}

interface FieldProps {
  name: string;
  helpMessage?: HelpMessage;
}

const ConditionalPropsComponent: React.FC<FieldProps> = ({
  name,
  helpMessage
}) => {
  const helpMessageIsString = typeof helpMessage === 'string';
  const helpText = helpMessageIsString ? helpMessage : helpMessage?.message;
....

Typescript is great, but in this particular case, I feel a bit disappointed with Typescript, if there is not a good solution for this...

Upvotes: 0

Views: 116

Answers (1)

Dennis Vash
Dennis Vash

Reputation: 53874

What you lack in this example is Type Assertion and Type Inference.

Typescript is a compiler, it can't interrupt types in run time.

When you writing such code, Typescript deduce helpMessageIsString type as boolean and not as "if its a truthy boolean so helpMessage is string".

// helpMessageIsString is boolean
const helpMessageIsString = typeof helpMessage === "string";

// Typescript can't interrupt if the boolean has a meaning for true
//                                      v string | HelpMessage
const helpText = helpMessageIsString ? helpMessage : helpMessage?.message;

The most simple way to make it work, is using Type Inference by condition blocks:

if (typeof helpMessage === "string") {
  // In this scope, helpMessage is a STRING
} else {
  // In this scope, helpMessage is of type HelpMessage
}

For other unreadable solutions, just pick one of available APIs in type assertion docs.

Upvotes: 1

Related Questions