Browse Source

Convert to typescript linting and type our TS!

Andrew Swistak 6 năm trước cách đây
mục cha
commit
f5abc3a1ec

+ 17 - 2
.eslintrc.js

@@ -4,7 +4,13 @@ module.exports = {
     es6: true,
     jest: true,
   },
-  extends: ['eslint:recommended', 'plugin:react/recommended', 'plugin:prettier/recommended'],
+  extends: [
+    'eslint:recommended',
+    'plugin:react/recommended',
+    'plugin:@typescript-eslint/recommended',
+    'prettier',
+    'prettier/@typescript-eslint',
+  ],
   settings: {
     react: {
       version: 'detect',
@@ -18,15 +24,23 @@ module.exports = {
     mount: true,
     global: true,
   },
-  parser: 'babel-eslint',
+  parser: '@typescript-eslint/parser',
   parserOptions: {
     ecmaFeatures: {
       jsx: true,
     },
     ecmaVersion: 2018,
     sourceType: 'module',
+    parserOptions: {
+      project: './tsconfig.json',
+    },
   },
+  plugins: ['@typescript-eslint'],
   rules: {
+    // TypeScript Rules
+    '@typescript-eslint/no-explicit-any': 0,
+
+    // JavaScript Rules
     'arrow-body-style': ['warn', 'as-needed'],
     'arrow-parens': ['warn', 'as-needed'],
     'arrow-spacing': 'error',
@@ -68,6 +82,7 @@ module.exports = {
     'space-infix-ops': ['error', {int32Hint: false}],
     'space-unary-ops': [2, {words: true, nonwords: false}],
 
+    // React Rules
     'react/jsx-filename-extension': ['error', {extensions: ['.jsx', '.tsx']}],
     'react/jsx-indent': [1, 2],
     'react/jsx-key': 1,

+ 2 - 3
app/javascript/packs/frontend/api.ts

@@ -12,10 +12,9 @@ export const instance = axios.create({
 });
 
 export const Pokemon = {
-  index: () => instance.get('/pokemon'),
-  get: (id: number | string) => instance.get(`/pokemon/${id}`),
+  index: (): any => instance.get('/pokemon'),
+  get: (id: number | string): any => instance.get(`/pokemon/${id}`),
 };
-
 const API = {
   Pokemon,
 };

+ 4 - 4
app/javascript/packs/frontend/app.tsx

@@ -4,13 +4,13 @@ import {hot} from 'react-hot-loader/root';
 
 import ApplicationLayout from './components/layout/application_layout';
 
-const Pokemon = lazy(() => import('./components/pages/pokemon'));
-const NotFound = lazy(() => import('./components/pages/not_found'));
-const TestComponent = lazy(() => import('./components/test_component'));
+const Pokemon = lazy((): Promise<any> => import('./components/pages/pokemon'));
+const NotFound = lazy((): Promise<any> => import('./components/pages/not_found'));
+const TestComponent = lazy((): Promise<any> => import('./components/test_component'));
 
 import './assets/stylesheets/app.scss';
 
-function App() {
+function App(): React.ReactElement {
   return (
     <BrowserRouter>
       <ApplicationLayout>

+ 2 - 2
app/javascript/packs/frontend/components/layout/application_layout.tsx

@@ -6,10 +6,10 @@ import {Link} from 'react-router-dom';
 import './style';
 
 interface Props {
-  children?: any;
+  children?: React.ReactElement;
 }
 
-function ApplicationLayout({children}: Props) {
+function ApplicationLayout({children}: Props): React.ReactElement {
   return (
     <div>
       <nav>

+ 1 - 1
app/javascript/packs/frontend/components/pages/not_found.tsx

@@ -1,6 +1,6 @@
 import React from 'react';
 
-function NotFound() {
+function NotFound(): React.FunctionComponentElement<void> {
   return <>Page was not found.</>;
 }
 

+ 4 - 4
app/javascript/packs/frontend/components/pages/pokemon.tsx

@@ -1,13 +1,13 @@
 import React, {lazy, Suspense} from 'react';
 import PropTypes from 'prop-types';
-import {Route, Switch} from 'react-router-dom';
+import {Route, Switch, RouteComponentProps} from 'react-router-dom';
 
-const PokemonShow = lazy(() => import('./pokemon/show'));
-const PokemonIndex = lazy(() => import('./pokemon/index'));
+const PokemonShow = lazy((): Promise<any> => import('./pokemon/show'));
+const PokemonIndex = lazy((): Promise<any> => import('./pokemon/index'));
 
 import NotFound from './not_found';
 
-function Pokemon({match}) {
+function Pokemon({match}: RouteComponentProps<void>): React.FunctionComponentElement<void> {
   return (
     <>
       <Suspense fallback={<div>Loading...</div>}>

+ 20 - 9
app/javascript/packs/frontend/components/pages/pokemon/index.tsx

@@ -3,12 +3,21 @@ import {Link} from 'react-router-dom';
 
 import {Pokemon} from '../../../api';
 
-class PokemonIndex extends React.Component {
-  state = {
+interface Pokemon {
+  id: number;
+  nickname: string;
+}
+
+interface State {
+  pokemon: Pokemon[];
+}
+
+class PokemonIndex extends React.Component<void, State> {
+  public state = {
     pokemon: [],
   };
 
-  async componentDidMount() {
+  public async componentDidMount(): Promise<any> {
     try {
       const data = await Pokemon.index();
       this.setState({pokemon: data.data});
@@ -18,12 +27,14 @@ class PokemonIndex extends React.Component {
     }
   }
 
-  render() {
-    const pokemon = this.state.pokemon.map(pkmn => (
-      <li key={pkmn.id}>
-        <Link to={`/pokemon/${pkmn.id}`}>{pkmn.nickname}</Link>
-      </li>
-    ));
+  public render(): JSX.Element {
+    const pokemon: JSX.Element[] = this.state.pokemon.map(
+      (pkmn): JSX.Element => (
+        <li key={pkmn.id}>
+          <Link to={`/pokemon/${pkmn.id}`}>{pkmn.nickname}</Link>
+        </li>
+      )
+    );
 
     return <ul>{pokemon}</ul>;
   }

+ 11 - 9
app/javascript/packs/frontend/components/pages/pokemon/show.tsx

@@ -1,29 +1,31 @@
 import * as React from 'react';
-import {RouteComponentProps} from 'react-router';
+import {RouteComponentProps} from 'react-router'; //eslint-disable-line no-unused-vars
 import {Link} from 'react-router-dom';
 
 import {Pokemon} from '../../../api';
 
-export interface IPokemon {
+interface Pokemon {
   id: number;
   nickname: string;
 }
 
-interface State {
-  pokemon: IPokemon;
+interface PassedRouteProps {
+  id?: string;
 }
 
-interface Props extends RouteComponentProps<any> {}
+interface State {
+  pokemon: Pokemon;
+}
 
-class PokemonShow extends React.Component<Props, State> {
-  state = {
+class PokemonShow extends React.Component<RouteComponentProps<PassedRouteProps>, State> {
+  public state = {
     pokemon: {
       id: null,
       nickname: null,
     },
   };
 
-  async componentDidMount() {
+  public async componentDidMount(): Promise<any> {
     try {
       const data = await Pokemon.get(this.props.match.params.id);
       this.setState({pokemon: data.data});
@@ -33,7 +35,7 @@ class PokemonShow extends React.Component<Props, State> {
     }
   }
 
-  render(): JSX.Element {
+  public render(): JSX.Element {
     return (
       <>
         {this.state.pokemon.id}: {this.state.pokemon.nickname}

+ 2 - 2
app/javascript/packs/frontend/components/test_component.tsx

@@ -1,6 +1,6 @@
 import React, {useState} from 'react';
 
-function TestComponent() {
+function TestComponent(): React.FunctionComponentElement<void> {
   const [text, setText] = useState('');
 
   return (
@@ -10,7 +10,7 @@ function TestComponent() {
       </div>
       Is React working? Test here: {text}
       <hr />
-      <input onChange={e => setText(e.target.value)} />
+      <input onChange={(e): void => setText(e.target.value)} />
     </div>
   );
 }

+ 1 - 0
package.json

@@ -57,6 +57,7 @@
     "@types/react-dom": "^16.8.4",
     "@types/react-router": "^4.4.5",
     "@types/react-router-dom": "^4.3.2",
+    "@typescript-eslint/eslint-plugin": "^1.7.0",
     "@typescript-eslint/parser": "^1.7.0",
     "axios-mock-adapter": "^1.16.0",
     "babel-core": "^7.0.0-bridge.0",

+ 11 - 9
spec/javascript/frontend/api.test.ts

@@ -2,15 +2,17 @@ import API, {instance} from 'packs/frontend/api';
 
 import MockAdapter from 'axios-mock-adapter';
 
-describe('API', () => {
+describe('API', (): void => {
   const mock = new MockAdapter(instance);
-  afterAll(() => {
-    mock.restore();
-  });
+  afterAll(
+    (): void => {
+      mock.restore();
+    }
+  );
 
-  describe('API.Pokemon', () => {
-    describe('index()', () => {
-      it('returns the response', async () => {
+  describe('API.Pokemon', (): void => {
+    describe('index()', (): void => {
+      it('returns the response', async (): Promise<any> => {
         const expected = {foo: 'bar'};
         mock.onGet('/api/v1/pokemon').reply(200, expected);
 
@@ -19,8 +21,8 @@ describe('API', () => {
       });
     });
 
-    describe('get(id)', () => {
-      it('returns the response', async () => {
+    describe('get(id)', (): void => {
+      it('returns the response', async (): Promise<any> => {
         const expected = {id: 1, name: 'pukuchu'};
         mock.onGet(`/api/v1/pokemon/${expected.id}`).reply(200, expected);
 

+ 19 - 15
spec/javascript/frontend/components/pages/pokemon/show.test.tsx

@@ -1,16 +1,16 @@
 import * as React from 'react';
-import {shallow, mount} from 'enzyme';
+import {shallow, mount, ReactWrapper, ShallowWrapper} from 'enzyme';
 
 import PokemonShow from 'packs/frontend/components/pages/pokemon/show';
 import {instance} from 'packs/frontend/api';
 
-import {Route, StaticRouter} from 'react-router-dom';
+import {StaticRouter} from 'react-router-dom';
 import MockAdapter from 'axios-mock-adapter';
 
 const defaultProps = {
   match: {
     params: {
-      id: 1,
+      id: '1',
     },
     url: '/pokemon/show/1',
     path: '',
@@ -21,11 +21,11 @@ const defaultProps = {
   pokemon: {},
 };
 
-function setup(props = defaultProps) {
+function setup(props = defaultProps): ShallowWrapper {
   return shallow(<PokemonShow {...props} />);
 }
 
-function mountSetup(props = defaultProps) {
+function mountSetup(props = defaultProps): ReactWrapper {
   return mount(
     <StaticRouter>
       <PokemonShow {...props} />
@@ -33,22 +33,26 @@ function mountSetup(props = defaultProps) {
   );
 }
 
-describe('<PokemonShow />', () => {
-  it('sets default state', () => {
+describe('<PokemonShow />', (): void => {
+  it('sets default state', (): void => {
     const wrapper = setup();
     expect(wrapper.state('pokemon')).toEqual({id: null, nickname: null});
   });
 
-  describe('componentDidMount()', () => {
+  describe('componentDidMount()', (): void => {
     let mock;
-    beforeEach(() => {
-      mock = new MockAdapter(instance);
-    });
-    afterEach(() => {
-      mock.restore();
-    });
+    beforeEach(
+      (): void => {
+        mock = new MockAdapter(instance);
+      }
+    );
+    afterEach(
+      (): void => {
+        mock.restore();
+      }
+    );
 
-    it('persists fetched pokemon to state', async () => {
+    it('persists fetched pokemon to state', async (): Promise<any> => {
       const mockData = {
         id: 1,
         nickname: 'Bulbasaur',

+ 26 - 2
yarn.lock

@@ -1127,7 +1127,19 @@
   resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.12.tgz#45dd1d0638e8c8f153e87d296907659296873916"
   integrity sha512-SOhuU4wNBxhhTHxYaiG5NY4HBhDIDnJF60GU+2LqHAdKKer86//e4yg69aENCtQ04n0ovz+tq2YPME5t5yp4pw==
 
-"@typescript-eslint/parser@^1.7.0":
+"@typescript-eslint/eslint-plugin@^1.7.0":
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.7.0.tgz#570e45dc84fb97852e363f1e00f47e604a0b8bcc"
+  integrity sha512-NUSz1aTlIzzTjFFVFyzrbo8oFjHg3K/M9MzYByqbMCxeFdErhLAcGITVfXzSz+Yvp5OOpMu3HkIttB0NyKl54Q==
+  dependencies:
+    "@typescript-eslint/parser" "1.7.0"
+    "@typescript-eslint/typescript-estree" "1.7.0"
+    eslint-utils "^1.3.1"
+    regexpp "^2.0.1"
+    requireindex "^1.2.0"
+    tsutils "^3.7.0"
+
+"@typescript-eslint/parser@1.7.0", "@typescript-eslint/parser@^1.7.0":
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-1.7.0.tgz#c3ea0d158349ceefbb6da95b5b09924b75357851"
   integrity sha512-1QFKxs2V940372srm12ovSE683afqc1jB6zF/f8iKhgLz1yoSjYeGHipasao33VXKI+0a/ob9okeogGdKGvvlg==
@@ -8143,6 +8155,11 @@ require-main-filename@^2.0.0:
   resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
   integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
 
+requireindex@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.2.0.tgz#3463cdb22ee151902635aa6c9535d4de9c2ef1ef"
+  integrity sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==
+
 requires-port@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
@@ -9239,11 +9256,18 @@ ts-pnp@^1.1.2:
   resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.1.2.tgz#be8e4bfce5d00f0f58e0666a82260c34a57af552"
   integrity sha512-f5Knjh7XCyRIzoC/z1Su1yLLRrPrFCgtUAh/9fCSP6NKbATwpOL1+idQVXQokK9GRFURn/jYPGPfegIctwunoA==
 
-tslib@^1.9.0:
+tslib@^1.8.1, tslib@^1.9.0:
   version "1.9.3"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
   integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==
 
+tsutils@^3.7.0:
+  version "3.10.0"
+  resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.10.0.tgz#6f1c95c94606e098592b0dff06590cf9659227d6"
+  integrity sha512-q20XSMq7jutbGB8luhKKsQldRKWvyBO2BGqni3p4yq8Ys9bEP/xQw3KepKmMRt9gJ4lvQSScrihJrcKdKoSU7Q==
+  dependencies:
+    tslib "^1.8.1"
+
 tty-browserify@0.0.0:
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"