3

I have a React application (although the question is not really about React), with typescript.

In a functional component (whose consumer I want to be able to pass OR styles OR a class, but not both), I use the following props type:

type AnimationContainerProps = { hidingTimeoutMs: number } & (
    | {
        displayingClass: string;
        hidingClass: string;
      }
    | {
        displayingInlineStyle: string;
        hidingInlineStyle: string;
      }
  );

I would like to destructure it in a single instruction, like:

const { hidingTimeoutMs, displayingClass, hidingClass, displayingInlineStyle, hidingInlineStyle } = props

In javascript, it would be no problem and I would receive undefined in the non-existent props. But in Typescript, I can't do this, because I'll get a Property 'XXX' does not exist on type 'AnimationContainerProps'.ts(2339).

I want to avoid making the attributions one by one. Do you guys think of a way to destructure it?

2
  • 1
    You need your union to be properly exclusive for the props, so that TS knows that it can expect undefined (TS types are not sealed, objects can always have props TS doesn't know about. The value const x = { hidingTimeoutMs: 0, displayingClass: "", hidingClass: "", displayingInlineStyle: 1} is a perfectly valid AnimationContainerProps.) For example, as shown here using a utility type to automatically transform a union to an exclusive version. Does that fully address the question? If so I'll write up an answer explaining; if not, what am I missing? Commented Mar 7, 2024 at 18:48
  • This is perfect, I implemented it in this way. Thanks! Commented Mar 8, 2024 at 15:45

4 Answers 4

3

You could use a typeGuard function to decide what to destructure from your props (inline or class)

// Type guard function
const hasInlineStyles = (
  props: AnimationContainerProps
): props is {
  displayingInlineStyle: string;
  hidingInlineStyle: string;
  hidingTimeoutMs: number;
} => 'displayingInlineStyle' in props;

// Example usage in your component
const Component = (props: AnimationContainerProps) => {
  const { hidingTimeoutMs } = props;
  let displayingStyle: string;
  let hidingStyle: string;

  if (hasInlineStyles(props)) {
    displayingStyle = props.displayingInlineStyle;
    hidingStyle = props.hidingInlineStyle;
    // Use displayingStyle and hidingStyle
  } else {
    displayingStyle = props.displayingClass;
    hidingStyle = props.hidingClass;
    // Use displayingStyle and hidingStyle
  }

  // Rest of your component logic
};

Sign up to request clarification or add additional context in comments.

2 Comments

In this case, won't the destructured variables be in the scope of the if, therefore unusable in the Rest of my component logic?
I have updated my answer for fixing this scoping issue
2

Here's one way you could do it:

const { hidingTimeoutMs, displayingClass, hidingClass, displayingInlineStyle, hidingInlineStyle } = {
    displayingClass: undefined,
    hidingClass: undefined,
    displayingInlineStyle: undefined,
    hidingInlineStyle: undefined,
    ...props
};

For the missing properties, you can use undefined or any other value or object that's convenient.


In a comment, jcalz points out that your definition of the AnimationContainerProps type is not properly exclusive. For example, someone could pass string values for all four class/style props to your component. Or they could pass the following object, which has displayingInlineStyle and hidingInlineStyle values of the wrong type:

const props = {
    hidingTimeoutMs: 0,
    displayingClass: "",
    hidingClass: "",
    displayingInlineStyle: 0,
    hidingInlineStyle: false,
};

<AnimationContainer {...props} />

If you're concerned about this case, you can change your type and force missing/undefined values for displayingInlineStyle and hidingInlineStyle if displayingClass and hidingClass are specified, and vice versa:

type AnimationContainerProps = { hidingTimeoutMs: number } & (
    | {
        displayingClass: string;
        hidingClass: string;
        displayingInlineStyle?: undefined;
        hidingInlineStyle?: undefined;
      }
    | {
        displayingClass?: undefined;
        hidingClass?: undefined;
        displayingInlineStyle: string;
        hidingInlineStyle: string;
      }
);

With this change, your original destructuring code would work as desired.

2 Comments

A fallback for the props! nice! It works for me.
Ok, this edit I didn't imagine would work! It's even better.
1

TypeScript enforces type conformity, hence you have two options to retain type enforcement.

Option 1 - supercomposition:

type AnimationContainerProps = { hidingTimeoutMs: number } & { option: (
  | {
    displayingClass: string;
    hidingClass: string;
    }
  | {
    displayingInlineStyle: string;
    hidingInlineStyle: string;
    }
) };

let props = {} as AnimationContainerProps;
const { hidingTimeoutMs, option } = props;

Option 2 - decomposition:

type AnimationContainerProps = {
  hidingTimeoutMs: number;
  displayingClass: string | undefined;
  hidingClass: string | undefined;
  displayingInlineStyle: string | undefined;
  hidingInlineStyle: string | undefined;
};

let props = {} as AnimationContainerProps;

const { hidingTimeoutMs, displayingClass, hidingClass, displayingInlineStyle, hidingInlineStyle } = props;

1 Comment

This was helpful (hence the upvote), but I prefer the other answer. Thank you for your time!
1

Here you will find a nice utility for such case:


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;

// taken from here https://stackoverflow.com/questions/65805600/type-union-not-checking-for-excess-properties#answer-65805753
type StrictUnion<T> = StrictUnionHelper<T, T>

type AnimationContainerProps = { hidingTimeoutMs: number } & StrictUnion<(
    | {
        displayingClass: string;
        hidingClass: string;
    }
    | {
        displayingInlineStyle: string;
        hidingInlineStyle: string;
    }
)>;

declare let props:AnimationContainerProps

// ok
const { hidingTimeoutMs, displayingClass, hidingClass, displayingInlineStyle, hidingInlineStyle } = props

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.