Oct 8, 2019
Use Overload to Type Dynamic Function Signature
Sometimes, you want to make your function return different result depends on the parameter.
Take, for example the following React custom hooks:
ts
export function usePreloadImage(imageSrc: string, eager = true) {/* if it is eager,then set loadIt to true, else make loadIt false */const [loadIt, setLoadIt] = React.useState(eager);React.useEffect(() => {if (imageSrc && loadIt) {// if loadIt is true, load the imageconst image = new Image();image.src = imageSrc;}}, [loadIt, imageSrc]);if (eager) {/* if eager, this hooks should NOT returna function because the image already loaded */return;}/* if not eager, returns a functionthat user can call to start load the image */return () => setLoadIt(true);}
ts
export function usePreloadImage(imageSrc: string, eager = true) {/* if it is eager,then set loadIt to true, else make loadIt false */const [loadIt, setLoadIt] = React.useState(eager);React.useEffect(() => {if (imageSrc && loadIt) {// if loadIt is true, load the imageconst image = new Image();image.src = imageSrc;}}, [loadIt, imageSrc]);if (eager) {/* if eager, this hooks should NOT returna function because the image already loaded */return;}/* if not eager, returns a functionthat user can call to start load the image */return () => setLoadIt(true);}
To use it:
tsx
export const Page = () => {// this image will be loaded when on mountusePreloadImage('http://placecorgi.com/200/200');// this image only load when you hover over the buttonconst loadLargeImage = usePreloadImage('http://placecorgi.com/300/300',false);return (<div><p>Open DevTools (F12) Network Tabs to verify</p><buttononMouseEnter={() => {// typecheck as typescript infer this may be undefined// highlight-next-lineif (loadLargeImage) {loadLargeImage();}}}>Hover Me</button></div>);};
tsx
export const Page = () => {// this image will be loaded when on mountusePreloadImage('http://placecorgi.com/200/200');// this image only load when you hover over the buttonconst loadLargeImage = usePreloadImage('http://placecorgi.com/300/300',false);return (<div><p>Open DevTools (F12) Network Tabs to verify</p><buttononMouseEnter={() => {// typecheck as typescript infer this may be undefined// highlight-next-lineif (loadLargeImage) {loadLargeImage();}}}>Hover Me</button></div>);};
See it in action:
jsx
function usePreloadImage(imageSrc, eager = true) {const [loadIt, setLoadIt] = React.useState(eager);React.useEffect(() => {if (imageSrc && loadIt) {const image = new Image();image.src = imageSrc;}}, [loadIt, imageSrc]);if (eager) {return;}return () => setLoadIt(true);}const Page = () => {// this image will be loaded when on mountusePreloadImage('http://placecorgi.com/200/200');// this image only load when you hover over the buttonconst loadLargeImage = usePreloadImage('http://placecorgi.com/300/300',false);return (<div><p>Open DevTools (F12) Network Tabs to verify</p><buttononMouseEnter={() => {loadLargeImage();}}>Hover Me</button></div>);};const Container = () => {const [key, setKey] = React.useState(0);return (<div><buttononClick={() => setKey(key + 1)}className="btn btn-raised btn-primary">Reload App</button><Page key={key} /></div>);};render(<Container />);
jsx
function usePreloadImage(imageSrc, eager = true) {const [loadIt, setLoadIt] = React.useState(eager);React.useEffect(() => {if (imageSrc && loadIt) {const image = new Image();image.src = imageSrc;}}, [loadIt, imageSrc]);if (eager) {return;}return () => setLoadIt(true);}const Page = () => {// this image will be loaded when on mountusePreloadImage('http://placecorgi.com/200/200');// this image only load when you hover over the buttonconst loadLargeImage = usePreloadImage('http://placecorgi.com/300/300',false);return (<div><p>Open DevTools (F12) Network Tabs to verify</p><buttononMouseEnter={() => {loadLargeImage();}}>Hover Me</button></div>);};const Container = () => {const [key, setKey] = React.useState(0);return (<div><buttononClick={() => setKey(key + 1)}className="btn btn-raised btn-primary">Reload App</button><Page key={key} /></div>);};render(<Container />);
This works, but I dislike that as a user of the custom hook, I need to do a check on the type of the returned callback of the custom hook. We know that when we pass false
as second parameter to the usePreloadImage
hook, it will definitely return a function. Now we do that type checking not because it is necessary, but just to shut TypeScript up.
This is because TypeScript is not that smart to infer that logic; it only sees that there are two possible outcomes of usePreloadImage
function, which is either undefined
or a function.
We need to teach TypeScript to recognize that patterns by overload the function:
ts
// highlight-startexport function usePreloadImage(imageSrc: string, eager?: true): void;export function usePreloadImage(imageSrc: string, eager: false): () => void;// highlight-endexport function usePreloadImage(imageSrc: string, eager = true) {const [loadIt, setLoadIt] = React.useState(eager);React.useEffect(() => {if (imageSrc && loadIt) {const image = new Image();image.src = imageSrc;}}, [loadIt, imageSrc]);if (eager) {return;}return () => setLoadIt(true);}
ts
// highlight-startexport function usePreloadImage(imageSrc: string, eager?: true): void;export function usePreloadImage(imageSrc: string, eager: false): () => void;// highlight-endexport function usePreloadImage(imageSrc: string, eager = true) {const [loadIt, setLoadIt] = React.useState(eager);React.useEffect(() => {if (imageSrc && loadIt) {const image = new Image();image.src = imageSrc;}}, [loadIt, imageSrc]);if (eager) {return;}return () => setLoadIt(true);}
Now TypeScript can infer the returned type correctly.