Search code examples
javascripttypescriptmapped-types

How can I map some parameters to be optional?


Context:

I'm trying to write a function that will allow the user of the function to define a certain type using no typescript type assertions (just plain old javascript syntax). Basically, I'm trying to write something like React's PropTypes but I want to map the type defined with these "PropTypes" to a type that can be enforced with typescript.

Maybe it's easier to understand what I'm trying to do by seeing the code. Below defineFunction is a function that returns a function that takes in an object defined by "PropTypes"`.

interface PropType<T> { isOptional: PropType<T | undefined> }
type V<Props> = {[K in keyof Props]: PropType<Props[K]>};
interface PropTypes {
  string: PropType<string>,
  number: PropType<number>,
  bool: PropType<boolean>,
  shape: <R>(definer: (types: PropTypes) => V<R>) => PropType<R>
}

// the only purpose of this function is to capture the type and map it appropriately
// it does do anything else
function defineFunction<Props, R>(props: (types: PropTypes) => V<Props>, func: (prop: Props) => R) {
  return func;
}

// define a function
const myFunction = defineFunction(
  // this is the interface definition that will be mapped
  // to the actual required props of the function
  types => ({
    stringParam: types.string,
    optionalNumberParam: types.number.isOptional,
    objectParam: types.shape(types => ({
      nestedParam: types.string,
    }))
  }),
  // this is the function definition. the type of `props`
  // is the result of the mapped type above
  props => {
    props.objectParam.nestedParam
    return props.stringParam;
  }
);

// use function
myFunction({
  stringParam: '',
  optionalNumberParam: 0,
  objectParam: {
    nestedParam: ''
  }
});

Here is the resulting type of myFunction found by hovering over the type in VS Code:

const myFunction: (prop: {
    stringParam: string;
    optionalNumberParam: number | undefined;
    objectParam: {
        nestedParam: string;
    };
}) => string

Question:

There is an issue with the code above--optionalNumberParam is correctly defined as number | undefined but it is not actually optional!

If I omit the optionalNumberParam, the typescript compiler will yell at me.

enter image description here

Is there anyway to assert that a type is optional instead of just T | undefined?


Replying to cale_b's comment:

Just a thought - have you tried optionalNumberParam?: types.number.isOptional

Yes and it's invalid syntax:

enter image description here

And to clarify, this defineFunction should let the user define a type using no typescript type assertions--instead everything should be inferred using mapped types. The ? is typescript only. I'm trying to write this function so that--theoretically--javascript users could define proptypes and still have the typescript compiler enforce those proptypes.


Solution

  • So, closest workaround I can do involves that two-object-literal solution I mentioned:

    interface PropType<T> { type: T }
    

    First I removed isOptional since it does you no good, and second I added a property with T in it since TypeScript can't necessarily tell the difference between PropType<T> and PropType<U> if T and U differ, unless they differ structurally. I don't think you need to use the type property though.

    Then some stuff I didn't touch:

    type V<Props> = {[K in keyof Props]: PropType<Props[K]>};
    interface PropTypes {
      string: PropType<string>,
      number: PropType<number>,
      bool: PropType<boolean>,
      shape: <R>(definer: (types: PropTypes) => V<R>) => PropType<R>
    }
    function defineFunction<Props, R>(props: (types: PropTypes) => V<Props>, func: (prop: Props) => R) {
      return func;
    }
    

    Now, I'm creating the function withPartial, which takes two parameters of types R and O and returns a value of type R & Partial<O>.

    function withPartial<R, O>(required: R, optional: O): R & Partial<O> {
      return Object.assign({}, required, optional);
    }
    

    Let's try it out:

    const myFunction = defineFunction(
      types => withPartial(
        {
          stringParam: types.string,
          objectParam: types.shape(types => ({
            nestedParam: types.string,
          }))
        },
        {
          optionalNumberParam: types.number
        }
      ),
      props => {
        props.objectParam.nestedParam
        return props.stringParam;
      }
    );
    

    Note how I split the original object literal into two: one with required properties, and the other with optional ones, and recombine them using the withPartial function. Also note how the user of withPartial() doesn't need to use any TypeScript-specific notation, which is I think one of your requirements.

    Inspecting the type of myFunction gives you:

    const myFunction: (
      prop: {
        stringParam: string;
        objectParam: {
          nestedParam: string;
        };
        optionalNumberParam?: number;
      }
    ) => string
    

    which is what you want. Observe:

    // use function
    myFunction({
      stringParam: '',
      //optionalNumberParam: 0,  // no error
      objectParam: {
        nestedParam: ''
      }
    });
    

    Hope that helps; good luck!