Link 两种独立的类型来检测正确的打字

Link two independend types to detect correct typings

我有以下文档结构:

onst blockTypes = [
    'Title',
    'Image',
] as const;
type BlockType = typeof blockTypes[number];

interface IDocumentBlock {
    id: string;
    type: BlockType;
    position: number;
}

interface IConfigurableDocumentBlock<T> extends IDocumentBlock {
    config: T;
}

interface ITitleBlockConfig {
    size: number;
    subtitle: boolean;
}
interface ITitleBlock
    extends IConfigurableDocumentBlock<ITitleBlockConfig> {
    type: 'Title';
    content: string;
}

interface IImageBlockConfig {
    alignment: 'center' | 'left' | 'right';
}
interface IImageBlock
    extends IConfigurableDocumentBlock<IImageBlockConfig> {
    type: 'Image';
    source: string;
    title?: string;
}

type DocumentBlock =
    | IImageBlock
    | ITitleBlock
    
const isConfigurableBlock = (
    block: IDocumentBlock
): block is IConfigurableDocumentBlock<unknown> => {
    return (block as IConfigurableDocumentBlock<unknown>).config !== undefined;
};

对于可配置的块,有一个 BlockConfigurator 类型可以传递到任何地方来配置这些块:

type BlockConfigurator<
    T extends IConfigurableDocumentBlock<unknown>,
    V extends keyof T['config']
> = T extends IConfigurableDocumentBlock<infer R>
    ? {
            blockType: T['type'];
            title: string;
            parameter: V;
            value: T['config'][V] | ((config: R) => T['config'][V]);
      }
    : never;

const ImageBlockConfigurator: BlockConfigurator<IImageBlock, 'alignment'> =
    {
        blockType: 'Image',
        title: 'Right',
        parameter: 'alignment',
        value: (config) => (config.alignment === 'center' ? 'left' : 'right'),
    };
    

const TitleBlockConfigurator: BlockConfigurator<ITitleBlock, 'size'> = {
    blockType: 'Title',
    title: 'Font Size 2',
    parameter: 'size',
    value: 2,
};

type Configurator =
    | typeof ImageBlockConfigurator
    | typeof TitleBlockConfigurator;

我在连接这两种类型时遇到了一些问题。我知道我得到的任何 BlockConfigurator 对象都必须具有有效的参数和值类型,因此如果我只检查块类型,我永远不会收到错误。但是 any 的转换看起来真的很难看,我想知道是否有办法编写更智能的类型保护?

const configureBlock = (block: DocumentBlock, configurator: Configurator) => {
    if (
        isConfigurableBlock(block) &&
        block.type === configurator.blockType
    ) {
        const value =
            typeof configurator.value === 'function'
                ? configurator.value(block.config as any)
                : configurator.value;
        (block.config as any)[configurator.parameter] = value;
    }
}
如果有帮助,

Here 是 link 游乐场文件。

E:例如,这将消除任何类型转换,但我必须手动编写任何参数值和块类型组合:

if (
    isConfigurableBlockNormalized(block) &&
    block.type === configurator.blockType
) {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    if (
        configurator.blockType === 'Title' &&
        configurator.parameter === 'size' &&
        block.type === 'Title'
    ) {
        const value =
            typeof configurator.value === 'function'
                ? configurator.value(block.config)
                : configurator.value;
        block.config[configurator.parameter] = value;
    }
}

if 块的内容对于任何组合都完全相同,只是条件会改变。但这些条件已融入配置器本身,并且必须为真。

这很难。我能理解这个问题,即 block.type === configurator.blockType 不会以 TypeScript 知道 configurator.valueblock.config 必须是匹配对的方式来保护类型。

您当前的 isConfigurableBlock 检查仅在此 configureBlock 功能中有用,作为运行时保障。众所周知,DocumentBlock 联盟的所有成员都可以配置为 IImageBlockITitleBlock 扩展 IConfigurableDocumentBlock。所以 isConfigurableBlock(block) 必须始终为真。

我们需要检查的是 block 变量可由这个特定的 configurator 变量配置。


我的第一个方法是使用高阶函数来创建特定于配置器的类型保护。这是一团糟,断言超出实际检查范围的内容,still doesn't work。供参考:

// do not try this at home -- bad code below
const isConfigurableBlock = <Config, Key extends keyof Config, Block extends IConfigurableDocumentBlock<Config>>(
    configurator: BlockConfigurator<Block, Key>
) => (block: IConfigurableDocumentBlock<unknown>): block is IConfigurableDocumentBlock<Config> => {
    return block.type === configurator.blockType;
}

然后我更“回到绘图板”看看这些类型。当前检查只查看 blockType 并依赖于具有特定块类型的 DocumentBlock 联合成员具有与 Configurator 成员相同的 config 类型的假设与该块类型联合。这目前是正确的,但并非天生或自动正确。

我们想让这种关系更加明确。我们可以通过使用 DocumentBlock 作为关系的规范参考点来做到这一点。

type ConfigForType<T extends BlockType> = Extract<DocumentBlock, {type: T}>['config']

type Check = ConfigForType<'Image'> // inferred as IImageBlockConfig -- correct

我会放弃 extends 的一层,让方块直接延伸 IDocumentBlock 而不是通过 IConfigurableDocumentBlock。请注意 ITitleBlock 仍然可以分配给 IConfigurableDocumentBlock<ITitleBlockConfig> 因为它满足该类型的条件,无论它是否扩展它。

interface ITitleBlock extends IDocumentBlock {
    type: 'Title';
    config: ITitleBlockConfig
    content: string;
}

BlockConfigurator 泛型依赖于整个块对象 ITitleBlock,但它实际查看的唯一属性是 typeconfig。它可以配置一个块,而不管该块是否具有 ITitleBlock.

所需的 content 属性

我们实际上可以从 BlockConfigurator 类型中删除条件。它只需要知道 BlockType 和 属性 名称。它可以从先前建立的规范关系中获取配置。

type BlockConfigurator<
    T extends BlockType,
    V extends keyof ConfigForType<T>
> = {
    blockType: T;
    title: string;
    parameter: V;
    value: ConfigForType<T>[V] | ((config: ConfigForType<T>) => ConfigForType<T>[V]);
}

这也使得推断 BlockConfigurator 的泛型变得容易得多,因为它们都是直接存在于对象中的字符串。不再需要指定任何未知的外部信息。

您可以像以前一样使用直接赋值:

const TitleBlockConfiguratorT2: BlockConfigurator<'Title', 'size'> = { ...

但你也可以这样做:

// identity function which validates types
const createBlockConfigurator = <
    T extends BlockType,
    V extends keyof ConfigForType<T>
>(config: BlockConfigurator<T, V>) => config;

// type is inferred as BlockConfigurator<"Title", "size">
const TitleBlockConfiguratorT2 = createBlockConfigurator({
    blockType: 'Title',
    title: 'Font Size 2',
    parameter: 'size',
    // type of config is inferred as ITitleBlockConfig
    value: (config) => config.size,
});

这个 configureBlock 函数确实很痛苦。甚至我常用的 union of valid pairings 技巧在这里也不起作用。如果我没有投入足够的时间和精力,我会在这一点上放弃。但我已经找到了,所以我必须找到 一些东西 即使它不完美也能工作。

这里应用高阶函数方法的问题是第一个函数的泛型不会具体,因为我们的 configurator 参数是 Configurator 类型。为了获得这种特异性,我们可以将类型保护附加到各个配置器对象。我们可以使用前面演示的 createBlockConfigurator 函数,但修改它以包含 属性 这是一个类型保护。

const createBlockConfigurator = <
    T extends BlockType,
    V extends keyof ConfigForType<T>
>(config: BlockConfigurator<T, V>) => ({
    ...config,
    canConfigure: <B extends DocumentBlock>(block: B): block is B & { type: T; config: ConfigForType<T>; } => 
        block.type === config.blockType
});

这个还是不行。所以现在我疯了,我需要打败红色波浪线。

下一步是通过配置器对象的 属性 调用函数。

我对你在这里做的事情感到困惑 block.config[configurator.parameter] = value;。通过配置器上的价值创造者功能,您正在使用 block.config 作为 value 的基础。但是您还使用 value 作为设置 block.config.

的基础

二选一。也许我们正在处理的 block 还没有 config 属性 而我们正在创建它——但是这整个问题都没有实际意义。

所以现在我只是假设您想要潜在地调用一个函数,该函数从块中获取配置并创建一个值。我忽略了您使用该计算值所做的不合逻辑的事情。

当提供无效的 block 参数时,配置器可以抛出一个 Error 或 return 一些值,例如 nullundefined稍后检查。

const createBlockConfigurator = <
    T extends BlockType,
    V extends keyof ConfigForType<T>
>(config: BlockConfigurator<T, V>) => {
    const canConfigure = <B extends DocumentBlock>(block: B): block is B & { type: T; config: ConfigForType<T>; } => 
        block.type === config.blockType;
    const computeValue = (block: DocumentBlock): ConfigForType<T>[V] => {
        if ( ! canConfigure(block) ) {
            throw new Error(`Invalid block object. Expected type ${config.blockType} but received type ${block.type}`);
        }
        // error on the next line
        return ( typeof config.value === "function" ) ? config.value(block.config) : config.value;
    }
    return {
        ...config,
        canConfigure,
        computeValue,
    }
};
const configureBlock = (block: DocumentBlock, configurator: Configurator) => {
    // no problems here
    const value = configurator.computeValue(block);
}

TypeScript Playground Link

现在我们几乎完成了,除了我不断收到 infuriating 错误,比如这个:

Not all constituents of type '((config: ConfigForType) => ConfigForType[V]) | (ConfigForType[V] & Function)' are callable.

Type 'ConfigForType[V] & Function' has no call signatures.

我认为这个错误是由于我们正在计算的值 属性 本身可能是一个函数。我们知道这永远不会发生,但我相信 ConfigForType<T>[V] 没有被完全评估为所有可能成分的联合,因为它是一个通用的。

所以...我想我们可以用另一个类型保护来做出这个断言?但它开始变得一团糟,而且开始看起来像是一份 class 的工作。而且我什至不能让那个守卫工作所以现在我正式放弃。