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



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

假设我想在下面定义 UI 组件 方式(以下行不得以任何方式更改 - 任何 坦率地说,将更改以下行的解决方案不是解决方案 我正在寻找...例如,只是写render({ name: 'World' })不是一种选择...非空断言运算符也不是...既不使用柯里或构建器模式或类似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 {
...
}

请在此处找到一个非工作(!(演示:

» 演示

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

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

正如您在问题中指出的那样:

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

TS 不会在函数/类声明时推断泛型。您的问题的想法与问题 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.
}

代码上出现编译错误的原因是,实际上,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,您只需拨打.toUpperCaseundefined。在控制台上测试如果这样做会发生什么(PS.:在真实的应用程序上,结果会更加混乱(。

进一步说明

顺便说一下,在典型的TypeScript + React应用程序上,您将使用默认道具声明默认道具。

public static defaultProps = {
...
};

而不是您使用的方法。

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

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

您可能遇到的主要问题是string不是有效的ReactElement。组件需要返回ReactElement。我们将字符串包装在一个片段中,这是一个ReactElement,然后返回它。打字稿将推断函数的正确类型。

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

演示

基本上我是这样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
}

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

是的,这似乎是可行的!看看这个游乐场。

有趣的类型:

// 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调用采用默认 props 的显式类型。不过,它确实强制要求类型是正确的,并且通过将默认 props 拉到单独的对象中,您可以将其作为简单的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
}

一个有效的解决方案(但不完全是我所要求的(是使用以下语法

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

而不是

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

这里有一个小演示:

演示