Typescript: Exclude properties provided in a Partial

Viewed 953

I've written a function called withDefaults that takes a React component (a function really) and provides an object with default parameter values. It returns a function that that invokes the original React component (function) and merges the default parameters with the final set of parameters.

How can I make required parameters no longer required when they're provided in the default set?

function withDefaultProps<C extends React.ElementType>(Component: c) {
  const ResultComponent = Component as React.FunctionComponent;
  return (
    (defaultProps: Partial<React.ComponentProps<C>>) => (
      (finalProps: React.ComponentProps<C>) => (
        <ResultComponent {...defaultProps} {...finalProps}>
          {finalProps.children}
        </ResultComponent>
      )
    )
  )
}

So for example I have a component that requires a and b as properties.

type MyCompProps = {
  a: string,
  b: string
};

function MyComp(props: MyCompProps) {
  return null; // My react structure here
}

const MyPrefilledComp = withDefaultProps(MyComp)({ a: 'alwaysThisString' });

Here's where the Typescript error shows up. When I try to use MyPrefilledComp it still requires that I provide an a property even though its got a default value. How can I adjust my withDefaulProps method to make it optional, without making ALL the properties optional. I've tried Diff and Omit with keyOf but my Typescript is very beginner level so I may be doing those wrong.

// Typescript error: Property a is required
function MyApp() {
  return (
    <MyPrefilledComp b="a non default value" />
  )
}

I've attempted to simplify it and can easily see where the issue is, I'm just not sure how to fix it. Something like: Omit<OriginalProps, Partial<OriginalProps>> but with that partial being given a dynamic type based on the invocation.

function withDefaults(defaultProps: Partial<OriginalProps>) {
  return function wrapper(finalProps: OriginalProps) {
    return {
      ...defaultProps,
      ...finalProps
    };
  }
}
2 Answers

I'd be inclined to write your withDefaults() function at the end this way:

const makeWithDefaults = <T,>() =>
    <K extends keyof T>(defaultProps: Pick<T, K>) =>
        (finalProps: Partial<Pick<T, K>> & Omit<T, K>) =>
            ({ ...defaultProps, ...finalProps }) as T;

type MyCompProps = {
    a: string,
    b: string
};

const withDefaults = makeWithDefaults<MyCompProps>();

Notice that makeWithDefaults() doesn't do much at runtime except return a function, but at compile time you can use the generic parameter T to specify the actual object type you're trying to build.

The function it returns uses the utility types Pick<T, K>, Partial<T>, and Omit<T, K>.

Once T is specified, the withDefaults() function will accept a defaultProps object with any subset of the properties from T, and will infer K to be the keys of this defaultProps object. The returned function accepts a finalProps of type Partial<Pick<T, K>> & Omit<T, K>. The Omit<T, K> part says that finalProps must have all the properties from T which are missing from defaultProps, and the Partial<Pick<T, K>> part says that finalProps may have any of the properties from T which were included in defaultProps. And the return type of this function is asserted to be the original T (this should be true, but the compiler can't verify it).


Let's see if it works:

const hasDefaultA = withDefaults({ a: "hello" });

const foo = hasDefaultA({ b: "works" });
console.log(JSON.stringify(foo)); // {"a":"hello","b":"works"}
const bar = hasDefaultA({}); // error! property b missing
const baz = hasDefaultA({ a: "goodbye", b: "works" });
console.log(JSON.stringify(baz)); // { "a": "goodbye", "b": "works" }

Looks good. I don't have specific reactjs experience so I'll defer to others about how to make your withDefaultProps() version work.

Playground link to code

I'll share my solution as well

You got it right with Omit, but to actually use it you will need to infer passed default props type

function withDefaultProps<C extends React.ElementType>(Component: C) {
  const ResultComponent = Component as React.FunctionComponent;

  // we return generic function, that accepts Partial component props as argument
  return <D extends Partial<React.ComponentProps<C>>>(defaultProps: D) => (
    // now we have access to D keys and can exclude them from final props with Omit
    finalProps: Omit<React.ComponentProps<C>, keyof D> & {
      // but it needs a little workaround, because it will not have children prop otherwise
      children: React.ReactNode;
    }
  ) => (
    <ResultComponent {...defaultProps} {...finalProps}>
      {finalProps.children}
    </ResultComponent>
  );
}

Usage example: Codesandbox link

Related