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
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.
T | undefined
?Replying to cale_b
's comment:
Just a thought - have you tried optionalNumberParam?: types.number.isOptional
Yes and it's invalid syntax:
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.
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!