Definitelytyped: Implementando defaultProps com verificações nulas estritas do ts 2.0

Criado em 30 set. 2016  ·  50Comentários  ·  Fonte: DefinitelyTyped/DefinitelyTyped

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?

Comentários muito úteis

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>
    )
  }
}

Todos 50 comentários

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):

  1. Requer um HOC, mas como este é um paradigma bastante comum no mundo React, isso parece OK.
  2. Você precisa declarar as props como um parâmetro de tipo genérico e não pode confiar na inferência da propriedade props .
  3. Não há verificação implícita dos tipos de 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.

Esta página foi útil?
0 / 5 - 0 avaliações