Olá Habr, hoje trabalharemos com TypeScript e React-hooks. Este tutorial o ajudará a entender o básico do "script" e a trabalhar em uma tarefa de teste para o front-end .
As tarefas de teste no projeto "Sem água" são uma oportunidade para obter uma revisão de código. O prazo para a atribuição atual é 11 de abril de 2019.

Versão em vídeo
Se você estiver com preguiça de ler, participe do seminário on-line em 20 de março às 21:00, horário de Moscou. Registro (sem e-mails e sms). Webinar realizado, gravação de webinar.
Preparação
Para começar, você pode usar a versão TypeScript Create-react-app ou usar o meu starter (que já inclui o roteador de alcance)
Vou usar meu iniciador (mais sobre isso na seção "Prática").
Teoria TypeScript
O TS resolve o problema da "digitação dinâmica" em JavaScript, quando sua variável pode assumir valores diferentes. Agora uma linha, depois um número ou mesmo um objeto. Era tão conveniente escrever no "século 19", no entanto, agora todos concordam que, se você tiver tipos predefinidos (considere regras), a base de código é mais fácil de manter. E há menos bugs no estágio de desenvolvimento.
Por exemplo, se você tiver um componente que exibe um item de notícia, podemos especificar o seguinte tipo para o item de notícia:
Assim, especificamos tipos "estáticos" estritos para as propriedades do nosso objeto "notícias". Se tentarmos obter uma propriedade inexistente, o TypeScript mostrará um erro.
import * as React from 'react' import { INewsItem } from '../models/news'

Além disso, o Visual studio Code e outros editores avançados mostrarão um erro:

Visualmente, conveniente. No meu caso, o VS Code mostra dois erros ao mesmo tempo: o tipo da variável não está definido (ou seja, não existe em nossas notícias sobre "interface") e "a variável não é usada". Além disso, as variáveis não utilizadas ao usar o TypeScript são destacadas em cores claras no VS Code, por padrão.
Aqui vale a pena indicar em uma linha o motivo de uma integração tão estreita do TypeScript e do VS Code: ambos os produtos são o desenvolvimento da Microsoft.
O que mais o TypeScript pode nos dizer imediatamente? Falando no contexto de variáveis, é isso. TS é muito poderoso, ele entende o que é o quê.
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> ) }

Aqui, o TypeScript jura imediatamente em uma propriedade inexistente - toUpperCase
do number
do tipo. E como sabemos, de fato, apenas os tipos de string têm métodos toUpperCase () .
Agora imagine, você começa a escrever o nome de alguma função, abre o colchete e o editor mostra imediatamente uma janela de ajuda pop-up, que indica quais argumentos e que tipo podem ser passados para a função.
Ou imagine: você seguiu rigorosamente as recomendações e a digitação no seu projeto é à prova de balas. Além da substituição automática, você se livra do problema com valores implícitos ( undefined
) no projeto.
Prática
Reescrevemos a primeira tarefa de teste em react -hooks + TypeScript. Por enquanto, vamos omitir o Redux , caso contrário, em vez de trabalhar no " TK reiniciado nº 1 " , basta copiar tudo daqui.
Toolkit
(para quem usa o código VS )
Por conveniência, recomendo que você instale a extensão TSLint .
Para ativar a correção automática de erro do TSLint no momento do salvamento, adicione as configurações do editor:
Você pode acessar as configurações através do menu ou ver onde elas moram fisicamente em seu sistema operacional.
As configurações do TSLint são padrão, mais eu desabilitei uma regra.
{ "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"], "linterOptions": { "exclude": [ "node_modules/**/*.ts", "src/serviceWorker.js" ] }, "rules": { "object-literal-sort-keys": false
A integração acabou!
Estamos escrevendo um aplicativo
Vamos nos familiarizar com coisas novas no decorrer da peça. Para começar, clone sua ramificação inicial ou sincronize com o meu código no seu código.
Todos os arquivos de script do tipo react têm a extensão .tsx .
O que é interessante no modelo inicial?
- gancho de pré-confirmação (já discutimos isso: texto , vídeo )
- TSLint (substituição de ESLint )
- arquivos a serem iniciados e dependências para esta e as próximas etapas
Vamos começar com o 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, começo padrão. Vamos tentar adicionar alguma propriedade ao <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" /> }
Temos o erro:

(se você não recebeu o erro, verifique o rigor das configurações do tsconfig.json, deve haver uma regra noImplicitAny)
Você já concluiu pela tradução do texto do erro que nossas propriedades não devem ser do tipo nenhuma . Este tipo pode ser traduzido como "qualquer coisa". Nosso projeto tem uma regra que proíbe saída implícita desse tipo.
- Inferência implícita de tipo?
Exatamente! O TypeScript é capaz de deduzir o tipo de uma variável por padrão e lida com isso também. Isso é chamado de inferência de tipo.
Um exemplo:
let x = 3
No caso de props
- TS não pode determinar o tipo da variável em 100% e, portanto, diz - deixe que seja
(ou seja, digite any
). Isso é feito implicitamente e é proibido pela regra noImplicitAny nas configurações do projeto (tsconfig.json)
Podemos especificar explicitamente o tipo any e o erro desaparecerá. O tipo da variável é indicado por dois pontos.
Feito, não há erros, o projeto funciona, mas qual é a utilidade dessa digitação quando props
podem ser alguma coisa? Temos certeza de que nosso nome é uma
. A regra segue disso:
Tente evitar digitar qualquer
Há momentos em que any
necessária e isso é normal, mas é imediatamente um golpe abaixo da cintura por digitação estrita.
Para descrever os tipos de props
, usaremos a palavra-chave da interface
:
Tente alterar o tipo de name
para number
e ocorrerá um erro imediatamente.

Além disso, o erro também será enfatizado no VS Code (e em muitos outros editores). O erro indica que não temos uma correspondência: passamos uma string, mas esperamos um número.
Vamos props
e adicionar outros props
- site ao <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" /> }
Ocorreu um erro:
Type error: Property 'site' does not exist on type 'IAppProps'. TS2339
A propriedade do site
não existe no tipo IAppProps
. Aqui, quero dizer imediatamente que, pelo nome do tipo, entendemos imediatamente onde procurar. Portanto, nomeie os tipos corretamente.
Antes de corrigi-lo, vamos fazer o seguinte: exclua o parágrafo que props.site
.
Temos outro texto de erro:

Aqui, quero observar apenas o que o TS deduziu: site
é um tipo de string
(isso é sublinhado na captura de tela).
Fix:
interface IAppProps { name: string; site: string;
Sem erros, sem problemas.
Para trabalhar com o roteamento, precisamos renderizar filhos. Vamos nos antecipar e tentar desenhar um "componente filho".
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 jura, eles dizem isso e aquilo - as children
não children
descritas no IAppProps
.

Obviamente, não queremos "tipificar" algumas coisas comuns, e aqui a comunidade vem em socorro, que já digitou muito antes de nós. Por exemplo, o pacote @ types / react contém toda a digitação para react.
Ao instalar este pacote (no meu exemplo, ele já está instalado), podemos usar a seguinte entrada:
React.FunctionComponent<P> React.FC<P>
onde <P>
são os tipos de nossos props
, ou seja, o registro assumirá o formato.
React.FC<IAppProps>
Para quem gosta de ler grandes volumes de texto, antes de praticar, posso oferecer um artigo sobre " genéricos " (aqueles <e>). Quanto ao resto, por enquanto, basta traduzirmos esta frase assim: um componente funcional que aceita <tais e tais propriedades>.
A entrada para o componente App mudará um pouco. Versão completa.
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> ) }
Vamos analisar a seguinte linha em caracteres:
const App: React.FC<IAppProps> = props => {
- Por que o tipo desapareceu após props
?
- Porque, após o App
- adicionado.
Registramos que a variável App será do tipo: React.FC<IAppProps>
.
React.FC
é um tipo de "função" e, dentro de <>, indicamos que tipo de argumento é necessário, ou seja, indicamos que nossos props
serão do tipo IAppProps
.
(existe o risco de mentir um pouco para você em termos de termos, mas para simplificar o exemplo, acho que está tudo bem)
Total: aprendemos a especificar o tipo de propriedades para os props
transmitidos, sem perder "suas" propriedades dos componentes React.
O código fonte no momento.
Adicionar roteamento
Usaremos o roteador de alcance para ampliar nossos horizontes. Este pacote é muito semelhante ao reag-router.
Adicione a página - Notícias (Notícias), limpe o <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'
O aplicativo quebrou, um erro (um dos erros, pois o terminal mostra o primeiro):

Já estamos acostumados a esse registro um pouco e entendemos que esse path
não existe na descrição do tipo para <App />
.
Novamente, tudo está descrito diante de nós. Usaremos o pacote @ types / reach__router e o tipo RouteComponentProps
. Para não perder nossas propriedades, usaremos a extends
.
import * as React from 'react'
Para os curiosos, que tipos são descritos em RouteComponentProps .
O erro em <App />
desapareceu, mas permaneceu em <News />
, pois não especificamos a digitação para este componente.
Mini quebra-cabeça: especifique a digitação para <News />
. No momento, apenas as propriedades do roteador são transferidas para lá.
A resposta é:
src / Pages / News.tsx
import * as React from 'react' import { RouteComponentProps } from '@reach/router'

Seguimos em frente e adicionamos uma rota com um parâmetro. Os parâmetros no roteador de alcance vivem diretamente em adereços. No react-router, como você se lembra, eles vivem em 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'
src / páginas / 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 }
Um erro que não esperávamos:

A propriedade source não existe ... Por um lado, perplexidade: passamos para o caminho, que é string, por outro, alegria: Ah, bem, como os autores da biblioteca e a digitação tentaram nos adicionar esse aviso.
Para corrigi-lo, usaremos uma das opções : estender de RouteComponentProps
e especificar a propriedade de source
opcional. Opcional, porque pode não estar no nosso URL.
O TypeScript usa um ponto de interrogação para indicar uma propriedade opcional.
src / páginas / About.tsx
import * as React from 'react' import { RouteComponentProps } from '@reach/router' interface IAboutProps extends RouteComponentProps { source?: string;
src / App.tsx (ao mesmo tempo, russificaremos a navegação)
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 : aprendemos a tipificar os componentes envolvidos na rotação.
O código fonte no momento.
Vamos trabalhar com ganchos e continuar digitando
Lembro que nossa tarefa é implementar uma tarefa de teste , mas sem o Redux.
Eu preparei uma ramificação para iniciar esta etapa com roteamento, um formulário de login que não funciona e as páginas necessárias.

Download de notícias
Notícias é uma variedade de objetos.
Apresente nossas novidades:
{ 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'), },
Vamos escrever um modelo imediatamente (tipos de notícias):
src / models / news.ts (extensão .ts )
export interface INewsItem { id: number; title: string; text: string; link: string; timestamp: Date; }
No novo registro de data e hora, indicava o tipo Date
.
Imagine nossa chamada de dados:
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,
Nossa ligação da api getNews
retorna Promise , e essa "Promise" tem um certo tipo, que também podemos descrever:
interface INewsResponse { status: number;
Está quente Agora ficará ainda mais quente, já que o tipo Promise é genérico, teremos que lidar novamente com <
e >
. Esta é a parte mais difícil do tutorial, então lemos o código final:
src / api / News.ts
import { INewsItem } from '../models/news'
Pausa para fumar.
Mostrar notícias
src / pages / News.tsx
import * as React from 'react' import { RouteComponentProps } from '@reach/router' import { getNews } from '../api/news' import { NewsItem } from '../components/NewsItem'
O código fornece comentários relacionados ao TypeScript. Se precisar de ajuda com os ganchos de reação, você pode ler aqui: documentação (EN), tutorial (RU).
Tarefa: escreva um componente <NewsItem />
que exibirá notícias. Lembre-se de especificar o tipo correto. Use o modelo INewsItem
.
O resultado pode ser assim:

A solução está abaixo.
src / components / NewsItem.tsx
import * as React from 'react' import { INewsItem } from '../models/news' interface INewsItemProps { data: INewsItem;
A questão é por que descrevemos a interface dessa maneira (comentários nos códigos [1] e [2]). Poderíamos simplesmente escrever:
React.FC<INewsItem>
A resposta está abaixo.
.
.
.
Poderia, já que estamos passando as notícias na propriedade data
, ou seja, devemos escrever:
React.FC<{ data: INewsItem }>
O que fizemos, apenas com a diferença que especificamos interface
, caso outras propriedades fossem adicionadas ao componente. E a legibilidade é melhor.
Total: praticamos a digitação para Promise e useEffect. Descreveu o tipo para outro componente.
Se você ainda não bate palmas contra a substituição automática e o rigor do TS, não pratica ou a digitação estrita não é para você. No segundo caso - não posso ajudar em nada, isso é uma questão de gosto. Chamo a atenção para o fato de que o mercado dita suas condições e mais e mais projetos convivem com a digitação, se não com o TypeScript, e com o fluxo .
O código fonte no momento.
API de autenticação
Escreva api
para login. O princípio é semelhante - retornamos uma promise
com a resposta autorizada / erro. Armazenaremos informações sobre o status da autorização no localStorage
( muitos consideram isso uma violação flagrante da segurança, estou um pouco atrás das disputas e não sei como terminou ).
Em nossa aplicação, o login é um monte de nome de usuário e senha (ambos são string), descreveremos o modelo:
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'
, .
:
: . 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'
, 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'
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'
, .

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 }
, . , "".

→
TypeScript. , TS . , , "one" "two" ( — union).
, — .
" " telegram youtube ( 11 2019).
Obrigado pela atenção! , :

CRA + TypeScript
TypeScript Playground
— Understanding TypeScript's type notation ( Dr.Axel Rauschmayer)
Microsoft,
TSLint
tslint
tslint
tsconfig.json tslint.json
d.ts .ts
, staging .
react-typescript-samples LemonCode
:
es5 (!)
React v16
typescript-.
Microsoft. UI- Fabric . , github .