Home > OS >  Typescript object properties depending on type
Typescript object properties depending on type

Time:04-17

I'm trying to define an event handler that can be used for different components that mostly share event.target structure:

import { SelectChangeEvent } from '@mui/material'

const handleValueChange = ({ target }: ChangeEvent<HTMLInputElement> | SelectChangeEvent) => {
  const { name, value, type, checked } = target

  setState({
    [name]: type === 'checkbox' ? checked : value
  })
}

The problem that I have is that type and checked do not exist on SelectChangeEvent type:

Property 'checked' does not exist on type '(EventTarget & { value: string; name: string; }) | (EventTarget & HTMLInputElement)'.

I don't really want to duplicate the code and create another handler just because of type difference. I can ensure in the code that when I call this handler it is actually going to have the needed properties, but how do I tell that to Typescript?

If my approach is incorrect, how should I refactor this to make it work without duplication (if possible)?

Thank you

CodePudding user response:

You could do it with a type assertion, like this:

const handleValueChange = ({ currentTarget }: ChangeEvent<HTMLInputElement> | SelectChangeEvent) => {
    const { name, type, value, checked } = currentTarget as HTMLInputElement;
    setState({
        [name]: type === "input" ? checked : value
    });
};

...it's best to avoid type assertions where possible. (Playground link.) In the above, for instance, you know you're getting undefined for type when it's used with a select element. It still works, because type won't be "input" when it's a select element, so you'll end up using value instead of checked, but it's best avoided in all but very limited situations. The above may well be limited enough for many people, given the types on the function.

Instead, to be more rigorous, you can narrow the type of currentTarget (usually a better choice than target) with a type guard or two so that the type information is guaranteed to match the runtime reality.

We can start with an instanceof check:

const handleValueChange = ({ currentTarget }: ChangeEvent<HTMLInputElement> | SelectChangeEvent) => {
    let value: string | boolean;
    if (currentTarget instanceof HTMLInputElement) {
        value = currentTarget.checked;
    } else {
        value = currentTarget.value; // <== But we still have errors here
    }
    const { name } = currentTarget;  // <== and one here
    setState({
        [name]: value
    });
};

The errors are because currentTarget may be null and, in the else branch, is just of type EventTarget.

We can solve that with a second instanceof check and a throw:

const handleValueChange = ({ currentTarget }: ChangeEvent<HTMLInputElement> | SelectChangeEvent) => {
    let value: string | boolean;
    if (currentTarget instanceof HTMLInputElement) {
        value = currentTarget.checked;
    } else if (currentTarget instanceof HTMLSelectElement) {
        value = currentTarget.value;
    } else {
        throw new Error(`Expected an HTMLInputElement or HTMLSelectElement`);
    }
    const { name } = currentTarget;
    setState({
        [name]: value
    });
};

Playground link

  • Related