Definitelytyped: Реализация defaultProps со строгими нулевыми проверками ts 2.0

Созданный на 30 сент. 2016  ·  50Комментарии  ·  Источник: DefinitelyTyped/DefinitelyTyped

Свойства по умолчанию в настоящее время не работают должным образом с включенным strictNullChecks. Например:

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

Ошибки с error TS2322: Type 'number | undefined' is not assignable to type 'number' , хотя это гарантированно работает во время выполнения.

Прямо сейчас defaultProps и Props, кажется, всегда рассматриваются как один и тот же тип, но на самом деле они почти никогда не бывают одного и того же типа, потому что необязательные поля в Props должны быть перезаписаны обязательными значениями в DefaultProps.

Что, если бы было что-то вроде...

class ComponentWithDefaultProps<P, D, S> {
    props: P & D & {children?: React.Children};
}

который идентичен существующему типу React.Component, за исключением типа реквизита?

Самый полезный комментарий

Если у кого-то есть хорошее решение для типов и defaultProps, я внимательно слушаю. В настоящее время мы делаем это:

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

Все 50 Комментарий

Поскольку реквизиты по умолчанию устанавливаются во время выполнения, я не уверен, что есть способ хорошо с этим справиться, кроме утверждения типа. (Конечно, вы всегда можете просто отключить строгие проверки нулей.)

Вот как вы можете обойти это в своем примере:

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

См. https://www.typescriptlang.org/docs/handbook/basic-types.html#type -assertions.

Если есть более изящный способ справиться с этим, я бы хотел его услышать.

Отказ от ответственности: я использую TypeScript около трех дней, я устал и, вероятно, понятия не имею, о чем говорю.

+1000 при обновлении файла определения типа для включения трех общих определений типов.

Раньше это было нормально без ---strictNullChecks , но теперь это определенно станет проблемой для многих классов компонентов.

Flow также реализует аналогичную реализацию класса из-за природы строгой проверки нулевого типа.
https://github.com/facebook/flow/blob/master/lib/react.js#L16
https://github.com/facebook/flow/blob/master/lib/react.js#L104 -L105

Похоже, у нас здесь не так много вариантов, кроме ожидания разрешения https://github.com/Microsoft/TypeScript/issues/2175 , чтобы добавить третий универсальный.
Я не думаю, что такое (критическое) изменение (я имею в виду class Component<P, S, D> ) может быть одобрено рецензентами.
@johnnyreilly @bbenezech @pzavolinsky у вас есть мнение по этому поводу?

@r00ger согласился. Изменение определения слишком разрушительно.

Кто-нибудь рассматривал возможность использования Partial ?

Как в:

    interface ComponentClass<P> {
-        defaultProps?: P;
+        defaultProps?: Partial<P>;
    }

Не обращайте внимания на то, что выше Partial .

Частичный решает только то, как объявить проблему частичного propTypes. Внутри render lastName по-прежнему имеет тип string | undefined . Чтобы обойти это, вы должны преобразовать его в строку, используя as или ! , как показано ниже. Это работает, но не идеально.

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

Я только начал пользоваться ТС. Я что-то упустил?

Если у кого-то есть хорошее решение для типов и defaultProps, я внимательно слушаю. В настоящее время мы делаем это:

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
В настоящее время я борюсь с этой проблемой.

+1

+1

В дополнение к добавлению параметра третьего типа вам понадобится возможность сравнивать реквизиты с реквизитами по умолчанию. К счастью, с TS 2.4 это стало возможным! См. https://github.com/Microsoft/TypeScript/issues/12215#issuecomment -319495340.

ИМХО, добавление третьего параметра — это большое, нет, нет, также команда Flow знала об этом и недавно они изменили это для большего блага. Ответственность за проверку типов должна заключаться в том, чтобы знать, как обращаться с такими вещами.

Не поймите меня неправильно, я люблю Typescript, но, начиная с Flow 0.53, я должен сказать, что он лучше подходит для разработки React https://medium.com/flow-type/even-better-support-for-react-in-flow- 25b0a3485627

@Hotell Flow имеет три параметра типа для React.Component — согласно статье Medium, которую вы связали с Flow, можно вывести параметры типа класса из аннотаций подкласса — изящная функция на уровне языка, которую TS не поддерживает, но не тип -декларация рассмотрена AFAIK.

@aldendaniels

Flow имеет три параметра типа для React.Component.

нет, раньше так было до 0.53 , теперь уже нет :)

@Hotell Ах, конечно! Спасибо, что поправили меня.

Насколько я знаю, в TS нет возможности определить тип реквизита по умолчанию. Используя подход с тремя параметрами типа, мы, вероятно, сможем получить правильную типизацию, не блокируя исходные изменения от команды TypeScript.

Знаете ли вы способ использовать предполагаемый тип статического свойства без передачи typeof MyComponent.defaultProps в качестве параметра типа?

Есть новости на эту тему? Кто-нибудь делает PR, чтобы добавить параметр третьего типа и использовать https://github.com/Microsoft/TypeScript/issues/12215#issuecomment -319495340?

Проблема с голосованием: та же проблема

+1

Я также столкнулся с этим и решил (пока это не будет исправлено должным образом) воздерживаться от использования static defaultProps и вместо этого использовать вспомогательный HOC:

Компоненты файла/помощники/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}/>
}

Теперь я могу использовать:

Компоненты файла/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)

Три потенциальных недостатка (о которых я могу думать):

  1. Для этого требуется HOC, но поскольку это довольно распространенная парадигма в мире React, все в порядке.
  2. Вы должны объявить свойства как параметр универсального типа и не можете полагаться на вывод из свойства props .
  3. Нет неявной проверки типов defaultProps , но это можно исправить, указав export const defaultProps: Partial<ButtonProps> = {...} .

Согласно @vsaarinen , я пишу базовый класс с props: Props & DefaultProps , поэтому весь класс, который расширяет базовый класс, может напрямую использовать this.props без использования this.props as PropsWithDefaults .

Так:

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

На самом деле @ qiu8310 , который не работал полностью, все еще были проблемы с сайтами звонков, кричащими о том, что эти реквизиты по умолчанию не являются необязательными. Заработало с небольшой доработкой

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

Я играл с третьим дженериком и получил что-то похожее на предложение @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>;
  }
}

Однако оба этих подхода (мой и описанный выше) вызывают большую проблему. В моем примере есть типы создаваемого компонента:

User: React.ComponentClass<P & DP>
User["props"]: Readonly<{ children?: React.ReactNode }> & Readonly<P & DP>

Судя по всему, интерфейс User неверен. React.ComponentClass<P & DP> означает, что lastName также требуется, так что

<User firstName="" />;
//    ~~~~~~~~~~~~  Property 'lastName' is missing...

В примерах @qiu8310 разные типы:

User: React.ComponentClass<P>
User["props"]: Readonly<{ children?: React.ReactNode }> & Readonly<P> & Readonly<DP>

Но та же часть JSX вызывает ту же ошибку, потому что проверки JSX tsc основаны на типе props ' .

<User firstName="John" />;
//    ~~~~~~~~~~~~~~~~  Property 'lastName' is missing...

Забавно то, что <User firstName="John" /> превращается в React.createElement(User, {firstName: "John"}) , который будет действительным TypeScript. В этом случае проверки типа полагаются на параметр первого типа ComponentClass , поэтому

<User firstName="Jonh" />; // doesn't work, but
React.createElement(User, { firstName: "John" }); // works

Как видите, даже имея третий дженерик, нам все равно придется добавить еще одну хитрость, чтобы экспортировать компонент с корректным интерфейсом:

export const User = class extends Component<Props, {}, DefaultProps> {
    // ...
} as React.ComponentClass<Props>;

<User firstName="Jonh" />; // works

Так что иметь третий дженерик не имеет особого смысла.

Кажется, нет хорошего решения, которое можно объединить с определением React , пока я придерживаюсь использования ComponentWithDefaultProps и утверждения типа экспортируемого компонента.

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>`

Кроме того, вы можете утверждать каждое использование типа this.props в методах компонента (например, const { lastName } = this.props as Props & DefaultProps или везде использовать восклицательный знак this.props.lastName!.toLowerCase() ).

я нашел несколько примеров этого обсуждения - https://github.com/gcanti/typelevel-ts#objectdiff

@rifler , так называемый подход HOC (я предпочитаю декоратор) , существует уже некоторое время , мы пытаемся найти решение, которое не добавляет накладных расходов во время выполнения.

О, круто
Надеюсь, вы найдете решение

какой-либо прогресс?

Ниже приведен вариант техники, упомянутой @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>"

Использование приведенного выше фрагмента будет работать, но вы потеряете возможность использовать статические свойства для пользователя, поскольку он становится анонимным классом. Хакерским решением было бы скрыть имя класса, например:

// tslint:disable-next-line:no-shadowed-variable
const User = class User extends React.Component<IUser>

Теперь вы можете использовать закрытые статические поля внутри класса. Общедоступная статика по-прежнему непригодна для использования. Также обратите внимание на необходимость отключить tslint.

Я подумал, что стоит упомянуть, что начиная с TS 2.8 официально поддерживается тип Exclude :

type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;

См . https://github.com/Microsoft/TypeScript/pull/21847.

Итак, все, что нам нужно, это чтобы React.createElement() требовало следующее вместо Props :

Omit<Props, keyof DefaultProps>

Единственная проблема заключается в том, что в объявлениях React нет типа DefaultProps — для этого нам нужен либо третий параметр типа, либо возможность выводить тип статических членов как языковая функция.

А пока мы работаем со следующим:

/**
 * 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) {
  ...
}

И все наши компоненты выглядят так:

export class InsertObjectMenu extends BaseComponent<Props, State> {
  static create: Create<InsertObjectMenu, typeof InsertObjectMenu.defaultProps>;
  static defaultProps = {
    promptForImageUpload: true,
  };
  ...
}

Наконец, у нас есть правило lint, согласно которому атрибут create объявляется во всех компонентах. Мы не используем JSX, поэтому используем:

InsertObjectMenu.create({...})

Вместо React.createElement() .

Мы успешно используем этот подход в большой кодовой базе уже почти год, но мы хотели бы внедрить JSX, и именно это нас сдерживает.

Столько времени вложено в этот "простой вопрос". Я просто оставлю это здесь 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: {}; }

Посмотрите внимательно на последнюю строку.

С этими изменениями мы могли бы иметь

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

Что лучше всего мы можем получить, используя static defaultProps (проверка ts должна быть изменена, если мы хотим опустить typeof Comp.defaultProps ).
Другие варианты, уже было сказано - HOC, типа слепки.

Вот моя (очень уродливая) попытка, основанная на идее из 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 кажется, мы могли бы просто использовать это решение на @types/react , не так ли? Я имею в виду, если мы заменим обычное React.ComponentType вашим решением.
Если это так, может быть, вы можете создать PR?

@decademoon ваше определение не обрабатывает случай, когда реквизиты не по умолчанию фактически включают необязательные поля, т.е.

interface IProps {
  required: number;
  notRequired?: () => void;
  defaulted: number;
}

class Foo extends React.Component<IProps> {
  public static defaultProps = {
    defaulted: 0,
  };
}

В моем случае это сработало, изменив ваш тип RequiredAndPartialDefaultProps, чтобы не оборачивать «RP» в «Required».

type RequiredAndPartialDefaultProps<RP, DP> = RP & Partial<DP>;

Я удивлен, что до сих пор нет правильного решения или, по крайней мере, работающего HOC для NPM; если я что-то не пропустил.

Всем привет. Просто хотел сказать, и если вы все еще читаете эту ветку: я думаю, что @JoshuaToenyes сделал наиболее значимое и полезное объяснение. Это определенно не проблема, так что здесь нет ничего общего. В этом случае используйте утверждение типа.

@toiletpatrol на самом деле, решение @decademoon (с моей небольшой поправкой) автоматически прекрасно обрабатывает реквизиты по умолчанию. Его определенно можно объединить с определениями DT для React, чтобы предоставить стандартную функцию для всех.

@toiletpatrol @RobRendell ты видел https://github.com/Microsoft/TypeScript/issues/23812?

@vkrol Я это видел, но я могу бросить реализацию decademoon в свою кодовую базу прямо сейчас, не дожидаясь выпуска новых функций.

Другой обходной путь, который я сейчас использую для сложных случаев:

const restWithDefaults = { ...Component.defaultProps, ...rest };
return <Component {...restWithDefaults} />;

Я думаю, в этом нет ничего плохого, поэтому я оставлю это здесь как грязный, но простой обходной путь.

TS 3.2 и react 16.7 это исправляют. мы можем закрыть?

@Hotell , как с этим поступить в конечном итоге? Я все еще не могу заставить это работать должным образом

Чтобы сэкономить время другим, вот ссылка на примечания к выпуску Typescript 3:
Поддержка defaultProps в JSX

@cbergmiller Боюсь, это примечания к выпуску TypeScript 3.1 🙃

все еще та же проблема с React.FunctionComponent

@denieler Я бы не советовал использовать defaultProps с React.FunctionComponent , это неестественно. Лучше использовать параметры функции по умолчанию:

interface HelloProps {
  name?: string;
  surname?: string;
}

const HelloComponent: React.FunctionComponent<HelloProps> = ({
  name = 'John',
  surname = 'Smith',
}) => {
  return <div>Hello, {name} {surname}!</div>
};

@mgol Как бы вы определили параметры функции по умолчанию, если бы я не хотел деструктурировать реквизит?
Я могу думать только о деструктурировании только свойств «по умолчанию», например:

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

Но я считаю постыдным извлекать только часть реквизита.

@glecetre Вы можете использовать:

HelloComponent.defaultProps = {
    name: 'John',
    surname: 'Smith'
}

@Glinkis , пожалуйста, обратите внимание на https://github.com/reactjs/rfcs/pull/107/files#diff -20b9b769068a185d90c23b58a2095a9dR184.

@glecetre Почему ты не хочешь деструктурировать весь реквизит? Это проще, чем определение defaultProps , и его легче набирать. Тип реквизита компонента на основе класса может вас укусить, если вы экспортируете для внешнего использования, поскольку требуемые реквизиты могут больше не требоваться, если для них есть запись в defaultProps . Использование defaultProps также кажется немного волшебным, в то время как в деструктурировании параметров все это JavaScript.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги