Search code examples
reactjstypescriptgenerics

Allow typescript arg of Partial type to contain none of the optional fields


I am trying to define an input type to a function that may contain fields defined by a type Foo.

[EDIT] -- a previous version of this question, retained below, used generics. The question seems to be able to be simplified without using generics to just focus on a Partial input that may not overlap.

interface Foo {
  foo: string;
}

function baz(x: Partial<Foo>) {
  return x;
}

// ok:
const args1 = { foo: '1', bar: 1 };
baz(args1);

// error: Type '{ bar: number; }' has no properties in common with type 'Partial<Foo>'
const args2 = { bar: 1 };
baz(args2);

Whatever else x has, I want to specify that it may have foo as defined in interface Foo, but it may not, and that's ok.

In my real-world example, it's a few different fields, not just the one. Specifically, I'm writing a function that takes some React props for an arbitrary component that may include className, style and a few others, as defined by a specific interface. The function merges these props with required props for a child component to return the final set of modified props for the child component.

function mergeDraggableProps(
  draggableProps: DraggableChildProps,
  childProps: Partial<DraggableChildProps>
) {
  return {
    ...childProps,
    className: cn(draggableProps.className, childProps.className),
    style: {
      ...draggableProps.style,
      ...childProps.style,
    },
    transform: draggableProps.transform,
    onMouseDown: draggableProps.onMouseDown,
    onMouseUp: draggableProps.onMouseUp,
    onTouchEnd: draggableProps.onTouchEnd,
  };
}

I can call this function with desired props that include className, but as soon as I remove it TypeScript errors for the same reason:

... has no properties in common with type 'Partial<DraggableChildProps>'.

How can I make it valid even if childProps contains none of the fields of DraggableChildProps? (In the simplified example, if x contains none of the fields of Foo?)



Original with generics

The following is the original question, retained for reference, which included generics that may not be necessary to ask the question (or indeed perhaps for my solution)

interface Foo {
  foo: string;
}

function baz<T extends Partial<Foo>>(x: T) {
  return x;
}

// ok:
const args1 = { foo: '1', bar: 1 };
baz(args1);

// error: Type '{ bar: number; }' has no properties in common with type 'Partial<Foo>'
const args2 = { bar: 1 };
baz(args2);
function mergeDraggableProps<T extends Partial<DraggableChildProps>>(
  draggableProps: DraggableChildProps,
  childProps: T
): T & DraggableChildProps {
  return {
    ...childProps,
    className: cn(draggableProps.className, childProps.className),
    style: {
      ...draggableProps.style,
      ...childProps.style,
    },
    transform: draggableProps.transform,
    onMouseDown: draggableProps.onMouseDown,
    onMouseUp: draggableProps.onMouseUp,
    onTouchEnd: draggableProps.onTouchEnd,
  };
}

How can I make it valid even if childProps contains none of the fields of DraggableChildProps? (In the simplified example, if x contains none of the fields of Foo?)


Solution

  • This is due to typescript's excess type checking, the args2 object has no property in common with interface Foo. Solution is to extend your argument type with intersection:

    interface Foo {
      foo: string;
    }
    
    function baz(x: Partial<Foo> & { [x: string]: any }) { 
    // an alternative to { [x: string]: any } is to use Record<string, any> 
      return x;
    }
    
    const args1 = { foo: '1', bar: 1 };
    baz(args1);
    
    // ok
    const args2 = { bar: 1 };
    baz(args2);