Skip to content

Commit da85af2

Browse files
arvindcheenuswyxio
andauthored
Troubleshooting : Type an untyped module (#254)
* Troubleshooting : Type an untyped module Added Docs for Exporting types for Untyped libraries. PR for the Issue #245 * Updated Readme for Troubleshooting Updated Readme for Troubleshooting for the issue Typing an untyped library. Addresses issue #245. * Changed to appropriate Language Format * Updated Readme Troubleshooting code formatting * Update types.md * Update README.md * Update docs/basic/troubleshooting/types.md Co-authored-by: swyx <[email protected]> * Update README.md Co-authored-by: swyx <[email protected]> * Update README.md Co-authored-by: swyx <[email protected]> * Update README.md Co-authored-by: swyx <[email protected]> * Typing an untyped library Explained workarounds and possible solutions for typing an untyped React Hook or a React Component with Code Examples. * fix formatting * tweaks * fix typescript spelling Co-authored-by: swyx <[email protected]>
1 parent 221d2b9 commit da85af2

File tree

2 files changed

+189
-1
lines changed

2 files changed

+189
-1
lines changed

Diff for: README.md

+31
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
- [Using Inferred Types](#using-inferred-types)
8989
- [Using Partial Types](#using-partial-types)
9090
- [The Types I need Weren't Exported!](#the-types-i-need-werent-exported)
91+
- [The Types I need Don't Exist!](#the-types-i-need-dont-exist)
9192
- [Troubleshooting Handbook: Operators](#troubleshooting-handbook-operators)
9293
- [Troubleshooting Handbook: Utilties](#troubleshooting-handbook-utilities)
9394
- [Troubleshooting Handbook: tsconfig.json](#troubleshooting-handbook-tsconfigjson)
@@ -1539,6 +1540,36 @@ let baz2: SubIsntType2 = {
15391540
- TS also ships with a `Parameters` utility type for extracting the parameters of a function
15401541
- for anything more "custom", the `infer` keyword is the basic building block for this, but takes a bit of getting used to. Look at the source code for the above utility types, and [this example](https://twitter.com/mgechev/status/1211030455224422401?s=20) to get the idea.
15411542
1543+
## The Types I need don't exist!
1544+
1545+
What's more annoying than modules with unexported types? Modules that are **untyped**!
1546+
1547+
Fret not! It's just a simple two step process.
1548+
1549+
- Create a new type declaration file, say `typedec.d.ts`– if you don't already have one. Ensure that the path to file is resolvable by Typescript. To do so, check the `include` array in the `tsconfig.json` file at the root of your directory.
1550+
1551+
```json
1552+
// inside tsconfig.json
1553+
{
1554+
...
1555+
"include": [
1556+
"src" // automatically resolves if path to declaration is src/typedec.d.ts
1557+
],
1558+
...
1559+
}
1560+
```
1561+
1562+
- Add the `declare` syntax for your desired module, say `my-untyped-module`– to the declaration file.
1563+
1564+
```ts
1565+
// inside typedec.d.ts
1566+
declare module "my-untyped-module";
1567+
```
1568+
1569+
This one-line alone is enough if you just need it to work without errors. If you need to, you can include your type definitions inferred from the untyped module's source or docs within curly braces following this declaration.
1570+
1571+
If you're working with untyped class components in React, you can also checkout this [short post](https://templecoding.com/blog/2016/03/31/creating-typescript-typings-for-existing-react-components) as a reference.
1572+
15421573
# Troubleshooting Handbook: Images and other non-TS/TSX files
15431574
15441575
Use [declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html):

Diff for: docs/basic/troubleshooting/types.md

+158-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ sidebar_label: Types
88
99
Facing weird type errors? You aren't alone. This is the hardest part of using TypeScript with React. Be patient - you are learning a new language after all. However, the more you get good at this, the less time you'll be working _against_ the compiler and the more the compiler will be working _for_ you!
1010

11-
Try to avoid typing with `any` as much as possible to experience the full benefits of typescript. Instead, let's try to be familiar with some of the common strategies to solve these issues.
11+
Try to avoid typing with `any` as much as possible to experience the full benefits of TypeScript. Instead, let's try to be familiar with some of the common strategies to solve these issues.
1212

1313
## Union Types and Type Guarding
1414

@@ -367,3 +367,160 @@ let baz2: SubIsntType2 = {
367367
368368
- TS also ships with a `Parameters` utility type for extracting the parameters of a function
369369
- for anything more "custom", the `infer` keyword is the basic building block for this, but takes a bit of getting used to. Look at the source code for the above utility types, and [this example](https://twitter.com/mgechev/status/1211030455224422401?s=20) to get the idea. Basarat [also has a good video on `infer`](https://www.youtube.com/watch?v=ijK-1R-LFII&list=PLYvdvJlnTOjF6aJsWWAt7kZRJvzw-en8B&index=3&t=0s).
370+
371+
## The Types I need don't exist!
372+
373+
What's more annoying than modules with unexported types? Modules that are **untyped**!
374+
375+
Fret not! There are more than a couple of ways in which you can solve this problem.
376+
377+
A **lazier** way would be to create a new type declaration file, say `typedec.d.ts`– if you don't already have one. Ensure that the path to file is resolvable by TypeScript by checking the `include` array in the `tsconfig.json` file at the root of your directory.
378+
379+
```json
380+
// inside tsconfig.json
381+
{
382+
// ...
383+
"include": [
384+
"src" // automatically resolves if the path to declaration is src/typedec.d.ts
385+
]
386+
// ...
387+
}
388+
```
389+
390+
Within this file, add the `declare` syntax for your desired module, say `my-untyped-module`– to the declaration file:
391+
392+
```ts
393+
// inside typedec.d.ts
394+
declare module "my-untyped-module";
395+
```
396+
397+
This one-liner alone is enough if you just need it to work without errors. A even hackier, write-once-and-forget way would be to use `"*"` instead which would then apply the `Any` type for all existing and future untyped modules.
398+
399+
This solution works well as a workaround if you have less than a couple untyped modules. Anything more, you now have a ticking type-bomb in your hands. The only way of circumventing this problem would be to define the missing types for those untyped modules as explained in the following sections.
400+
401+
### Typing Exported Hooks
402+
403+
Typing Hooks is just like typing pure functions.
404+
405+
The following steps work under two assumptions:
406+
407+
- You have already created a type declaration file as stated earlier in the section.
408+
- You have access to the source code - specifically the code that directly exports the functions you will be using. In most cases, it would be housed in an `index.js` file.
409+
Typically you need a minimum of **two** type declarations (one for **Input Prop** and the other for **Return Prop**) to define a hook completely. Suppose the hook you wish to type follows the following structure,
410+
411+
```js
412+
// ...
413+
const useUntypedHook = (prop) => {
414+
// some processing happens here
415+
return {
416+
/* ReturnProps */
417+
};
418+
};
419+
export default useUntypedHook;
420+
```
421+
422+
then, your type declaration should most likely follow the following syntax.
423+
424+
```ts
425+
declare module 'use-untyped-hook' {
426+
export interface InputProps { ... } // type declaration for prop
427+
export interface ReturnProps { ... } // type declaration for return props
428+
export default function useUntypedHook(
429+
prop: InputProps
430+
// ...
431+
): ReturnProps;
432+
}
433+
```
434+
435+
<details>
436+
<summary>
437+
438+
For instance, the [useDarkMode hook](https://github.com/donavon/use-dark-mode) exports the functions that follows a similar structure.
439+
440+
</summary>
441+
442+
```js
443+
// inside src/index.js
444+
const useDarkMode = (
445+
initialValue = false, // -> input props / config props to be exported
446+
{
447+
// -> input props / config props to be exported
448+
element,
449+
classNameDark,
450+
classNameLight,
451+
onChange,
452+
storageKey = "darkMode",
453+
storageProvider,
454+
global,
455+
} = {}
456+
) => {
457+
// ...
458+
return {
459+
// -> return props to be exported
460+
value: state,
461+
enable: useCallback(() => setState(true), [setState]),
462+
disable: useCallback(() => setState(false), [setState]),
463+
toggle: useCallback(() => setState((current) => !current), [setState]),
464+
};
465+
};
466+
export default useDarkMode;
467+
```
468+
469+
As the comments suggest, exporting these config props and return props following the aforementioned structure will result in the following type export.
470+
471+
```ts
472+
declare module "use-dark-mode" {
473+
/**
474+
* A config object allowing you to specify certain aspects of `useDarkMode`
475+
*/
476+
export interface DarkModeConfig {
477+
classNameDark?: string; // A className to set "dark mode". Default = "dark-mode".
478+
classNameLight?: string; // A className to set "light mode". Default = "light-mode".
479+
element?: HTMLElement; // The element to apply the className. Default = `document.body`
480+
onChange?: (val?: boolean) => void; // Overide the default className handler with a custom callback.
481+
storageKey?: string; // Specify the `localStorage` key. Default = "darkMode". Set to `null` to disable persistent storage.
482+
storageProvider?: WindowLocalStorage; // A storage provider. Default = `localStorage`.
483+
global?: Window; // The global object. Default = `window`.
484+
}
485+
/**
486+
* An object returned from a call to `useDarkMode`.
487+
*/
488+
export interface DarkMode {
489+
readonly value: boolean;
490+
enable: () => void;
491+
disable: () => void;
492+
toggle: () => void;
493+
}
494+
/**
495+
* A custom React Hook to help you implement a "dark mode" component for your application.
496+
*/
497+
export default function useDarkMode(
498+
initialState?: boolean,
499+
config?: DarkModeConfig
500+
): DarkMode;
501+
}
502+
```
503+
504+
</details>
505+
506+
### Typing Exported Components
507+
508+
In case of typing untyped class components, there's almost no difference in approach except for the fact that after declaring the types, you export the extend the type using `class UntypedClassComponent extends React.Component<UntypedClassComponentProps, any> {}` where `UntypedClassComponentProps` holds the type declaration.
509+
510+
For instance, [sw-yx's Gist on React Router 6 types](https://gist.github.com/sw-yx/37a6a3d248c2d4031801f0d568904df8) implemented a similar method for typing the then untyped RR6.
511+
512+
```ts
513+
declare module "react-router-dom" {
514+
import * as React from 'react';
515+
// ...
516+
type NavigateProps<T> = {
517+
to: string | number,
518+
replace?: boolean,
519+
state?: T
520+
}
521+
//...
522+
export class Navigate<T = any> extends React.Component<NavigateProps<T>>{}
523+
// ...
524+
```
525+
526+
For more information on creating type definitions for class components, you can refer to this [post](https://templecoding.com/blog/2016/03/31/creating-typescript-typings-for-existing-react-components) for reference.

0 commit comments

Comments
 (0)