使用字符串值来推断同一对象中更深层次的依赖类型



背景

我正在尝试创建一个方法,该方法接受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
}
},
},
});

示例中的ComponentAComponentB都扩展了此函数返回的匿名类:

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强制要求componentNameprops至少独立有效。然而,这意味着componentNameprops可能会以意想不到的方式变化:

  • componentName可能是ComponentA,但props可能是ComponentBProps
  • componentName可以是组件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;
};
}

最新更新