Skip to content

Commit 4584408

Browse files
authored
feat(ts): properly type getComponent and hasComponent of ICatalog (#52)
1 parent ae69468 commit 4584408

17 files changed

Lines changed: 196 additions & 53 deletions

File tree

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
src/**/*.d.ts

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ module.exports = {
3939
'react-hooks/exhaustive-deps': 'error',
4040

4141
// typescript settings
42+
'@typescript-eslint/explicit-function-return-type': 0,
4243
'@typescript-eslint/no-explicit-any': 0,
4344
'@typescript-eslint/no-unused-vars': [
4445
'error',

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,7 @@ dist
1919
es
2020
esm
2121
lib
22-
*.tgz
22+
*.tgz
23+
24+
# typescript declaration files generated by tsc in src
25+
src/**/*.d.ts

.travis.yml

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
language: node_js
22

3+
env:
4+
global:
5+
- YARN_VERSION="1.21.1"
6+
37
node_js:
48
- "node"
59

10+
before_install:
11+
- curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version $YARN_VERSION
12+
- export PATH="$HOME/.yarn/bin:$PATH"
13+
614
install:
7-
- npm install
15+
- yarn
816

917
script:
10-
- npm test --silent
11-
- npm run build
12-
- npm run size
18+
- yarn test
19+
- yarn build
20+
- yarn size
1321

1422
notifications:
1523
email:
@@ -18,6 +26,7 @@ notifications:
1826
after_success: "npm run coveralls"
1927

2028
cache:
29+
yarn: true
2130
directories:
2231
- ~/.npm # cache npm's cache
2332
- ~/npm # cache latest npm

example/client/base/components/button/index.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1-
import React from 'react'
1+
/* eslint-disable no-console */
2+
import React, { FunctionComponent } from 'react'
23

3-
const Button = () => (
4+
type ButtonProps = {
5+
text?: string
6+
}
7+
8+
const Button: FunctionComponent<ButtonProps> = ({ text = 'Hey it is me' }) => (
49
<button type="button" onClick={() => console.log('Hey :)')}>
5-
Hey, it is me!
10+
{text}
611
</button>
712
)
813

example/client/client1/components/app/index.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
import React, { FunctionComponent } from 'react'
22
import CatalogComponent, { useCatalog } from 'react-component-catalog'
3+
import { catalog as outerCatalog, innerCatalog } from '../../catalog'
4+
5+
type Catalog = typeof innerCatalog & typeof outerCatalog
6+
37
const FallbackComponent: FunctionComponent = () => (
48
<div>Component not found</div>
59
)
610

711
const App: FunctionComponent = () => {
8-
const catalog = useCatalog()
9-
const Button = catalog.getComponent('Button')
12+
const catalog = useCatalog<Catalog>()
13+
const hasButton = catalog.hasComponent('Button')
14+
15+
let Button
16+
if (hasButton) {
17+
Button = catalog.getComponent('Button')
18+
// this would work too:
19+
// const Button = catalog.getComponent(['Button'])
20+
}
1021

1122
// or you use them with the <CatalogComponent /> component
1223
return (

jest.config.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
module.exports = {
2-
collectCoverageFrom: ['src/**/*.ts'],
2+
collectCoverageFrom: [
3+
'src/**/*.{js,jsx,ts,tsx}',
4+
'!src/**/*.dt.ts',
5+
'!src/**/(__mocks__|__stories__|__tests__)/*.{js,jsx,ts,tsx}',
6+
],
37
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
8+
testMatch: [
9+
'<rootDir>/src/**/__tests__/*.test.ts',
10+
'<rootDir>/src/**/__tests__/*.test.tsx',
11+
],
412
testPathIgnorePatterns: ['<rootDir>/(dist|es|esm|lib|node_modules)/'],
513
transform: {
614
'^.+\\.(t|j)sx?$': 'ts-jest',

package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
"lib"
2424
],
2525
"scripts": {
26-
"build": " npm run build-types && npm run build-cjs && npm run build-es && npm run build-esm && npm run build-umd",
27-
"build-types": "npm run build-cjs-types && npm run build-es-types && npm run build-esm-types",
26+
"build": " yarn run build-types && yarn run build-cjs && yarn run build-es && yarn run build-esm && yarn run build-umd",
27+
"build-types": "yarn run build-cjs-types && yarn run build-es-types && yarn run build-esm-types",
2828
"build-cjs": "BABEL_ENV=cjs babel src --ignore **/*.test.tsx,**/*.test.ts --out-dir lib --extensions '.ts,.tsx'",
2929
"build-cjs-types": "tsc --outDir lib --module commonjs --target es5 -d --emitDeclarationOnly",
3030
"build-es": "BABEL_ENV=es babel src --ignore **/*.test.tsx,**/*.test.ts --out-dir es --extensions '.ts,.tsx'",
@@ -36,11 +36,11 @@
3636
"coveralls": "jest --coverage && cat ./coverage/lcov.info | coveralls",
3737
"lint": "eslint --cache 'src/**/*.{ts,tsx}' --quiet",
3838
"prebuild": "rimraf dist && rimraf es && rimraf esm && rimraf lib",
39-
"prepublishOnly": "npm run prerelease",
40-
"prerelease": "npm run build && npm run size && npm run test",
39+
"prepublishOnly": "yarn prerelease",
40+
"prerelease": "yarn build && yarn size && yarn test",
4141
"release": "HUSKY_SKIP_HOOKS=1 standard-version",
4242
"size": "size-limit",
43-
"test": "jest && npm run test-types",
43+
"test": "jest --detectOpenHandles && yarn test-types",
4444
"test-types": "tsc --noEmit",
4545
"watch": "BABEL_ENV=esm babel --watch src --ignore **/@types/*,**/*.test.tsx,**/*.test.ts --out-dir esm --extensions '.ts,.tsx'",
4646
"watch-test": "jest --watch"

src/catalog.ts

Lines changed: 95 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,111 @@
1-
import { get } from './utils'
1+
import { get, PropertyPath } from './utils'
22
import { CatalogComponents } from './types'
33

4-
export interface ICatalog<T extends CatalogComponents = CatalogComponents> {
4+
export interface ICatalog<T extends CatalogComponents> {
55
// contains the raw catalog
6-
_catalog: T | Record<string, any>
7-
// get a component by id, if not available it will return null
8-
getComponent: (component: string) => any
9-
// validates if the given component exists in the catalog
10-
hasComponent: (component: string) => boolean
6+
_catalog: T
7+
/**
8+
* Get a Component by it's path in the given catalog. If it is not available,
9+
* getComponent will return undefined
10+
*
11+
* @example
12+
* ```
13+
* const Button = catalog.getComponent('button');
14+
* const Button = catalog.getComponent(['button']);
15+
*
16+
* // or, if the components in the catalog are nested
17+
* const Button = catalog.getComponent("common.button")
18+
* const Button = catalog.getComponent(["common", "button"])
19+
* ```
20+
*
21+
* types are inspired by
22+
* @see https://github.com/DefinitelyTyped/DefinitelyTyped/blob/7caeca4bfbd5ca9f306c14def3dd6b416869c615/types/lodash/common/object.d.ts#L1669
23+
* @see https://codewithstyle.info/Deep-property-access-in-TypeScript/
24+
*
25+
* Playground
26+
* @see https://nttr.st/2xu77eG
27+
*
28+
* Additional references:
29+
* @see https://github.com/pirix-gh/ts-toolbelt
30+
* @see https://stackoverflow.com/q/47256723/1238150
31+
* @see https://stackoverflow.com/a/58436959/1238150
32+
*/
33+
getComponent<TKey extends keyof NonNullable<T>>(
34+
component: TKey | [TKey],
35+
): NonNullable<T>[TKey] | undefined
36+
getComponent<
37+
TKey1 extends keyof NonNullable<T>,
38+
TKey2 extends keyof NonNullable<T>[TKey1]
39+
>(
40+
component: [TKey1, TKey2],
41+
): NonNullable<T>[TKey1][TKey2] | undefined
42+
getComponent<
43+
TKey1 extends keyof NonNullable<T>,
44+
TKey2 extends keyof NonNullable<T>[TKey1],
45+
TKey3 extends keyof NonNullable<T>[TKey1][TKey2]
46+
>(
47+
component: [TKey1, TKey2, TKey3],
48+
): NonNullable<T>[TKey1][TKey2][TKey3] | undefined
49+
getComponent<
50+
TKey1 extends keyof NonNullable<T>,
51+
TKey2 extends keyof NonNullable<T>[TKey1],
52+
TKey3 extends keyof NonNullable<T>[TKey1][TKey2],
53+
TKey4 extends keyof NonNullable<T>[TKey1][TKey2][TKey3]
54+
>(
55+
component: [TKey1, TKey2, TKey3, TKey4],
56+
): NonNullable<T>[TKey1][TKey2][TKey3][TKey4] | undefined
57+
getComponent(component: PropertyPath): any
58+
/**
59+
* validates if the given component exists in the catalog
60+
*
61+
* @example
62+
* ```
63+
* const hasButton = catalog.hasComponent('button');
64+
* const hasButton = catalog.hasComponent(['button']);
65+
*
66+
* // or, if the components in the catalog are nested
67+
* const hasButton = catalog.hasComponent("common.button")
68+
* const hasButton = catalog.hasComponent(["common", "button"])
69+
* ```
70+
*/
71+
hasComponent<TKey extends keyof NonNullable<T>>(
72+
component: TKey | [TKey],
73+
): boolean
74+
hasComponent<
75+
TKey1 extends keyof NonNullable<T>,
76+
TKey2 extends keyof NonNullable<T>[TKey1]
77+
>(
78+
component: [TKey1, TKey2],
79+
): boolean
80+
hasComponent<
81+
TKey1 extends keyof NonNullable<T>,
82+
TKey2 extends keyof NonNullable<T>[TKey1],
83+
TKey3 extends keyof NonNullable<T>[TKey1][TKey2]
84+
>(
85+
component: [TKey1, TKey2, TKey3],
86+
): boolean
87+
hasComponent<
88+
TKey1 extends keyof NonNullable<T>,
89+
TKey2 extends keyof NonNullable<T>[TKey1],
90+
TKey3 extends keyof NonNullable<T>[TKey1][TKey2],
91+
TKey4 extends keyof NonNullable<T>[TKey1][TKey2][TKey3]
92+
>(
93+
component: [TKey1, TKey2, TKey3, TKey4],
94+
): boolean
95+
hasComponent(component: PropertyPath): boolean
1196
}
1297

13-
export class Catalog<T extends CatalogComponents = CatalogComponents>
14-
implements ICatalog<T> {
98+
export class Catalog<T extends CatalogComponents> implements ICatalog<T> {
1599
public _catalog: T
16100

17101
constructor(catalog: T) {
18102
this._catalog = catalog
19103
}
20104

21-
public getComponent: ICatalog<T>['getComponent'] = component =>
105+
public getComponent: ICatalog<T>['getComponent'] = (component: any) =>
22106
get(this._catalog, component)
23107

24-
public hasComponent: ICatalog<T>['hasComponent'] = component =>
108+
public hasComponent: ICatalog<T>['hasComponent'] = (component: any) =>
25109
!!get(this._catalog, component)
26110
}
27111

src/components/__tests__/catalog-component.test.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,28 @@
11
/* eslint-disable react/no-multi-comp */
2-
import React from 'react'
2+
import React, { FunctionComponent } from 'react'
33
import { mount } from 'enzyme'
44

55
import Catalog from '../../catalog'
66
import CatalogComponent from '../catalog-component'
77
import CatalogProvider from '../catalog-provider'
88
import { withCatalog } from '../with-catalog'
99

10-
const TestComponent = () => <div>Hello World</div>
11-
const BaseArticle = () => <div>Hello BaseArticle</div>
10+
const TestComponent: FunctionComponent = () => <div>Hello World</div>
11+
const BaseArticle: FunctionComponent = () => <div>Hello BaseArticle</div>
1212

13-
const FallbackComponent = () => <div>Fallback</div>
14-
const FallbackFromCatalog = () => <div>FallbackFromCatalog</div>
13+
const FallbackComponent: FunctionComponent = () => <div>Fallback</div>
14+
const FallbackFromCatalog: FunctionComponent = () => (
15+
<div>FallbackFromCatalog</div>
16+
)
17+
18+
type TestCatalog = {
19+
[name: string]: JSX.Element | FunctionComponent | TestCatalog
20+
}
1521

1622
describe('CatalogComponent', () => {
1723
let backupError: () => void
18-
let testCatalog: {}
19-
let emptyTestCatalog: {}
24+
let testCatalog: TestCatalog = null
25+
let emptyTestCatalog: TestCatalog = null
2026

2127
const components = {
2228
FallbackFromCatalog,

0 commit comments

Comments
 (0)