I think you ran into a subtle typescript typing bug that was fixed by the introduction of the strictFunctionTypes
compiler option. We didn't turn on this on while I was there as there was a lot of stuff to fix if we did.
I think the issue is that you are declaring your renderers to be of type Renderer
, which allows any type matching FieldValue
as an argument.
Specifically:
// `renderField1: Renderer` means that `renderField1` is considered to be
// of type `Renderer` when used elsewhere. The type declaration of `(value: string) => ...`
// only matters within the context of the function, as the declaration takes precidence.
const renderField1: Renderer = (value: string) =>
`<div>${value}</div>`
This type of annotation is actually an error, but (I think) wasn't caught until Typescript added the strictFunctionTypes
compiler option.
Try turning it on in the options to see the error. The issue is that you really shouldn't be able to assing a function
of type (value: string) => string
to a variable of type (value: string | boolean) => string
, as the former accepts
a more limited type of argument.
I see two solutions:
- make the signatures of all your
renderField
functions the same (and identical to theRenderer
type). For example:
// the type of `value` is inferred by the `Renderer` annotation.
const renderField1: Renderer = value =>
// now that renderField is a generic `Renderer`, we need to check to make sure we have the right value.
typeof value === 'string' ? `<div>${value}</div>` : '';
const renderField2: Renderer = value =>
typeof value === 'boolean' ? (value ? `<div>YES</div>` : `<div>NO</div>`) : '';
const renderField3: Renderer = value =>
typeof value === 'object' ? `<div>${value.whatever}${value.somethingElse}</div>` : '';
- Remove the
Renderer
annoations onrenderField
and check for the correct type ofvalue
ingetValue
before passing it torenderer
.
const renderField1 = (value: string) =>
`<div>${value}</div>`
const renderField2 = (value: boolean) =>
value ? `<div>YES</div>` : `<div>NO</div>`
const renderField3 = (value: ArbitrarilyComplexType) =>
`<div>${value.whatever}${value.somethingElse}</div>`
function getValue(id: FieldId, value: FieldValue) {
switch (true) {
case id === FieldId.FIELD_1 && typeof value === 'string': return renderField1(value);
case id === FieldId.FIELD_2 && typeof value === 'boolean': return renderField2(value);
case id === FieldId.FIELD_3 && typeof value === 'object': return renderField3(value);
}
}
Ahh, I see. So, as far as typescript knows
renderField1
is not of type(value: string) => string
, because I'm declaring it as typeRenderer
. And even though it seems like it should be,renderField1
is not really aRenderer
because aRenderer
allows more than just strings for the param.It's kinda confusing because I tend to think of union types as more permissive, but I overlooked the fact that having a union type for the param of a function, does not make the type of the function more permissive.
In other words:
(param: string | boolean) => void
is not the same as(param: string) => void | (param: boolean) => void
. It seems obvious after writing it out like that, but maybe I can take some solace in the thought that the typescript team seemed to have missed it too. 😜Thanks a lot for the response. That really clears it up for me.