参数类型被推断为“never”

问题描述 投票:0回答:1

这是我的代码:


    import classNames from 'classnames';
    import { uniqueId } from 'lodash';
    import React, { forwardRef, useCallback, useMemo } from 'react';
    
    interface BaseInputProps {
      className?: string;
      placeholder?: string;
      label?: string;
      suffix?: string;
      disabled?: boolean;
      autoFocus?: boolean;
    }
    
    type TextInputProps = {
      type?: 'search' | 'text';
      value?: string;
    } & BaseInputProps &
      OnChangeProps<string>;
    
    type NumberInputProps = {
      type: 'number';
      value?: number;
    } & BaseInputProps &
      OnChangeProps<number>;
    
    type OnChangeProps<T> =
      | {
          isForwarded: true;
          onChange?: (event: Event) => void;
        }
      | {
          isForwarded?: false;
          onChange?: (value: T) => void;
        };
    
    const Input = forwardRef<HTMLInputElement, TextInputProps | NumberInputProps>(
      (
        {
          className,
          isForwarded,
          placeholder,
          label,
          suffix,
          disabled,
          autoFocus,
          ...props
        },
        ref
      ) => {
        const handleOnChange = useCallback(
          (event: React.ChangeEvent<HTMLInputElement> | Event) => {
            if (isForwarded) {
              props.onChange?.(event as Event);
            } else if (props.type === 'number') {
              props.onChange?.(event.target.valueAsNumber);
            } else {
              props.onChange?.(event.target.value);
            }
          },
          [props.onChange, props.type]
        );
    
        const htmlId = useMemo(() => uniqueId('input_'), []);
    
        return (
          <div
            className={classNames(className, 'flex flex-col items-stretch gap-1')}
          >
            {label && (
              <label
                htmlFor={htmlId}
                className="mr-2 select-none font-medium text-sm text-carbon-800"
              >
                {label}
              </label>
            )}
            <div className="flex flex-row items-center h-9 px-2 border-[1.5px] border-gray-200 rounded-md overflow-hidden hover:border-gray-300 focus-within:border-gray-300">
              <input
                id={htmlId}
                ref={ref}
                onChange={handleOnChange}
                type={props.type}
                value={props.value}
                placeholder={placeholder || ''}
                className={classNames(
                  className
                    ?.split(' ')
                    .filter((c) => c.includes('bg-') || c.includes('text-'))
                    .join(' '),
                  'inline-block border-none w-full p-0 text-sm placeholder:text-gray-400 focus:ring-0'
                )}
                disabled={disabled}
                autoFocus={autoFocus}
              />
              {suffix && (
                <span className="text-sm ml-2 text-carbon-600">{suffix}</span>
              )}
            </div>
          </div>
        );
      }
    );
    
    Input.displayName = 'Input';
    
    export default Input;

上面的组件需要事件的类型为

Event
,因此有奇怪的条件类型。 我不明白为什么
props.onChange?.(event as Event);
正在触发以下错误:

  1. “Event”类型的参数不可分配给“never”类型的参数。 [2345]

你有什么线索吗?

reactjs typescript
1个回答
0
投票

您希望通过检查

isForwarded
props.type
,TypeScript 将 narrow
props.onChange
成为一个需要适当参数类型的函数。但这并没有发生;
props.onChange
仍然是函数的 union,因此只能使用参数类型的 intersection 安全地调用(请参阅对调用函数并集的支持),并且由于
string
number
没有共同居民,你得到
never
类型
。哎呀。


forwardRef()
的第一个参数是一个可区分的联合类型,如果你将其保留为这样,则可以检查其区分属性:

const Input = forwardRef<HTMLInputElement, TextInputProps | NumberInputProps>((
  arg, ref) => {
  const handleOnChange = useCallback(
    (event: React.ChangeEvent<HTMLInputElement> | Event) => {
      if (arg.isForwarded) {
        arg.onChange?.(event as Event); // okay
      } else if (arg.type === 'number') {
        arg.onChange?.((event as React.ChangeEvent<HTMLInputElement>).target?.valueAsNumber); // okay
      } else {
        arg.onChange?.((event as React.ChangeEvent<HTMLInputElement>).target?.value); // okay
      }
    },
    [arg.onChange, arg.type]
  );
  

但是你已经将其解构成变量了。再说一次,如果你完全这样做了,它也会起作用,因为 TypeScript 支持缩小解构的可区分联合

const Input = forwardRef<HTMLInputElement, TextInputProps | NumberInputProps>(({
  className, isForwarded, placeholder, label,
  suffix, disabled, autoFocus, onChange,
  type, value
}, ref) => {
  const handleOnChange = useCallback(
    (event: React.ChangeEvent<HTMLInputElement> | Event) => {
      if (isForwarded) {
        onChange?.(event as Event); // okay
      } else if (type === 'number') {
        onChange?.((event as React.ChangeEvent<HTMLInputElement>).target?.valueAsNumber); // okay
      } else {
        onChange?.((event as React.ChangeEvent<HTMLInputElement>).target?.value); // okay
      }
    },
    [onChange, type]
  );

请注意,您仍然可以拥有

...props
剩余属性;它只是不会参与缩小。所以你只需要“完全”解构到缩小的所有参与者都是他们自己的变量即可:

const Input = forwardRef<HTMLInputElement, TextInputProps | NumberInputProps>(({
  className, isForwarded, placeholder, label,
  suffix, disabled, autoFocus, onChange,
  type, ...props // <-- still usable
}, ref) => {
  const handleOnChange = useCallback(
    (event: React.ChangeEvent<HTMLInputElement> | Event) => {
      if (isForwarded) {
        onChange?.(event as Event); // okay
      } else if (type === 'number') {
        onChange?.((event as React.ChangeEvent<HTMLInputElement>).target?.valueAsNumber); // okay
      } else {
        onChange?.((event as React.ChangeEvent<HTMLInputElement>).target?.value); // okay
      }
    },
    [onChange, type]
  );

这两种方法都适合您。


目前不起作用的,是使用rest属性通过partial解构来缩小范围。这是 microsoft/TypeScript#46680 中请求的 TypeScript 缺少的功能。如果实现了这一点,那么您的代码可能会开始工作。但除非发生这种情况,否则您需要以某种方式解决它,例如上述两种方法之一。

Playground 代码链接

© www.soinside.com 2019 - 2024. All rights reserved.