Definitelytyped: 使用 ts 2.0 严格的空检查实现 defaultProps

创建于 2016-09-30  ·  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};
}

除了 props 的类型之外,这与现有的 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东西。

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

我刚开始使用TS。 我错过了什么吗?

如果有人对类型和 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的三个类型参数 - 根据您链接到 Flow 的 Medium 文章,可以从子类注释中推断类类型参数 - TS 不支持简洁的语言级功能,但不支持类型-声明考虑AFAIK。

@aldendaniels

Flow 确实为 React.Component 提供了三个类型参数

不,它在 0.53 之前曾经是这样,现在不是了 :) https://github.com/facebook/flow/commit/20a5d7dbf484699b47008656583b57e6016cfa0b#diff -5ca8a047db3f6ee8d65a46bba44712​​36R29

@Hotell啊,果然! 谢谢你纠正我。

AFAIK 虽然 TS 无法推断默认道具的类型。 使用三类型参数方法,我们可能能够在不阻塞来自 TypeScript 团队的上游更改的情况下获得正确的输入。

你知道在不传递typeof MyComponent.defaultProps作为类型参数的情况下使用静态属性的推断类型的方法吗?

关于这个主题的任何消息? 有人做 PR 来添加第三个类型参数并使用https://github.com/Microsoft/TypeScript/issues/12215#issuecomment -319495340?

投票问题:同样的问题

+1

我也遇到了这个问题,我选择(直到正确修复)放弃使用static defaultProps而是使用辅助 HOC:

文件components/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}/>
}

现在我可以使用:

文件组件/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 声明为泛型类型参数,并且不能依赖于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 会导致同样的错误,因为tsc的 JSX 检查是基于props ' type 的

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

使用上面的代码片段会起作用,但是您将失去在 User 上使用静态属性的能力,因为它变成了一个匿名类。 一个hacky的解决方案是隐藏类名,如下所示:

// 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 (如果我们想省略typeof Comp.defaultProps ,应该更改 ts 检查器),我们可以获得最好的结果。
其他选项,已经说过 - 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>;

我很惊讶在 NPM 上仍然没有合适的解决方案,或者至少没有一个有效的 HOC; 除非我错过了什么。

大家好。 只想说,如果您仍在阅读此主题:我认为@JoshuaToenyes做出了最有意义和最有帮助的解释。 这绝对不是问题,因此与它无关。 在这种情况下使用类型断言。

@toiletpatrol实际上, @decademoon的解决方案(经过我的轻微修改)自动处理默认道具就好了。 它绝对可以合并到 React 的 DT 定义中,为每个人提供功能标准。

@vkrol我确实看到了,但我现在可以在我的代码库中放弃十年月的实现,而无需等待新功能的发布。

我现在用于棘手情况的另一种解决方法:

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

我想这没什么问题,所以我把它留在这里作为一个肮脏而简单的解决方法。

TS 3.2 和 react 16.7 类型正在解决这个问题。 我们可以关闭吗?

@Hotell最终应该如何处理? 我仍然无法正常工作

为了节省其他人一些时间,这里是 Typescript 3 发行说明的链接:
支持 JSX 中的 defaultProps

@cbergmiller恐怕这些是 TypeScript 3.1 的发行说明🙃

React.FunctionComponent仍然有同样的问题

@denieler我不建议将defaultPropsReact.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 等级