Hi Habr, today we will work with TypeScript and React-hooks. This tutorial will help you understand the basics of "Tipscript" and help in the work on the test task for the frontendder .
Test tasks on the project "Without Water" - this is an opportunity to get code-review. The deadline for the current assignment is April 11, 2019.
If you are too lazy to read, come to the webinar on March 20 at 21:00 Moscow time. Registration (without e-mails and sms).
To start, you can take the Create-react-app TypeScript version, or use my starter (in which the reach-router is already included)
I will use my starter (about this in the "Practice" section).
TS solves the problem of "dynamic typing" in JavaScript, when your variable can take on different values. That line, the number, if not the object. It was so convenient to write in the "19th century", however, now everyone agrees that if you have predefined types (count the rules), then the code base is easier to maintain. Yes, and bugs at the design stage becomes smaller.
For example, you have a component that displays a single news item, we can specify the following type for news:
// - { } // c export interface INewsItem { id: number; // id - title: string; // title () - text: string; // text ( ) - link: string; // link () - timestamp: Date; // timestamp () - js }
Thus, we have specified strict "static" types for the properties of our object "news". If we try to retrieve a non-existing property, TypeScript will display an error.
import * as React from 'react' import { INewsItem } from '../models/news' // "" interface INewsItemProps { data: INewsItem; // , ( ) } const NewsItem: React.FC<INewsItemProps> = ({ data: { id, text, abracadabra }, // , id text - , abracadabra - }) => { return ( <article> <div>{id}</div> <div>{text}</div> </article> ) } export { NewsItem }
Also, Visual studio Code and other advanced editors will show you the error:
Visually, conveniently. In my case, VS Code shows two errors at once: the variable type is not set (that is, it does not exist in our “news” interface) and the “variable is not used”. Moreover, unused variables when using TypeScript are highlighted in pale color in VS Code by default.
Here it is necessary to indicate in one line the reason for such a tight integration of TypeScript and VS Code: both products are developed by Microsoft.
What else can TypeScript tell us right away? If we speak in the context of variables, that's all. TS is very powerful, he understands "what's what."
const NewsItem: React.FC<INewsItemProps> = ({ data: { id, title, text } }) => { return ( <article> <div>{id.toUpperCase()}</div> {/* , , 'number' toUpperCase() */} <div>{title.toUpperCase()}</div> {/* ! */} <div>{text}</div> </article> ) }
Here, TypeScript immediately swears at a non-existent property - the number
type toUpperCase
. And as we know, indeed, only string types have a toUpperCase () method.
Now imagine, you start to write the name of some function, open the bracket, and the editor immediately shows you a pop-up window that tells you which arguments and what type can be passed to the function.
Or imagine - you strictly followed the recommendations and typing on your project bulletproof. In addition to auto-substitution, you get rid of problems with implicit values ​​on a project.
Let's rewrite the first test task on react-hooks + TypeScript. For now, we ’ll skip Redux , otherwise, instead of working on " restarted at TC # 1, " you just copy everything from here.
(for those who use VS Code )
For convenience, I recommend that you put the extension TSLint .
To enable the autofix of TSLint errors at the moment of saving, add in the editor settings:
// settings.json visual studio "editor.codeActionsOnSave": { "source.fixAll.tslint": true }
You can get to the settings through the menu, or see where they live physically in your operating system.
TSLint settings are standard, plus I have disabled one rule.
{ "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"], "linterOptions": { "exclude": [ "node_modules/**/*.ts", "src/serviceWorker.js" ] }, "rules": { "object-literal-sort-keys": false // } }
At this integration is over!
We will meet with new things for us right in the course of the play. For starters, clone your 1-start branch or synchronize in your code with my code.
All react type script files have the extension .tsx .
Let's start working with src / App.tsx :
import * as React from 'react' import './App.css' const App = () => { return ( <div className="container"> <h1>TZ #1 with hooks & TypeScript</h1> <nav> <p></p> </nav> <p> </p> </div> ) } const RoutedApp = () => { return <App /> } export { RoutedApp }
Ok, standard start. Let's try to add some property in <App />
src / App.tsx
const App = props => { return ( <div className="container"> <h1>TZ #1 with hooks & TypeScript</h1> <nav> <p></p> </nav> <p> </p> {/* name props */} <p>, {props.name}</p> </div> ) } // name const RoutedApp = () => { return <App name="Max Frontend" /> }
We get the error:
(if you have not received an error, then check the strictness of the settings of your tsconfig.json, there should be a rule noImplicitAny)
You already guess from the translation of the text of the error that our properties should not be of type any . This type can be translated as "anything." Our project has a rule that prohibits implicit output of this type.
- Implicit type inference?
- Exactly! TypeScript by default is able to deduce the type of a variable and it copes well with it. This is called Type Inference.
Example:
let x = 3 // TS , x number, number " " (implicit) let x: number = 3 // (explicit) , x number // , // TS
In the case of props
, TS cannot determine the type of a variable by 100% and therefore says - let it be (that is, the type
any
). This is done implicitly and is prohibited by the noImplicitAny rule in the project settings (tsconfig.json)
We can explicitly specify the type any and the error will disappear. The type of the variable is indicated by a colon.
// :any // "props: any" // const App = (props: any) => { return ( <div className="container"> <h1>TZ #1 with hooks & TypeScript</h1> <nav> <p></p> </nav> <p> </p> <p>, {props.name}</p> </div> ) }
Done, no errors, the project works, but what is the use of such typing, when props
can be anything? We know for sure that our name is a . Hence the rule:
Try to avoid the any type.
There are cases when any
needed and this is normal, but it is immediately a low blow on strict typing.
To describe the types of props
, we will use the interface
keyword:
// , IAppProps // I, TSLint // I interface IAppProps { name: string; // name string } // props const App = (props: IAppProps) => { return ( <div className="container"> <h1>TZ #1 with hooks & TypeScript</h1> <nav> <p></p> </nav> <p> </p> <p>, {props.name}</p> </div> ) }
Try changing the name
type to number
and an error will immediately appear.
Moreover, the error will also be underlined in VS Code (and many other editors). The error indicates that we do not have a match: we pass a string, but we expect a number.
Fix it and add one more props
- site to <App />
src / App.tsx
interface IAppProps { name: string; } const App = (props: IAppProps) => { return ( <div className="container"> <h1>TZ #1 with hooks & TypeScript</h1> <nav> <p></p> </nav> <p> </p> <p>, {props.name}</p> {/* site */} <p>: {props.site}</p> </div> ) } // site const RoutedApp = () => { return <App name="Max Frontend" site="maxpfrontend.ru" /> }
Received an error:
Type error: Property 'site' does not exist on type 'IAppProps'. TS2339
The site
property does not exist in the IAppProps
type. Here I immediately want to say that by the type name we immediately understand where to look. Therefore, name the types correctly.
Before fixing, let's do this: remove the paragraph drawing props.site
.
Get another error text:
Here I want to note only that TS output: site
is a type of string
(in the screenshot, this is underlined).
Correct:
interface IAppProps { name: string; site: string; // } const App = (props: IAppProps) => { return ( <div className="container"> <h1>TZ #1 with hooks & TypeScript</h1> <nav> <p></p> </nav> <p> </p> <p>, {props.name}</p> <p>: {props.site}</p> </div> ) } const RoutedApp = () => { return <App name="Max Frontend" site="maxpfrontend.ru" /> }
No mistakes, no problems.
To work with routing, we need to draw children. Let's go ahead and try to draw the "child component".
const App = (props: IAppProps) => { return ( <div className="container"> <h1>TZ #1 with hooks & TypeScript</h1> ... // <p>: {props.site}</p> {props.children} </div> ) } const Baby = () => { return <p> </p> } const RoutedApp = () => { return ( <App name="Max Frontend" site="maxpfrontend.ru"> <Baby /> </App> ) }
TS curses, they say this way and that - children
not described in IAppProps
.
Of course, we would not like to "typify" some standard things, and here the community comes to the rescue, which has already typified much before us. For example, the @ types / react package contains all the typing for react.
Having installed this package (in my example it is already installed), we can use the following entry:
React.FunctionComponent<P> React.FC<P>
where <P>
are the types for our props
, that is, the record will take the form.
React.FC<IAppProps>
For those who are keen on reading large amounts of text, before practicing, I can offer an article about " generics " (the very <and>). For the rest, for now it is enough that we translate this phrase like this: a functional component accepting <such-and-such properties>.
The entry for the App component changes slightly. Full version.
src / App.tsx
// , // React React.XXX, // XXX - import * as React from 'react' // , , // @types/react // interface IAppProps { name: string; site: string; } // const App: React.FC<IAppProps> = props => { return ( <div className="container"> <h1>TZ #1 with hooks & TypeScript</h1> <nav> <p></p> </nav> <p> </p> <p>, {props.name}</p> <p>: {props.site}</p> {props.children} </div> ) } const Baby = () => { return <p> </p> } const RoutedApp = () => { return ( <App name="Max Frontend" site="maxpfrontend.ru"> <Baby /> </App> ) }
Let's sort the following line by character:
const App: React.FC<IAppProps> = props => {
- Why did the props
disappear after the props
?
- Because, after the App
- added.
We recorded that the App variable will be of the type: React.FC<IAppProps>
.
React.FC
is a type of "function", and we inside <> indicated what type of argument it takes, that is, indicated that our props
will be of type IAppProps
.
(there is a risk here that I lied to you a little in terms, but to simplify an example, I think it’s ok)
Total: we learned to specify the type of properties for the transmitted props
, without losing "our" properties of React components.
Source code for now.
We will use reach-router to expand the horizons. This package is very similar to react-router.
Add a page - News (News), clean up the <App />
.
src / pages / News.tsx
import * as React from 'react' const News = () => { return ( <div className="news"> <p></p> </div> ) } export { News }
src / App.tsx
import * as React from 'react' // reach-router import { Link, Router } from '@reach/router' import { News } from './pages/News' import './App.css' interface IAppProps { name: string; site: string; } const App: React.FC<IAppProps> = props => { return ( <div className="container"> <h1>TZ #1 with hooks & TypeScript</h1> <nav> <Link to="/">Home</Link> <Link to="news">News</Link>{' '} </nav> <hr /> <p> {' '} : {props.name} | : {props.site} </p> <hr /> {props.children} </div> ) } // Baby, News. app - path const RoutedApp = () => { return ( <Router> <App path="/" name="Max Frontend" site="maxpfrontend.ru"> <News path="/news" /> </App> </Router> ) } export { RoutedApp }
The application broke, an error (one of the errors, since the terminal shows the first one):
We have already become accustomed to such a record, and we understand from it that path
does not exist in the type description for <App />
.
Again, everything is described before us. We will use the @ types / reach__router package and the RouteComponentProps
type. In order not to lose our properties, we will use the extends
.
import * as React from 'react' // RouteComponentProps - // ts , import { Link, RouteComponentProps, Router } from '@reach/router' import { News } from './pages/News' import './App.css' // extends RouteComponentProps // interface IAppProps extends RouteComponentProps { name: string; site: string; } // ...
For the curious, what types are described in RouteComponentProps .
The error in <App />
disappeared, but remained in <News />
, since we did not specify the typing for this component.
Mini task: specify the typing for <News />
. At the moment, only properties from the router are transferred there.
Answer:
src / Pages / News.tsx
import * as React from 'react' import { RouteComponentProps } from '@reach/router' // RouteComponentProps // P ( React.FC<P> ) const News: React.FC<RouteComponentProps> = () => { return ( <div className="news"> <p></p> </div> ) } export { News }
Go ahead and add a route with a parameter. The parameters in the reach-router live directly in the props. In react-router, as you remember, they live in props.match
.
src / App.tsx
import * as React from 'react' import { Link, RouteComponentProps, Router } from '@reach/router' import { About } from './pages/About' import { News } from './pages/News' // ... () const RoutedApp = () => { return ( <Router> <App path="/" name="Max Frontend" site="maxpfrontend.ru"> <News path="/news" /> {/* source */} <About path="/about/:source" /> </App> </Router> ) } export { RoutedApp }
src / pages / About.tsx
import * as React from 'react' import { RouteComponentProps } from '@reach/router' const About: React.FC<RouteComponentProps> = props => { return ( <div className="about"> <p> about</p> {/* source */} <p>{props.source}</p> </div> ) } export { About }
Error that we did not expect:
The source property does not exist ... On the one hand, bewilderment: we pass it to path, which is string, on the other hand, joy: Oh, you, as the authors of the library and typing tried, adding this warning to us.
To fix this, we will use one of the options : inherit (extend) from RouteComponentProps
and specify the optional property source
. Optional, because it may not be in our URL.
In TypeScript, a question mark is used to specify an optional property.
src / pages / About.tsx
import * as React from 'react' import { RouteComponentProps } from '@reach/router' interface IAboutProps extends RouteComponentProps { source?: string; // source - , ( props.source undefined) } const About: React.FC<IAboutProps> = props => { return ( <div className="about"> <p> about</p> <p>{props.source}</p> </div> ) } export { About }
src / App.tsx (at the same time Russify navigation)
import * as React from 'react' import { Link, RouteComponentProps, Router } from '@reach/router' import { About } from './pages/About' import { News } from './pages/News' import './App.css' interface IAppProps extends RouteComponentProps { name: string; site: string; } const App: React.FC<IAppProps> = props => { return ( <div className="container"> <h1>TZ #1 with hooks & TypeScript</h1> <nav> <Link to="/"></Link> <Link to="news"></Link>{' '} <Link to="/about/habr"> habr</Link>{' '} </nav> <hr /> <p> {' '} : {props.name} | : {props.site} </p> <hr /> {props.children} </div> ) } const RoutedApp = () => { return ( <Router> <App path="/" name="Max Frontend" site="maxpfrontend.ru"> <News path="/news" /> <About path="/about/:source" /> </App> </Router> ) } export { RoutedApp }
Total : learned to typify the components involved in rooting.
Source code for now.
I remind you that our task is to implement a test task , but without Redux.
I have prepared a branch for starting this step with routing, a non-working login form and the necessary pages.
News is an array of objects.
Imagine our news:
{ id: 1, title: ' CRUD React-hooks', text: ' CRUD- ', link: 'https://maxpfrontend.ru/perevody/delaem-crud-prilozhenie-s-pomoschyu-react-hooks/', timestamp: new Date('01-15-2019'), },
Let's immediately write a model (types for news):
src / models / news.ts (extension .ts )
export interface INewsItem { id: number; title: string; text: string; link: string; timestamp: Date; }
From the new one - timestamp indicated type Date
.
Imagine our call for the data:
const fakeData = [ { id: 1, title: ' CRUD React-hooks', text: ' CRUD- ', link: 'https://maxpfrontend.ru/perevody/delaem-crud-prilozhenie-s-pomoschyu-react-hooks/', timestamp: new Date('01-15-2019'), }, { id: 2, title: ' React hooks', text: ' useState useEffect ', link: 'https://maxpfrontend.ru/perevody/znakomstvo-s-react-hooks/', timestamp: new Date('01-06-2019'), }, { id: 3, title: ' Google Sign In', text: ' Google Sign In ', link: 'https://maxpfrontend.ru/vebinary/avtorizatsiya-s-pomoschyu-google-sign-in/', timestamp: new Date('11-02-2018'), }, ] export const getNews = () => { const promise = new Promise(resolve => { resolve({ status: 200, data: fakeData, // }) }) return promise // promise }
Our call from api getNews
returns a Promise , and this "promise" has a certain type, which we can also describe:
interface INewsResponse { status: number; // - data: INewsItem[]; // data - , INewsItem [1] errorText?: string; // , errorText , } // [1] , models export interface INewsItem { id: number; title: string; text: string; link: string; timestamp: Date; } // __[] - __ // [{__}, {__}, {__}]
Hot? Now it will be even hotter, since the type of Promise is a generic, we will again have to deal with <
and >
. This is the most difficult place of the tutorial, so we read into the final code:
src / api / News.ts
import { INewsItem } from '../models/news' // interface INewsResponse { // __ status: number; data: INewsItem[]; errorText?: string; } const fakeData = [ //... ] // // : // const myFunc = ():__ { return _ } // getNews - , () ( ) // Promise // Promise - generic, : // Promise<T>, T - , [1] // , T , - INewsResponse export const getNews = (): Promise<INewsResponse> => { // , [1] const promise = new Promise<INewsResponse>(resolve => { // [2] resolve({ status: 200, data: fakeData, }) }) return promise // promise [2] Promise<INewsResponse> }
Smoke break
src / pages / News.tsx
import * as React from 'react' import { RouteComponentProps } from '@reach/router' import { getNews } from '../api/news' import { NewsItem } from '../components/NewsItem' // import { INewsItem } from '../models/news' const News: React.FC<RouteComponentProps> = () => { // useState - , T // , T - INewsItem // , , [] const [news, setNews] = React.useState<INewsItem[]>([]) // <- React.useEffect(() => { getNews() .then(res => { setNews(res.data) }) .catch(err => { // TSLint console.log // , "" // tslint:disable-next-line: no-console console.warn('Getting news problem', err) }) }, []) return ( <div className="news"> {news.map(item => ( <NewsItem data={item} key={item.id} /> ))} </div> ) } export { News }
The code contains comments that relate to TypeScript. If you need help with react-hooks, you can read here: documentation (EN), tutorial (RU).
Task: write the <NewsItem />
component that will show the news. Do not forget to specify the correct type. Use the INewsItem
model.
The result might look like this:
Solution below.
src / components / NewsItem.tsx
import * as React from 'react' import { INewsItem } from '../models/news' interface INewsItemProps { data: INewsItem; // [1] } // [2] const NewsItem: React.FC<INewsItemProps> = ({ data: { title, text, timestamp, link }, }) => { return ( <article> <br /> <div> { <a href={link} target="_blank"> {title} </a> }{' '} | {timestamp.toLocaleDateString()} </div> <div>{text}</div> </article> ) } export { NewsItem }
The question of why we described the interface in this way (comments in the code [1] and [2]). Could we just write:
React.FC<INewsItem>
Ovet below.
.
.
.
Could, since we pass the news in the data
property, that is, we must write:
React.FC<{ data: INewsItem }>
What we did, with the only difference that the interface
pointed out, in case other properties are added to the component. And readability is better.
Total: practice typing for Promise and useEffect. Described the type for another component.
If you still don’t clap your hands on auto-substitution and TS severity, then you either don’t practice or strict typing is not for you. In the second case - I can not help, this is a matter of taste. I pay attention that the market dictates its own conditions and more and more projects live with typing, if not with TypeScript, then with flow .
Source code for now.
Write api
for login. The principle is similar - we return a promise
with the answer authorized / error. Information about the status of authorization will be stored in localStorage
( many consider this a blatant violation of security, I am a little behind the controversy and I don’t know how it ended )
In our application, the login is a bunch of name and password (both are string), we describe the model:
src / models / user.ts
export interface IUserIdentity { username: string; password: string; }
src / api / auth.ts
import { navigate } from '@reach/router' import { IUserIdentity } from '../models/user' // // data - any ( , ) // , , , // , ( ) interface IAuthResponse { status: number; data?: any; [1] errorText?: string; } // -, - // IUserIdentity // - boolean (true false) const checkCredentials = (data: IUserIdentity): boolean => { if (data.username === 'Admin' && data.password === '12345') { return true } else { return false } } // "-", , // , 1 - data // Promise<T>, T - IAuthResponse export const authenticate = (data: IUserIdentity): Promise<IAuthResponse> => { const promise = new Promise<IAuthResponse>((resolve, reject) => { if (!checkCredentials(data)) { reject({ status: 500, errorText: 'incorrect_login_or_password', }) } window.localStorage.setItem('tstz.authenticated', 'true') resolve({ status: 200, data: 'ok', // - string, IAuthResponse [1] any string }) }) return promise } // , // 0 () // true false ( boolean) export const checkAuthStatus = (): boolean => { if (localStorage.getItem('tstz.authenticated')) { return true } else { return false } } // , 0 // ( void) export const logout = (): void => { window.localStorage.removeItem('tstz.authenticated') navigate('/') // url- (reach-router) }
, .
From the new:
void
: . useState . event
onChange
/ onSubmit
— any
. .
React.useState
— , ( React.useState<T>
)
, , , . , /profile
( navigate
)
.
.
.
.
.
src/pages/Login.tsx
import * as React from 'react' import { navigate, RouteComponentProps } from '@reach/router' import { authenticate } from '../api/auth' import { IUserIdentity } from '../models/user' // , // < > const Login: React.FC<RouteComponentProps> = () => { // useStaet , useEffect - , // , state const [user, setField] = React.useState<IUserIdentity>({ username: '', password: '', }) // , ( ) "" const [notification, setNotification] = React.useState<string>('') // e (event) , <input /> // : React.SyntheticEvent<HTMLInputElement> const onInputChange = (fieldName: string) => ( e: React.SyntheticEvent<HTMLInputElement> ): void => { setField({ ...user, [fieldName]: e.currentTarget.value, }) setNotification('') } // e (event) , form // : React.SyntheticEvent<HTMLFormElement> const onSubmit = (e: React.SyntheticEvent<HTMLFormElement>): void => { e.preventDefault() authenticate(user) .then(() => { navigate(`/profile`) // profile }) .catch(err => { if (err.errorText) { setNotification(err.errorText) } else { // tslint:disable-next-line: no-console console.warn('request problem', err) } }) } return ( <> <h2>Login</h2> <form onSubmit={onSubmit}> {notification ? <p>{notification}</p> : null} <input type="text" value={user.username} onChange={onInputChange('username')} /> <input type="text" value={user.password} onChange={onInputChange('password')} /> <button>Login</button> </form> </> ) } export { Login }
, TS — . , , JavaScript.
: useState event
TypeScript' , .
, reach-router , react-router. , , , .
src/components/common/Authenticated.tsx
import * as React from 'react' import { Redirect, RouteComponentProps } from '@reach/router' import { checkAuthStatus } from '../../api/auth' // noThrow - https://reach.tech/router/api/Redirect const Authenticated: React.FC<RouteComponentProps> = ({ children }) => { return checkAuthStatus() ? ( <React.Fragment>{children}</React.Fragment> ) : ( <Redirect to="/login" noThrow={true} /> ) } export { Authenticated }
src/App.tsx
import * as React from 'react' import { Link, RouteComponentProps, Router } from '@reach/router' import { Authenticated } from './components/ommon/Authenticated' import { Home } from './pages/Home' import { Login } from './pages/Login' import { News } from './pages/News' import { Profile } from './pages/Profile' import { checkAuthStatus, logout } from './api/auth' import './App.css' const App: React.FC<RouteComponentProps> = props => { return ( <div className="container"> <h1>TZ #1 with hooks & TypeScript</h1> <nav> <Link to="/"></Link> <Link to="news"></Link>{' '} <Link to="profile"></Link>{' '} {checkAuthStatus() ? <button onClick={logout}></button> : null} </nav> {props.children} </div> ) } const RoutedApp = () => { return ( <Router> <App path="/"> <Home path="/" /> <Login path="/login" /> <News path="/news" /> <Authenticated path="/profile"> <Profile path="/" /> </Authenticated> </App> </Router> ) } export { RoutedApp }
.
. type="password"
.
, . "-", , , react-intl , react-i18next .
, . :
src/localization/formErrors.ts
const formErrors = { ru: { incorrect_login_or_password: ' ', }, en: { incorrect_login_or_password: 'Incorrect login or password', }, } export { formErrors }
<Login />
src/pages/Login.tsx
import * as React from 'react' import { navigate, RouteComponentProps } from '@reach/router' import { authenticate } from '../api/auth' // import { formErrors } from '../localization/formErrors' import { IUserIdentity } from '../models/user' // const lang = 'ru' const Login: React.FC<RouteComponentProps> = () => { // ... const onSubmit = (e: React.SyntheticEvent<HTMLFormElement>): void => { e.preventDefault() authenticate(user) .then(() => { navigate(`/profile`) }) .catch(err => { if (err.errorText) { // ( , ru) setNotification(formErrors[lang][err.errorText]) } else { // tslint:disable-next-line: no-console console.warn('request problem', err) } }) } // ... } export { Login }
, .
TypeScript , , . , index signature ( , StackOverflow ).
interface IFormErrors { [key: string]: { [key: string]: string, }; } const formErrors: IFormErrors = { ru: { incorrect_login_or_password: ' ', }, en: { incorrect_login_or_password: 'Incorrect login or password', }, } export { formErrors }
, . , "".
→ Source Code
TypeScript. , TS . , , "one" "two" ( — union).
" " telegram youtube ( 11 2019).
— Understanding TypeScript's type notation ( Dr.Axel Rauschmayer)
react-typescript-samples LemonCode
Source: https://habr.com/ru/post/443424/
All Articles