Material-ui: `onChange` parameter has `unknown` type in `Select` component

Created on 5 Jun 2019  路  16Comments  路  Source: mui-org/material-ui

I have below code to use Select component but I get type error when upgrading to 4.0.2.

import Select from '@material-ui/core/Select';

<Select
            native
            value={selectedValue}
            onChange={(e) => onChange(e.target.value)}

          >

This is the error about e.target.value parameter. It says the value is unknown.

Argument of type 'unknown' is not assignable to parameter of type 'string'.
docs typescript

Most helpful comment

onChange={(e) => seValue(e.target.value as string)}

All 16 comments

As far as I can tell, you need to specify the type of the value, it's defaulted to unknown.

onChange={(e: React.ChangeEvent<{ value: string }>) => onChange(e.target.value)}

@oliviertassinari Can't find this in the migration guide

Might be a good idea to include #15245 and #15272 in the migration guide. This particular issue was more of a bug fix. The previous type was too narrow.

In my opinion, unknown is the wrong type for value. As mentioned in TS documentation:

Anything is assignable to unknown, but unknown isn鈥檛 assignable to anything.

Which means that in this component, values would accept any type from the outside, but when passing a function to the onChange props like @merceyz we get the following error:

Type 'unknown' is not assignable to type 'string'.ts(2322)

I think the type any is more fit to this situation, do you guys agree ?
If so I would like to open a PR to change this :)

If you want opt-out of strict types you can do so by using any. We are striving for strict by default, loose via opt-in. any goes against that.

As far as I can tell, you need to specify the type of the value, it's defaulted to unknown.

onChange={(e: React.ChangeEvent<{ value: string }>) => onChange(e.target.value)}

@oliviertassinari Can't find this in the migration guide

I tried this solution but it doesn't solve the issue. The error I got is:

TS2322: Type '(e: ChangeEvent<{ value: string; }>) => void' is not assignable to type '(event: ChangeEvent<{ name?: string | undefined; value: unknown; }>, child: ReactNode) => void'.
  Types of parameters 'e' and 'event' are incompatible.
    Type 'ChangeEvent<{ name?: string | undefined; value: unknown; }>' is not assignable to type 'ChangeEvent<{ value: string; }>'.
      Type '{ name?: string | undefined; value: unknown; }' is not assignable to type '{ value: string; }'.
        Types of property 'value' are incompatible.
          Type 'unknown' is not assignable to type 'string'.

Yes you need to either any it or narrow it at runtime. The value isn't always a string because its type depends on the children. It may be obvious to you but the compiler doesn't understand it (yet).

what is the best option for me to do at the moment? I know I can fix it by set it to any but I don't like any in my code. What do you mean by narrow it at runtime?

I know I can fix it by set it to any but I don't like any in my code.

Me neither which is why I changed it to unknown. string was wrong before.

What do you mean by narrow it at runtime?

if (typeof value === 'string') {
  // handle string
} else {
  // something unexpected! throw? warn? ignore?
}

This is what unknown is for. A type safe way of dealing with any value.

@zhaoyi0113 The snippet i provided works perfectly fine for me.

@eps1lon I could make the select generic, so you can provide the type, with it defaulting to unknown

@eps1lon I could make the select generic, so you can provide the type, with it defaulting to unknown

Generics in props are very tricky. There was a lot of back and forth with it concerning the component prop which essentially concluded with: TypeScripts support for JSX isn't sufficient.

You can try it but please include some tests with automatic inference and explicit generic argument and interactions between the 麓value麓 prop type and the the onChange value type.

Even if this all works it's still a hidden as cast. TypeScript cannot check if the value types of the children match the value type in the parent as far as I remember. So all you're doing is making it look sound but it isn't. And while it does create more work for collaborators by having to explain why unknown is the better solution I much prefer this over having an API that isn't safe.

Even if this all works it's still a hidden as cast. TypeScript cannot check if the value types of the children match the value type in the parent as far as I remember. So all you're doing is making it look sound but it isn't

Correct, it can't, however, when the user specifically sets the type, should it not be up to them to make sure it matches?

should it not be up to them to make sure it matches?

Then we don't need a type checker or am I misunderstanding something? If they have to make sure that it matches manually why can't they cast to any?

If they have to make sure that it matches manually why can't they cast to any?

I'm not arguing against this, I'm just saying it would be nice to be able to set the type.
If I know my select will only contain string then I would like to set that so the events show the correct type

Or one of the examples from the docs:

<Select<string | number>
  value={state.age}
  onChange={handleChange('age')}
>
  <option value="" />
  <option value={10}>Ten</option>
  <option value={20}>Twenty</option>
  <option value={30}>Thirty</option>
</Select>

As far as I can tell, you need to specify the type of the value, it's defaulted to unknown.

onChange={(e: React.ChangeEvent<{ value: string }>) => onChange(e.target.value)}

@oliviertassinari Can't find this in the migration guide
@zhaoyi0113 @merceyz I tried as "onChange={(event: React.ChangeEvent<{ value: string }>) => selectGroup({ id: event.target.value })}"

and getting 1 error as below:

Error 1:
No overload matches this call.
Overload 1 of 2, '(props: SelectProps, context?: any): ReactElement)> | null) | (new (props: any) => Component<...>)> | Component<...> | null', gave the following error.
Type '(e: ChangeEvent<{ value: string; }>) => void' is not assignable to type '(event: ChangeEvent<{ name?: string | undefined; value: unknown; }>, child: ReactNode) => void'.
Types of parameters 'e' and 'event' are incompatible.
Type 'ChangeEvent<{ name?: string | undefined; value: unknown; }>' is not assignable to type 'ChangeEvent<{ value: string; }>'.
Type '{ name?: string | undefined; value: unknown; }' is not assignable to type '{ value: string; }'.
Types of property 'value' are incompatible.
Type 'unknown' is not assignable to type 'string'.
Overload 2 of 2, '(props: PropsWithChildren, context?: any): ReactElement)> | null) | (new (props: any) => Component<...>)> | Component<...> | null', gave the following error.
Type '(e: ChangeEvent<{ value: string; }>) => void' is not assignable to type '(event: ChangeEvent<{ name?: string | undefined; value: unknown; }>, child: ReactNode) => void'.ts(2769)
SelectInput.d.ts(17, 3): The expected type comes from property 'onChange' which is declared here on type 'IntrinsicAttributes & SelectProps'
SelectInput.d.ts(17, 3): The expected type comes from property 'onChange' which is declared here on type 'IntrinsicAttributes & SelectProps & { children?: ReactNode; }'

Simpler and prettier solution:

use a string casting

onChange={(e) => setLocationCity(String(e.target.value))}

onChange={(e) => seValue(e.target.value as string)}

Was this page helpful?
0 / 5 - 0 ratings