Свойства по умолчанию в настоящее время не работают должным образом с включенным 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, за исключением типа реквизита?
Поскольку реквизиты по умолчанию устанавливаются во время выполнения, я не уверен, что есть способ хорошо с этим справиться, кроме утверждения типа. (Конечно, вы всегда можете просто отключить строгие проверки нулей.)
Вот как вы можете обойти это в своем примере:
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)
Три потенциальных недостатка (о которых я могу думать):
props
.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.
Самый полезный комментарий
Если у кого-то есть хорошее решение для типов и defaultProps, я внимательно слушаю. В настоящее время мы делаем это: