Skip to content

Instantly share code, notes, and snippets.

@bsouthga
Last active September 16, 2018 18:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bsouthga/9941550ab2cd8c42c240f9cc1a718d61 to your computer and use it in GitHub Desktop.
Save bsouthga/9941550ab2cd8c42c240f9cc1a718d61 to your computer and use it in GitHub Desktop.
Chris's Typescript Question

Original Code in Question

link to code

High level picture

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.

My thoughts

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:

  1. make the signatures of all your renderField functions the same (and identical to the Renderer 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>` : '';
  1. Remove the Renderer annoations on renderField and check for the correct type of value in getValue before passing it to renderer.
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);
    }
}
@blaqbern
Copy link

Ahh, I see. So, as far as typescript knows renderField1 is not of type (value: string) => string, because I'm declaring it as type Renderer. And even though it seems like it should be, renderField1 is not really a Renderer because a Renderer 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.

@bsouthga
Copy link
Author

👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment