TypeScript打字问题(与(P)React有关)

时间:2019-08-23 09:38:30

标签: reactjs typescript types typescript-typings

[编辑:我已经简化了原来的问题]

假设我要在以下确切定义UI组件 方式(以下几行不得以任何方式更改-任何 坦白说,这将改变以下几行的解决方案 我正在寻找...例如只写render({ name: 'World' }) 是不可行的……非null断言运算符也不是……既不使用currying或构建器模式,也不使用withDefaultProps辅助函数之类的东西……等等。可以轻松解决以下实际问题):

// please do not change anything in this code snippet

type HelloWorldProps = {
  name?: string
}

export default component<HelloWorldProps>({
  displayName: 'HelloWorld',
  defaultProps: { name: 'World' },

  render(props) {
    // the next line shall NOT throw a compile error
    // that props.name might be undefined
    return `HELLO ${props.name.toUpperCase()}`

    // [Edit] Please ignore that the function returns a string
    // and not a virtual element or whatever - this is not important here.
    // My question is about a TypeScript-only problem,
    // not about a React problem.

    // [Edit] As it has been caused some misunderstanding:
    // The type of argument `props` in the render function shall
    // basically be the original component props type plus (&) all
    // properties that are given in `defaultProps` shall be required now.
    // Those optional props that have no default value shall still
    // be optional. If ComponentType is the original type of the component
    // properties and the type of the `defaultProps` is D then
    // the type of the first argument in the render function shall
    // be: ComponentProps & D

     // [Edit] As it seems not to be 100% clear what I am looking for:
     // The problem is mainly because the function "component" depends basically
     // on two types: One is the type of the component props the other is
     // is the type of the default props. AFAIK it's currently only possible in
     // TypeScript to infer either both of them or none of them (or use
     // default types for the type variables - which is not very useful here
     // as the defaults are {}). But I only want to declare ONE type
     // (HelloWorldProps).
     // All workarounds that I know of are either to explictly declare both
     // types or split the single function "component" into two or more
     // functions - then you do not have that problem any more,
     // but then you have to change the syntax and that is exactly
     // what I do NOT want to do (a fact that is the most important
     // part of the  whole question):

     // [this is not the solution I am looking for]
     // export default component<HelloWorldProps>('HelloWorld')({
     //   defaultProps: {...},
     //   render(props) {...}
     // })

     // [this is not the solution I am looking for]
     // export default component<HelloWorldProps>('HelloWorld')
     //   .defaultProps({...})
     //   .render(props => ...) // `render` is the function component
     //                         // creator is this builder pattern

     // [this is not the solution I am looking for]
     // export default component<HelloWorldProps>({
     //   displayName: 'HelloWorld',
     //   render: withDefaultProps(defaultProps, props => { ... })
     // })

     // [this is not the solution I am looking for]
     // type HelloWorldProps = {...}
     // const defaultProps: Partial<HelloWorldProps> = {...}
     // export default component<HelloWorldProps, typeof defaultProps>({...})

     // [this is not the solution I am looking for]
     // return `HELLO ${props.name!.toUpperCase()}`

     // [this is not the solution I am looking for]
     // render(props: HelloWorldProps & typeof defaultProps) {...}   

     // [this is not the solution I am looking for]
     // render({ name = 'HelloWorld' }) {...}
  }
})

要如何精确键入函数component和类型ComponentConfig 上面的代码正常工作吗?

function component<...>(config: ComponentConfig<...>): any {
  ...
}

请在此处找到无效的(!)演示:

»DEMO

[编辑]也许这暂时不可能。我认为如果为TS编译器实现此功能应该是可能的。 https://github.com/Microsoft/TypeScript/issues/16597

6 个答案:

答案 0 :(得分:2)

代码中出现编译错误的原因是,实际上props.name可能未定义。

要解决此问题,您只需更改类型声明即可

type GreeterProps = {
  name?: string // the ? after name means name must be undefined OR string
}

type GreeterProps = {
  name: string // name must be a string
}

如果您真的希望props.name能够保持未定义状态怎么办?

您可以更改render内部的逻辑,例如:

render(props) {
  if (this.props.name === undefined) return 'You don\'t have a name =/';
  return 'HELLO ' + props.name.toUpperCase();
}
为什么您需要这样做?

答案非常简单,如果props.name可以为undefined,则只需致电.toUpperCase上的undefined。在控制台上测试如果执行此操作会发生什么情况(PS .:在实际应用中,结果甚至会更混乱)。

更多笔记

顺便说一句,在典型的TypeScript + React App上,您将使用声明默认道具

  public static defaultProps = {
    ...
  };

代替您使用的方法。

答案 1 :(得分:2)

经过几天的讨论和研究,鉴于您的限制,无法解决您的问题。

当您指出问题时:

  

[编辑]也许这暂时不可能。我认为如果为TS编译器实现此功能应该是可能的。 https://github.com/Microsoft/TypeScript/issues/16597

在函数/类声明时,TS不会推断泛型。您问题的想法与issue 16597的想法相同:

// issue example
class Greeter<T, S> {
    greeting: T;
    constructor(message: T, message2: S) {
        this.greeting = message;
    }

}

// your issue
function component<P extends {} = {}>(config: ComponentConfig<P>): any {
  return null
}

// generalizing
const function<SOME_GIVEN_TYPE, TYPE_TO_BE_INFERED?>() {
  // TYPE_TO_BE_INFERED is defined inside de function/class.
}

答案 2 :(得分:0)

认为您正在寻找的东西是这样的:

type HelloWorldProps = {
  name?: string
}

export default function HelloWorld({ name = "World"}: HelloWorldProps) {
    return <>HELLO {name.toUpperCase()}</>
}

您可能遇到的主要问题是string不是有效的ReactElement。组件需要返回ReactElement。我们将字符串包装在ReactElement的{​​{3}}中,并返回它。 Typescript将推断函数的正确类型。

答案 3 :(得分:0)

此解决方案可以解决您的问题吗?

DEMO

基本上,我是这样创建ComponentConfig的:

// Keep the original implementation
type PickOptionalProps<T> = Pick<T, {
    [K in keyof T]-?: T extends Record<K, T[K]> ? never : K
}[keyof T]>

// Transform all properties as required
type TransformKeysAsRequired<T> = {
  [P in keyof T]-?: T[P];
}

interface ComponentConfig<P extends {}> {
  displayName: string,
  // I pick the optional props and transform them as required
  defaultProps?: TransformKeysAsRequired<PickOptionalProps<P>>, 
  // Just transform the props as required
  render(props: TransformKeysAsRequired<P>): any
}

请告诉我我是否误解了您的问题。

答案 4 :(得分:0)

是的,这似乎可行!看看this playground

有趣的类型:

// Optional meaning 'could be undefined': both { x?: string } and { x: undefined }
type PickOptionalProps<T> = Pick<T, {
    [K in keyof T]-?:
      T extends Record<K, T[K]>
        ? undefined extends T[K] ? K : never
        : K
}[keyof T]>;

type DefaultsFor<P> = Partial<PickOptionalProps<P>>;

type WithoutPropsFrom<T, X> = Omit<T, keyof X>;

// Remove ? or undefined props
type WithoutOptional<T> = {
  [K in keyof T]-?: Exclude<T[K], undefined>
}

type MergePropsAndDefaults<P, D extends DefaultsFor<P>> = {    
    // Props not that aren't both optional & in the defaults stay as they are
    [K in keyof WithoutPropsFrom<P, (PickOptionalProps<P> | D)>]: P[K]
} & {
    // Optional props overridden by defaults:
    [K in keyof (PickOptionalProps<P> | D)]-?: WithoutOptional<P>[K] | D[K]
};

type ComponentConfig<
  P extends {},
  D extends DefaultsFor<P> | undefined = {}
> = D extends {}
  ? {
    displayName: string,
    defaultProps: D, 
    render(props: MergePropsAndDefaults<P, D>): any
  } 
  : {
    displayName: string, 
    render(props: P): any
  };

function component<
  P extends {} = {},
  D extends DefaultsFor<P> | undefined = undefined
>(config: ComponentConfig<P, D>): any {
  return null
}

这要求component调用采用默认道具的显式类型。它确实强制类型正确,并且通过将默认道具拉到一个单独的对象中,您可以像简单地typeof myDefaultProps那样传递它。

这里的合并涵盖了您未提及的其他几种情况。举一个最小的例子:

type Props = { name?: string, age: number, optional?: number, undef: string | undefined };
type DefaultProps = { name: 'default', undef: 'other-default' };
type ResultingProps = MergePropsAndDefaults<Props, DefaultProps>;

// Gives:
type ResultingProps = {
    age: number; // Required, because it's required in the main props
    optional?: number | undefined; // Not required but no default: stays optional
} & {
    name: string; // Optional prop overridden correctly
    undef: string; // Non-optional but could-be-undefined, overridden correctly
}

答案 5 :(得分:0)

一种可行的解决方案(但并非完全符合我的要求)将使用以下语法

const HelloWorld: Component<HelloWorldProps> = component({ ... })

代替

const HelloWorld = component<HelloWorldProps>({ ... })

这是一个小演示:

DEMO