(obj: O, prop: P): O[P] => obj[prop];
336 | const currifiedGetProp = (prop: P) => (obj: Record): T => obj[prop];
337 |
338 | interface User {
339 | id: number;
340 | name: string;
341 | }
342 |
343 | const user: User = { id: 33, name: 'John' };
344 | const id = getProp(user, 'id');
345 | console.log(id); // 33
346 | const getId = currifiedGetProp('id');
347 | const id1 = getId(user);
348 | console.log(id1); // 33
349 | ```
350 |
351 | # About Lemoncode
352 |
353 | We are a team of long-term experienced freelance developers, established as a group in 2010.
354 | We specialize in Front End technologies and .NET. [Click here](http://lemoncode.net/services/en/#en-home) to get more info about us.
355 |
356 | For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend
357 |
--------------------------------------------------------------------------------
/03 React/README.md:
--------------------------------------------------------------------------------
1 | # 03 React
2 |
3 | In this sample we'll transform a JavaScript React app to TypeScript and we'll see some nice features it provide us.
4 |
5 | The first step is change our entry point in webpack to point to `index.tsx`:
6 |
7 | ### _config/webpack/app/base.js_
8 |
9 | ```diff
10 | module.exports = merge(common, {
11 | context: helpers.resolveFromRootPath('src'),
12 | entry: {
13 | app: [
14 | - './index.jsx',
15 | + './index.tsx',
16 | ],
17 | vendor: [
18 | ```
19 |
20 | Next we'll rename our `index.jsx` to `index.tsx` and apply some TypeScript changes on imports:
21 |
22 | ### ~~_src/index.jsx_~~ → _src/index.tsx_
23 |
24 | ```diff
25 | - import React from 'react';
26 | - import ReactDOM from 'react-dom';
27 | + import * as React from 'react';
28 | + import * as ReactDOM from 'react-dom';
29 | import { AppContainer } from 'react-hot-loader';
30 | import { App } from './app';
31 | ```
32 |
33 | Next file is `app.jsx`. Let's change imports and type state and methods:
34 |
35 | ### ~~_src/app.jsx_~~ → _src/app.tsx_
36 |
37 | ```diff
38 | - import React, { PureComponent } from 'react';
39 | + import * as React from 'react';
40 | import { MultiStepForm, FirstStep, SecondStep, ThirdStep } from './components';
41 | - import styles from './app.scss';
42 | + const styles: any = require('./app.scss');
43 | +
44 | + interface State {
45 | + formData: {
46 | + agreement: boolean;
47 | + confirmPassword: string;
48 | + firstName: string;
49 | + lastName: string;
50 | + password: string;
51 | + phone: string;
52 | + username: string;
53 | + };
54 | + }
55 |
56 | - export class App extends PureComponent {
57 | + export class App extends React.PureComponent<{}, State> {
58 | ...
59 |
60 | - onChangeField = (field, value) => {
61 | + onChangeField = (field: string, value: any) => {
62 |
63 | ...
64 |
65 | onSubmit = () => {
66 | + // tslint:disable-next-line:no-console
67 | console.log(JSON.stringify(this.state.formData));
68 | }
69 | ```
70 |
71 | Now it's time to refactor `MultiStepForm`:
72 |
73 | ### ~~_src/components/multistepform/multistepform.jsx_~~ → _src/components/multistepform/multistepform.tsx_
74 |
75 | First we'll refactor imports section
76 |
77 | ```diff
78 | + import * as React from 'react';
79 | - import React, { PureComponent, Fragment } from 'react';
80 | - import PropTypes from 'prop-types';
81 | import { Button } from '../common';
82 | - import styles from './multistepform.scss';
83 | + const styles: any = require('./multistepform.scss');
84 | ```
85 |
86 | Next we'll define `Props` based on propTypes and `State` interfaces:
87 |
88 | ```diff
89 | + interface Props {
90 | + heading: string;
91 | + onSubmit(): void;
92 | + }
93 | +
94 | + interface State {
95 | + currentStep: number;
96 | + }
97 | +
98 | - export class MultiStepForm extends PureComponent {
99 | + export class MultiStepForm extends React.PureComponent {
100 | -
101 | - static propTypes = {
102 | - heading: PropTypes.string.isRequired,
103 | - onSubmit: PropTypes.func.isRequired,
104 | - }
105 |
106 | state = {
107 | currentStep: 0,
108 | };
109 | ```
110 |
111 | Now we'll add some types on methods and variables:
112 |
113 | ```diff
114 | render() {
115 | const children = React.Children.toArray(this.props.children);
116 | - const child = children[this.state.currentStep];
117 | + const child = children[this.state.currentStep] as React.ReactElement;
118 |
119 | ...
120 |
121 | }
122 |
123 | - goPreviousStep = (event) => {
124 | + goPreviousStep = (event: React.MouseEvent) => {
125 | ...
126 | }
127 |
128 | - goNextStep = (event) => {
129 | + goNextStep = (event: React.MouseEvent) => {
130 | ...
131 | }
132 |
133 | - onSubmit = (event) => {
134 | + onSubmit = (event: React.MouseEvent) => {
135 | ...
136 | }
137 |
138 | renderButtons(childrenLength) {
139 | const { currentStep } = this.state;
140 | - let content, type;
141 | + let content: string;
142 | + let type: string;
143 | ...
144 |
145 | return (
146 | -
147 | + <>
148 | {currentStep !== 0 &&
149 |
153 | }
154 |
159 | -
160 | + >
161 | );
162 | ```
163 |
164 | Now we'll refactor `Button`. Rename its extension to `.tsx` and make next changes:
165 |
166 | ### ~~_src/components/common/button.jsx_~~ → _src/components/common/button.tsx_
167 |
168 | ```diff
169 | - import React from 'react';
170 | + import * as React from 'react';
171 | - import PropTypes from 'prop-types';
172 | - import styles from './button.scss';
173 | + const styles: any = require('./button.scss');
174 | +
175 | + interface Props {
176 | + onClick(event: React.MouseEvent): void;
177 | + type?: string;
178 | + content: string;
179 | + }
180 |
181 | - export const Button = ({ onClick, type, content }) => (
182 | + export const Button: React.StatelessComponent = ({ onClick, type, content }) => (
183 | ...
184 | );
185 |
186 | - Button.propTypes = {
187 | - onClick: PropTypes.func.isRequired,
188 | - type: PropTypes.string,
189 | - content: PropTypes.string.isRequired,
190 | - };
191 | ```
192 |
193 | Let's refactor more `MultiStepForm` childs. We'll start with `FirstStep`. If you look at the component definition it's using a shared entity under `common.js` and this is using another shared entity inside `entities` so let's take that file as our entry point to refactor. Rename `entities.js` to `entities.ts` and make next change to export an interface instead of propType:
194 |
195 | ### ~~_src/entities.js_~~ → _src/entities.ts_
196 |
197 | ```diff
198 | - import PropTypes from 'prop-types';
199 | -
200 | - export const SignupDataType = PropTypes.shape({
201 | - agreement: PropTypes.bool.isRequired,
202 | - username: PropTypes.string.isRequired,
203 | - password: PropTypes.string.isRequired,
204 | - confirmPassword: PropTypes.string.isRequired,
205 | - firstName: PropTypes.string.isRequired,
206 | - lastName: PropTypes.string.isRequired,
207 | - phone: PropTypes.string.isRequired,
208 | - });
209 | + export interface SignupData {
210 | + agreement: boolean;
211 | + username: string;
212 | + password: string;
213 | + confirmPassword: string;
214 | + firstName: string;
215 | + lastName: string;
216 | + phone: string;
217 | + }
218 | ```
219 |
220 | Maybe you've noted down something. This is the same entity as the App's `formData` state. With this change we can use it directly in `App` so let's use it:
221 |
222 | ### _src/app.tsx_
223 |
224 | ```diff
225 | import * as React from 'react';
226 | import { MultiStepForm, FirstStep, SecondStep, ThirdStep } from './components';
227 | + import { SignupData } from './entities';
228 | const styles: any = require('./app.scss');
229 |
230 | interface State {
231 | - formData: {
232 | - agreement: boolean;
233 | - confirmPassword: string;
234 | - firstName: string;
235 | - lastName: string;
236 | - password: string;
237 | - phone: string;
238 | - username: string;
239 | - };
240 | + formData: SignupData;
241 | }
242 | ```
243 |
244 | Let's now refactor `common.js` by changing its extension to `ts` and changing the exporter propType to another interface:
245 |
246 | ### ~~_src/components/steps/multistepform/common.js_~~ → _src/components/steps/multistepform/common.ts_
247 |
248 | ```diff
249 | - import PropTypes from 'prop-types';
250 | - import { SignupDataType } from '../../../entities';
251 | import { SignupData } from '../../../entities';
252 |
253 | - export const FormStepPropTypes = {
254 | - title: PropTypes.string.isRequired,
255 | - formData: SignupDataType,
256 | - onChangeField: PropTypes.func.isRequired,
257 | - };
258 | + export interface FormStep {
259 | + title: string;
260 | + formData: SignupData;
261 | + onChangeField(field: string, value: any);
262 | + }
263 | ```
264 |
265 | This propType (now an interface) is used by every _step_ component. We can type now our `child` in `MultiStepForm`:
266 |
267 | ### _src/components/steps/multistepform/multistepform.tsx_
268 |
269 | ```diff
270 | import * as React from 'react';
271 | import { Button } from '../common';
272 | + import { FormStep } from './steps/common';
273 | const styles: any = require('./multistepform.scss');
274 |
275 | ...
276 |
277 | render() {
278 | const children = React.Children.toArray(this.props.children);
279 | + const child = children[this.state.currentStep] as React.ReactElement;
280 | - const child = children[this.state.currentStep] as React.ReactElement;
281 | ```
282 |
283 | Now we have ready step component's propTypes let's type `FirstStep`:
284 |
285 | ### ~~_src/components/multistepform/firstStep.jsx_~~ → _src/components/multistepform/firstStep.tsx_
286 |
287 | ```diff
288 | - import React from 'react';
289 | + import * as React from 'react';
290 | import { Input, Button } from '../../common';
291 | - import { FormStepPropTypes } from './common';
292 | + import { FormStep } from './common';
293 |
294 | - export const FirstStep = (props) => (
295 | + export const FirstStep: React.StatelessComponent = (props) => (
296 | ...
297 | );
298 |
299 | - FirstStep.propTypes = FormStepPropTypes;
300 | ```
301 |
302 | Since `FirstStep` is using `Input` let's refactor it too:
303 |
304 | ### ~~_src/components/common/input.jsx_~~ → _src/components/common/input.tsx_
305 |
306 | ```diff
307 | - import React from 'react';
308 | + import * as React from 'react';
309 | - import PropTypes from 'prop-types';
310 | - import styles from './input.scss';
311 | + const styles: any = require('./input.scss');
312 | +
313 | + interface Props {
314 | + value: string | number;
315 | + onChange(field: string, value: any);
316 | + label: string;
317 | + id: string;
318 | + name: string;
319 | + type?: string;
320 | + }
321 |
322 | - export const Input = (props) => (
323 | + export const Input: React.StatelessComponent = (props) => (
324 | ...
325 | );
326 |
327 | - Input.propTypes = {
328 | - value: PropTypes.oneOfType([
329 | - PropTypes.string,
330 | - PropTypes.number,
331 | - ]).isRequired,
332 | - onChange: PropTypes.func.isRequired,
333 | - label: PropTypes.string.isRequired,
334 | - id: PropTypes.string.isRequired,
335 | - name: PropTypes.string.isRequired,
336 | - type: PropTypes.string,
337 | - }
338 |
339 | Input.defaultProps = {
340 | type: 'text',
341 | };
342 |
343 | - const onChange = (props) => ({ target: { value, name } }) => {
344 | + const onChange = (props: Props) => ({ target: { value, name } }: React.ChangeEvent) => {
345 | props.onChange(name, value);
346 | };
347 | ```
348 |
349 | It's `SecondStep` component's turn to refactor:
350 |
351 | ### ~~_src/components/multistepform/secondStep.jsx_~~ → _src/components/multistepform/secondStep.tsx_
352 |
353 | ```diff
354 | - import React from 'react';
355 | + import * as React from 'react';
356 | import { Input } from '../../common';
357 | - import { FormStepPropTypes } from './common';
358 | + import { FormStep } from './common';
359 |
360 | - export const SecondStep = (props) => (
361 | + export const SecondStep: React.StatelessComponent = (props) => (
362 | ...
363 | );
364 |
365 | - SecondStep.propTypes = FormStepPropTypes;
366 | ```
367 |
368 | Next we'll refactor `ThirdStep` component:
369 |
370 | ### ~~_src/components/multistepform/thirdStep.jsx_~~ → _src/components/multistepform/thirdStep.tsx_
371 |
372 | ```diff
373 | - import React from 'react';
374 | + import * as React from 'react';
375 | import { Input, Checkbox } from '../../common';
376 | - import { FormStepPropTypes } from './common';
377 | + import { FormStep } from './common';
378 | - import styles from './stepStyles.scss';
379 | + const styles: any = require('./stepStyles.scss');
380 |
381 | - export const ThirdStep = (props) => (
382 | + export const ThirdStep: React.StatelessComponent = (props) => (
383 | ...
384 | );
385 |
386 | - ThirdStep.propTypes = FormStepPropTypes;
387 | ```
388 |
389 | Since `ThirdStep` component is using `Checkbox` let's refactor it:
390 |
391 | ### ~~_src/components/common/checkbox.jsx_~~ → _src/components/common/checkbox.tsx_
392 |
393 | ```diff
394 | - import React from 'react';
395 | + import * as React from 'react';
396 | - import PropTypes from 'prop-types';
397 | + import styles from './checkbox.scss';
398 | +
399 | + interface Props {
400 | + id: string;
401 | + name: string;
402 | + checked: boolean;
403 | + onChange(field: string, value: any): void;
404 | + }
405 |
406 | - export const Checkbox = (props) => (
407 | + export const Checkbox: React.StatelessComponent = (props) => (
408 | ...
409 | );
410 |
411 | - Checkbox.propTypes = {
412 | - id: PropTypes.string.isRequired,
413 | - name: PropTypes.string.isRequired,
414 | - checked: PropTypes.bool.isRequired,
415 | - onChange: PropTypes.func.isRequired,
416 | - }
417 |
418 | - const onChange = (props) => ({ target: { name, checked } }) => {
419 | + const onChange = (props: Props) => ({ target: { value, name } }: React.ChangeEvent) => {
420 | props.onChange(name, checked);
421 | };
422 | ```
423 |
424 | Now we have all components refactored to TypeScript. Our remaining task it's to refactor all `index.js` to `index.ts`:
425 |
426 | - ~~_src/components/common/index.js_~~ → _src/components/common/index.ts_
427 | - ~~_src/components/index.js_~~ → _src/components/index.ts_
428 | - ~~_src/components/multistepform/index.js_~~ → _src/components/multistepform/index.ts_
429 | - ~~_src/components/multistepform/steps/index.js_~~ → _src/components/multistepform/steps/index.ts_
430 |
431 | Finally let's make two refactors to improve typechecking.
432 |
433 | First we'll refactor the `value: any` from `onChangeField` method in `app.tsx` to a better type. Let's create a common `Value` entity with type `string | number | boolean`:
434 |
435 | ### _src/entities.ts_
436 |
437 | ```diff
438 | + export type Value = string | number | boolean;
439 | +
440 | export interface SignupData {
441 | ...
442 | }
443 | ```
444 |
445 | Next use it in `app.tsx`, `checkbox.tsx`, `input.tsx` and `common.ts`:
446 |
447 | ### _src/app.tsx_
448 |
449 | ```diff
450 | import * as React from 'react';
451 | import { MultiStepForm, FirstStep, SecondStep, ThirdStep } from './components';
452 | - import { SignupData } from './entities';
453 | + import { Value, SignupData } from './entities';
454 | const styles: any = require('./app.scss');
455 |
456 | ...
457 |
458 | - onChangeField = (field: string, value: any) => {
459 | + onChangeField = (field: string, value: Value) => {
460 | ...
461 | }
462 | ```
463 |
464 | ### _src/components/common/input.tsx_
465 |
466 | ```diff
467 | import * as React from 'react';
468 | + import { Value } from '../../entities';
469 | const styles: any = require('./input.scss');
470 |
471 | interface Props {
472 | value: string | number;
473 | - onChange(field: string, value: any): void;
474 | + onChange(field: string, value: Value): void;
475 | label: string;
476 | id: string;
477 | name: string;
478 | type?: string;
479 | }
480 | ```
481 |
482 | ### _src/components/common/checkbox.tsx_
483 |
484 | ```diff
485 | import * as React from 'react';
486 | + import { Value } from '../../entities';
487 | const styles: any = require('./checkbox.scss');
488 |
489 | interface Props {
490 | id: string;
491 | name: string;
492 | checked: boolean;
493 | - onChange(field: string, value: any): void;
494 | + onChange(field: string, value: Value): void;
495 | }
496 | ```
497 |
498 | ### _src/components/multistepform/steps/common.tsx_
499 |
500 | ```diff
501 | - import { SignupData } from '../../../entities';
502 | + import { Value, SignupData } from '../../../entities';
503 | import { SignupData } from '../../../entities';
504 |
505 | export interface FormStep {
506 | title: string;
507 | formData: SignupData;
508 | - onChangeField(field: string, value: any): void;
509 | onChangeField(field: string, value: Value): void;
510 | }
511 | ```
512 |
513 | Last but not less important, we can remove some babel plugins and presets like `babel-preset-react`, `babel-preset-stage-*` and `babel-plugin-transform-class-properties` (even `babel-loader` if you're using `awesome-typescript-loader`) if you use it because `tsc` (TypeScript) compiler will be take care of it.
514 |
515 | ## **(Optional)**
516 |
517 | If you look carefully at `Input` and `Checkbox` onChange method, we're using the `name` as `field`. What if we make a typo there? We can type it using `keyof SignupData` **taking the assumption those common components are tightly linked to `App` state**:
518 |
519 | ### _src/app.tsx_
520 |
521 | ```diff
522 | - onChangeField = (field: string, value: Value) => {
523 | + onChangeField = (field: keyof SignupData, value: Value) => {
524 | ```
525 |
526 | ### _src/components/multistepform/steps/common.ts
527 |
528 | ```diff
529 | export interface FormStep {
530 | title: string;
531 | formData: SignupData;
532 | - onChangeField(field: string, value: Value): void;
533 | + onChangeField(field: keyof SignupData, value: Value): void;
534 | }
535 | ```
536 |
537 | ### _src/components/common/input.tsx
538 |
539 | ```diff
540 | import * as React from 'react';
541 | - import { Value } from '../../entities';
542 | + import { Value, SignupData } from '../../entities';
543 | const styles: any = require('./input.scss');
544 |
545 | interface Props {
546 | value: string | number;
547 | - onChange(field: string, value: Value): void;
548 | + onChange(field: keyof SignupData, value: Value): void;
549 | label: string;
550 | id: string;
551 | name: keyof SignupData;
552 | type?: string;
553 | }
554 |
555 | ...
556 |
557 | const onChange = (props: Props) => ({ target: { value, name } }: React.ChangeEvent) => {
558 | - props.onChange(name, value);
559 | + props.onChange(name as keyof SignupData, value);
560 | };
561 | ```
562 |
563 | ### _src/components/common/checkbox.tsx
564 |
565 | ```diff
566 | import * as React from 'react';
567 | - import { Value } from '../../entities';
568 | + import { Value, SignupData } from '../../entities';
569 | const styles: any = require('./checkbox.scss');
570 |
571 | interface Props {
572 | id: string;
573 | name: keyof SignupData;
574 | checked: boolean;
575 | - onChange(field: string, value: Value): void;
576 | + onChange(field: keyof SignupData, value: Value): void;
577 | }
578 |
579 | ...
580 |
581 | const onChange = (props: Props) => ({ target: { name, checked } }: React.ChangeEvent) => {
582 | - props.onChange(name, checked);
583 | props.onChange(name as keyof SignupData, checked);
584 | };
585 | ```
586 |
587 | Now if we try to pass a `name="confirm-password"` instead of `name="confirmPassword" in `FirstStep` component we get an error.
588 |
589 | ---
590 |
591 | # About Lemoncode
592 |
593 | We are a team of long-term experienced freelance developers, established as a group in 2010.
594 | We specialize in Front End technologies and .NET. [Click here](http://lemoncode.net/services/en/#en-home) to get more info about us.
595 |
596 | For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend
597 |
--------------------------------------------------------------------------------