背景
我正在尝试创建一个方法,该方法接受Component类类型的映射,然后接受InstanceInfo对象的映射,这些对象通过字符串名称引用Components。每个Component类类型都包含一个类类型特定的泛型PropsType
,它是该类类型特有的各种属性值的映射。我希望我的InstanceInfo对象基于在type
属性中传递的Component的字符串名称,能够正确地推断出与引用Component配对的PropsType。
下面是一个被调用的方法示例:
const config = createConfig({
components: {
ComponentA,
ComponentB
}
}, {
instances: {
ComponentAInstance1: { // InstanceInfo
componentName: 'ComponentA',
props: {
componentAProp1: 123,
componentAProp2: 'abc'
}
},
ComponentBInstance1: { // InstanceInfo
componentName: 'ComponentB',
props: {
componentBProp1: true,
componentBProp2: false
}
},
},
});
示例中的ComponentA
和ComponentB
都扩展了此函数返回的匿名类:
function createComponentType<
PropsType = undefined
>() {
return class {
constructor(public props: PropsType) {
// Nothing
}
static __type_PropsType: PropsType; // Phantom type
}
}
该函数将一个";幻影型";用于PropsType的属性转换为静态属性,以便以后可以通过索引访问类型引用它。
以下是正在创建的两个组件,它们有自己独特的道具类型集:
type ComponentAProps = {
componentAProp1: number,
componentAProp2: string
}
class ComponentA extends createComponentType<ComponentAProps>() {}
type ComponentBProps = {
componentBProp1: boolean,
componentBProp2: boolean
}
class ComponentB extends createComponentType<ComponentBProps>() {}
为了定义我们的InstanceInfo,我们假设,通过通用参数,我们有组件映射的实际类型,以及组件的特定字符串名称,该名称受组件映射的键约束:
interface InstanceInfo<
Components extends Record<string, TypeOfComponent>,
ComponentName extends keyof Components
> {
componentName: ComponentName,
props: Components[ComponentName] extends TypeOfComponent ? Components[ComponentName]['__type_PropsType'] : never
};
在这里,我们将componentName
属性限制为有效的ComponentName
。对于props
,我们使用索引访问类型和条件类型来检查名称ComponentName引用的组件是否真的是TypeOfComponent。如果是这样,我们使用幻影道具类型作为props
的预期类型。如果不是,那么我们知道我们有一个无效的Component引用,所以never
是从条件中返回的。
ComponentConfig和InstanceConfig的结构相当简单:
/**
* Represents a configured set of Components
*/
interface ComponentConfig<
Components extends Record<string, TypeOfComponent>
> {
components: Components,
}
/**
* Represents a configured set of Instances
*/
interface InstanceConfig<
Components extends Record<string, TypeOfComponent>,
Instances extends Record<string, InstanceInfo<Components, keyof Components>>
> {
instances: Instances,
}
这两种情况下的泛型都用于促进createConfig()
方法的泛型类型推断。
为了使createConfig()
返回包含组件映射和实例映射的单个对象,我们创建了一个CombinedConfig接口:
interface CombinedConfig<
Components extends Record<string, TypeOfComponent>,
Instances extends Record<string, InstanceInfo<Components, keyof Components>>,
> extends ComponentConfig<Components>, InstanceConfig<Components, Instances> {}
最后,这里是createConfig()
方法,它的工作原理相当直接:
function createConfig<
Components extends Record<string, TypeOfComponent>,
Instances extends Record<string, InstanceInfo<Components, keyof Components>>
>(componentConfig: ComponentConfig<Components>, instanceConfig: InstanceConfig<Components, Instances>): CombinedConfig<Components, Instances> {
return {
...componentConfig,
...instanceConfig
};
}
问题
我让这个几乎按照我想要的方式工作。然而,有一个很大的问题,我很难纠正。
键入在一定程度上是强制的,但在一个糟糕的方面,它太宽松了。上面的第一个代码示例运行良好。TypeScript强制要求componentName
和props
至少独立有效。然而,这意味着componentName
和props
可能会以意想不到的方式变化:
componentName
可能是ComponentA,但props
可能是ComponentBPropscomponentName
可以是组件B,但props
可以是组件AProps(相反)
我正在寻找componentName
来确定props
的类型。即props
的类型应取决于componentName
使用的字符串。
const config = createConfig({
components: {
ComponentA,
ComponentB
}
}, {
instances: {
ComponentAInstance2: {
componentName: 'ComponentA',
// Bad!!! I want an error because using ComponentBProps with ComponentA!
props: {
componentBProp1: false,
componentBProp2: true
}
},
ComponentBInstance2: {
componentName: 'ComponentB',
// Bad!!! I want an error because using ComponentAProps with ComponentB!
props: {
componentAProp1: 123,
componentAProp2: 'abc'
}
},
},
});
我想我有点理解这个基本问题。返回InstanceConfig:
interface InstanceConfig<
Components extends Record<string, TypeOfComponent>,
Instances extends Record<string, InstanceInfo<Components, keyof Components>>
> {
instances: Instances,
}
Instances
扩展了Record<string, InstanceInfo<Components, keyof Components>>
,它最终将扩展为如下内容:
Record<
string,
{
componentName: 'ComponentA' | 'ComponentB',
props: Components['ComponentA']['__type_PropsType'] | Components['ComponentB']['__type_PropsType']
}
>
这解释了接受componentNames和props类型的任何组合。
然而,我需要一种方法来告诉TypeScript将其扩展为这样的东西,这样就有了一个严格区分的联盟:
Record<
string,
{
componentName: 'ComponentA',
props: Components['ComponentA']['__type_PropsType']
} |
{
componentName: 'ComponentB',
props: Components['ComponentB']['__type_PropsType']
}
>
有人知道目前使用TypeScript是否有可能实现这一点吗?还是我真的把可能的东西伸得太远了?
完整代码
//
// Set up
// Create a class factory function for a generic class containing a generic PropsType
//
type Constructor<T = {}> = new (...args: any[]) => T;
interface Component<PropsType> {
props: PropsType;
}
type TypeOfComponent<
PropsType = unknown
> = Constructor<Component<PropsType>> & { __type_PropsType: PropsType };
function createComponentType<
PropsType = undefined
>() {
return class {
constructor(public props: PropsType) {
// Nothing
}
static __type_PropsType: PropsType; // Phantom type
}
}
//
// Create components
//
type ComponentAProps = {
componentAProp1: number,
componentAProp2: string
}
class ComponentA extends createComponentType<ComponentAProps>() {}
type ComponentBProps = {
componentBProp1: boolean,
componentBProp2: boolean
}
class ComponentB extends createComponentType<ComponentBProps>() {}
//
// Set up our essential types
//
/**
* Represents Instance Data of a Component referenced by its string name
*/
interface InstanceInfo<
Components extends Record<string, TypeOfComponent>,
ComponentName extends keyof Components
> {
componentName: ComponentName,
/**
* This type extracts the __type_PropsType from the passed in component
*/
props: Components[ComponentName] extends TypeOfComponent ? Components[ComponentName]['__type_PropsType'] : never
};
/**
* Represents a configured set of Components
*/
interface ComponentConfig<
Components extends Record<string, TypeOfComponent>
> {
components: Components,
}
/**
* Represents a configured set of Instances
*/
interface InstanceConfig<
Components extends Record<string, TypeOfComponent>,
Instances extends Record<string, InstanceInfo<Components, keyof Components>>
> {
instances: Instances,
}
/**
* Represents the combined Component and Instance config returned by createConfig
*/
interface CombinedConfig<
Components extends Record<string, TypeOfComponent>,
Instances extends Record<string, InstanceInfo<Components, keyof Components>>,
> extends ComponentConfig<Components>, InstanceConfig<Components, Instances> {}
/**
* Creates configuration of both components and their instance data, first inferring the Components type from componentConfig, and using that to
* inform how the Instances are to be formatted
*/
function createConfig<
Components extends Record<string, TypeOfComponent>,
Instances extends Record<string, InstanceInfo<Components, keyof Components>>
>(componentConfig: ComponentConfig<Components>, instanceConfig: InstanceConfig<Components, Instances>): CombinedConfig<Components, Instances> {
return {
...componentConfig,
...instanceConfig
};
}
const config = createConfig({
components: {
ComponentA,
ComponentB
}
}, {
instances: {
ComponentAInstance1: {
componentName: 'ComponentA',
// Good!
props: {
componentAProp1: 123,
componentAProp2: 'abc'
}
},
ComponentBInstance1: {
componentName: 'ComponentB',
// Good!
props: {
componentBProp1: true,
componentBProp2: false
}
},
ComponentAInstance2: {
componentName: 'ComponentA',
// Bad!!! I want an error because using ComponentBProps with ComponentA!
props: {
componentBProp1: false,
componentBProp2: true
}
},
ComponentBInstance2: {
componentName: 'ComponentB',
// Bad!!! I want an error because using ComponentAProps with ComponentB!
props: {
componentAProp1: 123,
componentAProp2: 'abc'
}
},
},
});
console.log(config.instances.ComponentAInstance1.props.componentAProp1); // Good
console.log(config.instances.ComponentAInstance1.props.componentAProp2); // Good
console.log(config.instances.ComponentBInstance1.props.componentBProp1); // Good
console.log(config.instances.ComponentBInstance1.props.componentBProp2); // Good
console.log(config.instances.ComponentAInstance2.props.componentBProp1); // I don't want this to be allowed
console.log(config.instances.ComponentAInstance2.props.componentBProp2); // I don't want this to be allowed
console.log(config.instances.ComponentBInstance2.props.componentAProp1); // I don't want this to be allowed
console.log(config.instances.ComponentBInstance2.props.componentAProp2); // I don't want this to be allowed
TypeScript版本:4.3(有关编译器选项,请参阅Playground)游乐场
您面临的核心问题是,正如您已经提到的那样,缺乏受歧视的联盟。现在可以通过创建一个对象并引用该对象(从@jcalz复制)
type ComponentInstance<
Components extends Record<string, TypeOfComponent>,
> = {
[K in keyof Components]: {
componentName: K,
props: Components[K]["__type_PropsType"]
}
}[keyof Components];
然后我们可以简单地将您的类型更新为:
interface ComponentConfig<
Components extends Record<string, TypeOfComponent>
> {
components: Components,
}
interface InstanceConfig<
Components extends Record<string, TypeOfComponent>,
> {
instances: Record<string, ComponentInstance<Components>>,
}
现在,您实际上可以将其插入createConfig
函数中,如图所示:
function createConfig2<
Components extends Record<string, TypeOfComponent>,
>(componentConfig: ComponentConfig<Components>, instanceConfig: InstanceConfig<Components>) {
return {
...componentConfig,
...instanceConfig
};
}
但问题是,返回值不是基于您传递的参数。(产生问题2)。这是因为输出类型为:
type Config: {
instances: Record<string, ComponentInstance<{
ComponentA: typeof ComponentA;
ComponentB: typeof ComponentB;
}>>;
components: {
ComponentA: typeof ComponentA;
ComponentB: typeof ComponentB;
};
}