0

I'm writing a React component that wraps react-select's Select component, which has a prop declaration that starts like this:

export type SelectComponentsProps = { [key in string]: any };

export interface Props<OptionType extends OptionTypeBase = { label: string; value: string }> extends SelectComponentsProps {

I imported this type and had tried to declare the props of my wrapper component like this:

import { Props as SelectProps } from "react-select/src/Select";
type Props = Omit<SelectProps, "inputId"> & { ... some other stuff };

But I had a problem where I could "type-safely" pass anything I wanted to my component, even for fields that had explicit type declarations on SelectProps:

// okay
<MySelectWrapper onChange="definitely not a function"/>

// not okay, fails type checking
<Select onChange="definitely not a function"/>

After some digging, I found that the combination of Omit and an index signature (in the react-select example: SelectComponentsProps) causes the compiler to drop explicitly-specified fields and instead just use the index signature.

interface ArbitraryKeyable {
  foo: number;
  [key: string]: any;
}

const value = {
  foo: 'not a number',
  bar: 'this can be anything'
}

// Fails: foo is the wrong type.
const arbitrary: ArbitraryKeyable = value;

type OmittedArbitraryKeyable = Omit<ArbitraryKeyable, 'this is not a field'>;

// Succeeds, even though the type should functionally be the same.
const omittedArbitrary: OmittedArbitraryKeyable = value;

(playground link)

Here's what the playground thinks that type is:

enter image description here

So of course it accepts everything! Is there a way to define a version of Omit that preserves the explicitly-defined fields? Alternately, is there a type operation I can perform to remove the index signature and only have the explicit fields? (I don't really need it for my use-case, so I'd be okay losing that flexibility to gain type safety elsewhere.)

skelley
  • 475
  • 5
  • 15

1 Answers1

1

Omit in combination with an index signature will not work, because it internally uses the keyof operator. When you apply keyof to a type with an index signature, it will only return the type of the index and not include explicit members:

interface ArbitraryKeyable {
  foo: number;
  [key: string]: any;
}

type Keys = keyof ArbitraryKeyable // type Keys = string | number (no "foo")

And excluding a property "foo" from a string index signature still preserves the index.

type S1 = Omit<ArbitraryKeyable, 'foo'>
type S2 = Pick<ArbitraryKeyable, Exclude<keyof ArbitraryKeyable, "foo">>;
type S3 = Pick<ArbitraryKeyable, Exclude<string| number, "foo">>; 
type S4 = Pick<ArbitraryKeyable, string | number>; // { [x: string]: any; [x: number]: any;}

There is a way to drop the index signature, but feels a bit hackish, if you ask me. You're probably better off by explicitly picking all desired properties, when applicable. That also has the benefit that you can control and narrow the external API better.

// instead of omit...
type Props = Omit<SelectProps, "inputId"> & { ... some other stuff }

// ... pick the props
type Props = Pick<SelectProps, "prop1" | "prop2">
ford04
  • 66,267
  • 20
  • 199
  • 171