As propriedades padrão não parecem funcionar bem atualmente com strictNullChecks habilitado. Por exemplo:
interface TestProps { x?: number}
class Test extends React.Component<TestProps, null> {
static defaultProps = {x: 5};
render() {
const x: number = this.props.x;
return <p>{x}</p>;
}
}
Erros com error TS2322: Type 'number | undefined' is not assignable to type 'number'
mesmo que isso seja garantido para funcionar em tempo de execução.
Agora defaultProps e Props parecem ser tratados como sempre do mesmo tipo, mas na verdade quase nunca são do mesmo tipo porque campos opcionais em Props devem ser substituídos por valores obrigatórios em DefaultProps.
E se houvesse algo como...
class ComponentWithDefaultProps<P, D, S> {
props: P & D & {children?: React.Children};
}
que é idêntico à tipagem React.Component existente, exceto pelo tipo de props?
Como as props padrão são definidas em tempo de execução, não tenho certeza se há uma maneira de lidar com isso bem, além de uma declaração de tipo. (Claro, você sempre pode desabilitar verificações nulas estritas.)
Veja como você pode contornar isso no seu exemplo:
interface TestProps { x?: number}
class Test extends React.Component<TestProps, null> {
static defaultProps = {x: 5};
render() {
const x: number = (this.props.x as number);
return <p>{x}</p>;
}
}
Consulte https://www.typescriptlang.org/docs/handbook/basic-types.html#type -assertions
Se houver uma maneira mais graciosa de lidar com isso, eu adoraria ouvi-la.
Isenção de responsabilidade: estou usando o TypeScript há cerca de três dias, estou cansado e provavelmente não tenho ideia do que estou falando.
+1000 na atualização do arquivo de definição de tipo para incluir três definições de tipo genérico.
Isso costumava ser bom sem ---strictNullChecks
, mas agora definitivamente será um problema para muitas classes de componentes.
O Flow também implementa uma implementação de classe semelhante devido à natureza da verificação de tipo nulo estrita.
https://github.com/facebook/flow/blob/master/lib/react.js#L16
https://github.com/facebook/flow/blob/master/lib/react.js#L104 -L105
Parece que não temos muitas opções aqui, exceto esperar que https://github.com/Microsoft/TypeScript/issues/2175 seja resolvido para adicionar um terceiro genérico.
Eu não acho que uma mudança tão (quebra) (quero dizer class Component<P, S, D>
) possa ser aprovada pelos revisores.
@johnnyreilly @bbenezech @pzavolinsky vocês têm uma opinião sobre isso?
@r00ger concordou. Mudar a definição é muito perturbador.
Alguém já pensou em usar Partial
?
Como em:
interface ComponentClass<P> {
- defaultProps?: P;
+ defaultProps?: Partial<P>;
}
Não se importe com essas coisas Partial
acima.
Parcial só resolve como declarar o problema de propTypes parcial. Dentro de render
, lastName
ainda é do tipo string | undefined
. Para contornar isso, você precisa lançar uma string usando as
ou !
como mostrado abaixo. Funciona, mas não é o ideal.
interface IUser {
firstName: string
lastName?: string
}
export class User extends React.Component<IUser, {}> {
public static defaultProps: Partial<IUser> = {
lastName: 'None',
}
public render () {
const { firstName, lastName } = this.props
// error
lastName.toUpperCase()
return (
<div>{firstName} {lastName}</div>
)
}
}
Acabei de começar a usar o TS. Estou faltando alguma coisa?
Se alguém tiver uma boa solução para tipos e defaultProps, sou todo ouvidos. Atualmente fazemos isso:
interface Props {
firstName: string;
lastName?: string;
}
interface DefaultProps {
lastName: string;
}
type PropsWithDefaults = Props & DefaultProps;
export class User extends React.Component<Props> {
public static defaultProps: DefaultProps = {
lastName: 'None',
}
public render () {
const { firstName, lastName } = this.props as PropsWithDefaults;
return (
<div>{firstName} {lastName}</div>
)
}
}
+1
Atualmente estou lutando contra esse problema.
+1
+1
Além de adicionar o terceiro parâmetro de tipo, você precisará da capacidade de diferenciar props contra props padrão. Felizmente a partir do TS 2.4 isso agora é possível! Consulte https://github.com/Microsoft/TypeScript/issues/12215#issuecomment -319495340
IMHO adicionando terceiro parâmetro é um grande não, não, também a equipe Flow sabia disso e recentemente eles mudaram isso para um bem maior. Deve ser responsabilidade do verificador de tipos saber como lidar com esse tipo de coisa.
Não me entenda mal, eu amo o Typescript, mas desde o Flow 0.53 eu tenho que dizer que é superior para o desenvolvimento do React https://medium.com/flow-type/even-better-support-for-react-in-flow- 25b0a3485627
@Hotell Flow tem três parâmetros de tipo para React.Component
- de acordo com o artigo Medium que você vinculou ao Flow, pode inferir parâmetros de tipo de classe a partir das anotações de subclasse - um recurso de nível de idioma puro que o TS não suporta, mas não um tipo -declaração consideração AFAIK.
@aldendaniels
Flow tem três parâmetros de tipo para React.Component
não, costumava ser assim antes de 0.53, não mais :) https://github.com/facebook/flow/commit/20a5d7dbf484699b47008656583b57e6016cfa0b#diff -5ca8a047db3f6ee8d65a46bba4471236R29
@Hotell Ah, com certeza! Obrigado por me corrigir.
AFAIK não há como no TS inferir o tipo dos adereços padrão. Usando a abordagem de três parâmetros de tipo, provavelmente seríamos capazes de obter a digitação correta sem bloquear as alterações upstream da equipe do TypeScript.
Você conhece uma maneira de usar o tipo inferido de uma propriedade estática sem passar typeof MyComponent.defaultProps
como um parâmetro de tipo?
Alguma novidade sobre este assunto? Alguém faz um PR para adicionar um terceiro parâmetro de tipo e usa https://github.com/Microsoft/TypeScript/issues/12215#issuecomment -319495340?
Upvoting problema: mesmo problema
+1
Eu também me deparei com isso e escolhi (até que isso seja corrigido corretamente) me abster de usar static defaultProps
e, em vez disso, usar um HOC auxiliar:
Componentes do arquivo/helpers/withDefaults.tsx :
import * as React from 'react'
export interface ComponentDefaulter<DP> {
<P extends {[key in keyof DP]?: any}>(Component: React.ComponentType<P>): React.ComponentType<
Omit<P, keyof DP> & // Mandate all properties in P and not in DP
Partial<Pick<P, keyof DP>> // Accept all properties from P that are in DP, but use type from P
>
}
export default function withDefaults<DP>(defaultProps: DP): ComponentDefaulter<DP> {
return Component => props => <Component {...defaultProps} {...props}/>
}
Agora posso usar:
Componentes do arquivo/Button.tsx :
import * as React from 'react'
import withDefaults from './helpers/withDefaults'
export interface ButtonProps {
label: string
onPress: () => any
}
export const defaultProps = {
onPress: () => undefined
}
class Button extends React.Component<ButtonProps> {
// ...
}
export default withDefaults(defaultProps)(Button)
Três desvantagens potenciais (que eu consigo pensar):
props
.defaultProps
, mas isso pode ser corrigido especificando export const defaultProps: Partial<ButtonProps> = {...}
.De acordo com @vsaarinen , eu escrevo uma classe base com props: Props & DefaultProps
, então toda a classe que estende a classe base pode usar diretamente this.props
sem usar this.props as PropsWithDefaults
.
Assim:
import * as React from 'react'
export class Component<P = {}, S = {}, DP = {}> extends React.Component<P, S> {
props: Readonly<{ children?: React.ReactNode }> & Readonly<P> & Readonly<DP>
}
export interface Props {
firstName: string
lastName?: string
}
export interface DefaultProps {
lastName: string
}
export class User extends Component<Props, any, DefaultProps> {
render() {
const { firstName, lastName } = this.props
// no error
return (
<div>{firstName} {lastName.toUpperCase()}</div>
)
}
}
Na verdade , @ qiu8310 que não funcionou totalmente, ainda teve problemas com sites de chamadas gritando sobre esses adereços padrão não serem opcionais. Conseguiu funcionar com um pequeno ajuste
import * as React from 'react'
export class Component<P = {}, S = {}, DP = {}> extends React.Component<P, S> {
// Cast the props as something where readonly fields are non optional
props = this.props as Readonly<{ children?: React.ReactNode }> & Readonly<P> & Readonly<DP>
}
export interface Props {
firstName: string
lastName?: string
}
export interface DefaultProps {
lastName: string
}
export class User extends Component<Props, any, DefaultProps> {
render() {
const { firstName, lastName } = this.props
// no error
return (
<div>{firstName} {lastName.toUpperCase()}</div>
)
}
}
Joguei com terceiro genérico e acabei tendo algo parecido com a proposta do @qiu8310 :
// ComponentWithDefaultProps.ts
import * as React from "react";
export declare class ComponentWithDefaultProps<P, S, DP extends Partial<P>> extends React.Component<P & DP, S> {}
type redirected<P, S, DP> = ComponentWithDefaultProps<P, S, DP>;
const redirected: typeof ComponentWithDefaultProps = React.Component as any;
export const Component = redirected;
// User.ts
import { Component } from "ComponentWithDefaultProps";
export interface Props {
firstName: string
lastName?: string
}
export interface DefaultProps {
lastName: string
}
export class User extends Component<Props, {}, DefaultProps> {
public render() {
const { firstName, lastName } = this.props;
return <div>{firstName} {lastName.toUpperCase()}</div>;
}
}
No entanto, ambas as abordagens (a minha e a abordagem acima) causam o problema maior. Existem tipos de componentes criados no meu exemplo:
User: React.ComponentClass<P & DP>
User["props"]: Readonly<{ children?: React.ReactNode }> & Readonly<P & DP>
Aparentemente, a interface do User
está errada. React.ComponentClass<P & DP>
significa que lastName
também é necessário, de modo que
<User firstName="" />;
// ~~~~~~~~~~~~ Property 'lastName' is missing...
No exemplo do @qiu8310 , os tipos são diferentes:
User: React.ComponentClass<P>
User["props"]: Readonly<{ children?: React.ReactNode }> & Readonly<P> & Readonly<DP>
Mas a mesma parte de JSX causa o mesmo erro, porque as verificações de JSX de tsc
são baseadas em props
'type .
<User firstName="John" />;
// ~~~~~~~~~~~~~~~~ Property 'lastName' is missing...
O engraçado é que <User firstName="John" />
está sendo transformado em React.createElement(User, {firstName: "John"})
que seria um TypeScript válido. Nesse caso, as verificações de tipo dependem do primeiro parâmetro de tipo ComponentClass
, então
<User firstName="Jonh" />; // doesn't work, but
React.createElement(User, { firstName: "John" }); // works
Como você vê, mesmo tendo terceiro genérico ainda temos que adicionar outro truque para exportar um componente com interface correta:
export const User = class extends Component<Props, {}, DefaultProps> {
// ...
} as React.ComponentClass<Props>;
<User firstName="Jonh" />; // works
Portanto, ter um terceiro genérico não faz muito sentido.
Parece que não há uma boa solução que possa ser mesclada com a definição de React
, por enquanto fico usando ComponentWithDefaultProps
e afirmando o tipo de componente exportado.
export interface DefaultProps {
lastName: string;
}
export interface Props extends Partial<DefaultProps> {
firstName: string;
}
export type PropsWithDefault = Props & DefaultProps;
export const User: as React.ComponentClass<Props> =
class extends React.Component<PropsWithDefault> {
render() {
// no error
return <div>
{this.props.firstName}
{this.props.lastName.toUpperCase()}
</div>;
}
};
// Note, we've assigned `React.Component<PropsWithDefault>` to `React.ComponentClass<Props>`
Além disso, você pode declarar todos os usos do tipo this.props
nos métodos do componente (por exemplo const { lastName } = this.props as Props & DefaultProps
, ou usar ponto de exclamação em todos os lugares this.props.lastName!.toLowerCase()
).
eu encontrei alguns exemplos sobre esta discussão - https://github.com/gcanti/typelevel-ts#objectdiff
@rifler , a chamada abordagem HOC (prefiro decorador) está aqui há um tempo , tentamos encontrar uma solução que não adicione sobrecarga de tempo de execução
Ah, ótimo
Espero que você encontre a solução
algum progresso?
O seguinte é uma variação da técnica mencionada por @r00ger :
interface IUser {
name: string;
}
const User = class extends React.Component<IUser> {
public static defaultProps: IUser = {name: "Foo"}
public render() {
return <div>{this.props.name}</div>;
}
} as React.ComponentClass<Partial<IUser>>;
React.createElement(User, {}); // no error, will output "<div>Foo</div>"
Usar o snippet acima funcionará, mas você perderá a capacidade de usar propriedades estáticas em User, já que se torna uma classe anônima. Uma solução hacky seria sombrear o nome da classe, assim:
// tslint:disable-next-line:no-shadowed-variable
const User = class User extends React.Component<IUser>
Agora você pode usar campos estáticos privados dentro da classe. As estáticas públicas ainda são inutilizáveis. Além disso, observe a necessidade de silenciar tslint.
Achei que vale a pena mencionar que a partir do TS 2.8, o tipo Exclude
é oficialmente suportado:
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
Consulte https://github.com/Microsoft/TypeScript/pull/21847.
Então, tudo o que precisamos é que React.createElement()
exija o seguinte em vez de Props
:
Omit<Props, keyof DefaultProps>
O único problema é que nas declarações do React, não existe o tipo DefaultProps
- para isso, precisamos de um terceiro parâmetro de tipo OU da capacidade de inferir o tipo de membros estáticos como um recurso de linguagem.
Enquanto isso, estamos rolando com o seguinte:
/**
* The Create type allow components to implement a strongly thed create() function
* that alows the caller to omit props with defaults even though the component expects
* all props to be populated. The TypeScript React typings do not natively support these.
*/
export type Create<C extends BaseComponent<any, any>, D extends {} = {}> = (
props?: typeHelpers.ObjectDiff<C['props'], D> & React.ClassAttributes<C>,
...children: React.ReactNode[]
) => React.ComponentElement<any, any>;
export interface DomPropsType {
domProps?: domProps.DomProps;
}
export class BaseComponent<P, S = {}> extends React.Component<P & DomPropsType, S> {
static create(props?: object, ...children: React.ReactNode[]) {
return React.createElement(this, props, ...children);
}
constructor(props: P & DomPropsType, context?: any) {
...
}
E todos os nossos componentes se parecem com:
export class InsertObjectMenu extends BaseComponent<Props, State> {
static create: Create<InsertObjectMenu, typeof InsertObjectMenu.defaultProps>;
static defaultProps = {
promptForImageUpload: true,
};
...
}
Finalmente, temos uma regra lint impondo que o atributo create
seja declarado em todos os componentes. Nós não usamos JSX, então usamos:
InsertObjectMenu.create({...})
Em vez de React.createElement()
.
Estamos usando essa abordagem em uma grande base de código há quase um ano com bom sucesso, mas adoraríamos adotar o JSX e é isso que está nos impedindo.
Tanto tempo investido nesta "questão simples" . Vou deixar isso aqui https://medium.com/@martin_hotell/ultimate -react-component-patterns-with-typescript-2-8-82990c516935 🖖
interface Component<P = {}, S = {}, DP extends Partial<P>=P> extends ComponentLifecycle<P, S> { }
class Component<P, S, DP extends Partial<P> = P> {
constructor(props: P & DP, context?: any);
// We MUST keep setState() as a unified signature because it allows proper checking of the method return type.
// See: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment-351013257
// Also, the ` | S` allows intellisense to not be dumbisense
setState<K extends keyof S>(
state: ((prevState: Readonly<S>, props: P) => (Pick<S, K> | S | null)) | (Pick<S, K> | S | null),
callback?: () => void
): void;
forceUpdate(callBack?: () => void): void;
render(): ReactNode;
// React.Props<T> is now deprecated, which means that the `children`
// property is not available on `P` by default, even though you can
// always pass children as variadic arguments to `createElement`.
// In the future, if we can define its call signature conditionally
// on the existence of `children` in `P`, then we should remove this.
private __externalProps: Readonly<{ children?: ReactNode }> & Readonly<P>;
props: Readonly<{ children?: ReactNode }> & Readonly<P> & DP;
state: Readonly<S>;
context: any;
refs: {
[key: string]: ReactInstance
};
}
class PureComponent<P = {}, S = {}, DP extends Partial<P>=P> extends Component<P, S, P> { }
interface ElementAttributesProperty { __externalProps: {}; }
Olhe atentamente para a última linha.
Com essas mudanças poderíamos ter
interface Props {
a: string
b?: string
c?: string
}
class Comp extends React.Component<Props, {}, typeof Comp.defaultProps> {
static defaultProps = {
b: ''
}
render() {
const {a, b, c} = this.props
let res = a.concat(b) // ok
let res1 = a.concat(c) //fail
return null
}
}
const res1= <Comp a=''/> // ok
const res3 = <Comp /> // fail
Qual é o melhor que podemos obter se usar static defaultProps
(o verificador ts deve ser alterado se quisermos omitir typeof Comp.defaultProps
).
Outras opções, já foi dito - HOC, tipo casts.
Aqui está minha tentativa (muito feia) baseada na ideia de https://medium.com/@martin_hotell/ultimate -react-component-patterns-with-typescript-2-8-82990c516935:
type ExtractProps<T> = T extends React.ComponentType<infer Q> ? Q : never;
type ExtractDefaultProps<T> = T extends { defaultProps?: infer Q } ? Q : never;
type RequiredProps<P, DP> = Pick<P, Exclude<keyof P, keyof DP>>;
type RequiredAndPartialDefaultProps<RP, DP> = Required<RP> & Partial<DP>;
type ComponentTypeWithDefaultProps<T> =
React.ComponentType<
RequiredAndPartialDefaultProps<
RequiredProps<ExtractProps<T>, ExtractDefaultProps<T>>,
ExtractDefaultProps<T>
>
>;
function withDefaultProps<T extends React.ComponentType<any>>(Comp: T) {
return Comp as ComponentTypeWithDefaultProps<T>;
}
interface IProps {
required: number;
defaulted: number;
}
class Foo extends React.Component<IProps> {
public static defaultProps = {
defaulted: 0,
};
}
// Whichever way you prefer... The former does not require a function call
const FooWithDefaultProps = Foo as ComponentTypeWithDefaultProps<typeof Foo>;
const FooWithDefaultProps = withDefaultProps(Foo);
const f1 = <FooWithDefaultProps />; // error: missing 'required' prop
const f2 = <FooWithDefaultProps defaulted={0} />; // error: missing 'required' prop
const f3 = <FooWithDefaultProps required={0} />; // ok
const f4 = <FooWithDefaultProps required={0} defaulted={0} />; // ok
@decademoon , parece que poderíamos usar essa solução em @types/react
, podemos? Quero dizer, se substituirmos o usual React.ComponentType
pela sua solução.
Se assim for, talvez você possa criar um PR?
@decademoon sua definição não lida com o caso em que as props não padrão realmente incluem campos opcionais, ou seja
interface IProps {
required: number;
notRequired?: () => void;
defaulted: number;
}
class Foo extends React.Component<IProps> {
public static defaultProps = {
defaulted: 0,
};
}
Eu consegui trabalhar no meu caso alterando seu tipo RequiredAndPartialDefaultProps para não envolver o "RP" com "Required"
type RequiredAndPartialDefaultProps<RP, DP> = RP & Partial<DP>;
Estou surpreso que ainda não exista uma solução adequada ou pelo menos um HOC funcional no NPM; a menos que eu tenha perdido alguma coisa.
Olá a todos. Só queria dizer e se você ainda está lendo este tópico: acho que @JoshuaToenyes fez a explicação mais significativa e útil. Isso definitivamente não é um problema, então não há nada a ver com isso. Use asserção de tipo neste caso.
@toiletpatrol , na verdade, a solução de @decademoon (com minha pequena alteração) lida automaticamente com adereços padrão muito bem. Definitivamente, poderia ser mesclado nas definições de DT para React para fornecer o padrão de recursos para todos.
@toiletpatrol @RobRendell você viu https://github.com/Microsoft/TypeScript/issues/23812?
@vkrol Eu vi isso, mas posso descartar a implementação do decademoon na minha base de código agora mesmo sem esperar por lançamentos de novos recursos.
Outra solução alternativa que estou usando por enquanto para casos complicados:
const restWithDefaults = { ...Component.defaultProps, ...rest };
return <Component {...restWithDefaults} />;
Não há nada de errado com isso, eu acho, então estou deixando aqui como uma solução suja, mas simples.
As tipagens TS 3.2 e react 16.7 estão corrigindo isso. podemos fechar?
@Hotell como deve ser tratado eventualmente? Eu ainda não consigo fazer isso funcionar corretamente
Para economizar algum tempo, aqui está um link para as notas de lançamento do Typescript 3:
Suporte para defaultProps em JSX
@cbergmiller Receio que essas sejam as notas de lançamento do TypeScript 3.1 🙃
ainda tendo o mesmo problema com React.FunctionComponent
@denieler Eu não aconselharia usar defaultProps
com React.FunctionComponent
, não é natural. É melhor usar os parâmetros de função padrão:
interface HelloProps {
name?: string;
surname?: string;
}
const HelloComponent: React.FunctionComponent<HelloProps> = ({
name = 'John',
surname = 'Smith',
}) => {
return <div>Hello, {name} {surname}!</div>
};
@mgol Como você definiria os parâmetros de função padrão se eu não quisesse desestruturar os adereços?
Só consigo pensar em desestruturar apenas as propriedades "padrão" assim:
interface HelloProps {
name?: string;
surname?: string;
}
const HelloComponent: React.FunctionComponent<HelloProps> = ({
name = 'John',
surname = 'Smith',
...props
}) => {
return <div>Hello, {name} {surname}! You are {props.age} years old.</div>
};
Mas acho vergonhoso extrair apenas alguns dos adereços.
@glecetre Você pode usar:
HelloComponent.defaultProps = {
name: 'John',
surname: 'Smith'
}
@Glinkis , por favor, observe https://github.com/reactjs/rfcs/pull/107/files#diff -20b9b769068a185d90c23b58a2095a9dR184.
@glecetre Por que você não quer desestruturar todos os adereços? É mais simples do que definir defaultProps
e mais fácil de digitar. O tipo de props do componente baseado em classe pode morder você se você exportar para usar externamente, pois as props que são necessárias podem não ser mais necessárias se houver uma entrada para elas em defaultProps
. Usar defaultProps
também parece um pouco mágico enquanto na desestruturação de parâmetros é tudo JavaScript.
Comentários muito úteis
Se alguém tiver uma boa solução para tipos e defaultProps, sou todo ouvidos. Atualmente fazemos isso: