【问题标题】:Link two independend types to detect correct typings链接两个独立的类型以检测正确的类型
【发布时间】:2021-06-09 13:25:51
【问题描述】:

我有以下文档结构:

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 必须具有有效的参数和值类型,所以如果我只检查块类型,我永远不会收到错误。但是演员阵容看起来真的很丑,我想知道是否有办法编写更智能的类型保护?

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 是指向 Playground 文件的链接(如果有帮助)。

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

【问题讨论】:

    标签: javascript typescript types typeguards


    【解决方案1】:

    这很难。我可以理解这个问题,那就是 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&lt;ITitleBlockConfig&gt;,因为它满足该类型的条件,无论它是否扩展它。

    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 或返回一些值,例如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

    现在我们差不多完成了,只是我不断收到令人愤怒的错误,比如这个:

    并非所有类型的成分 '((config: ConfigForType) => ConfigForType[V]) | (ConfigForType[V] & Function)' 是可调用的。

    Type 'ConfigForType[V] & Function' 没有调用签名。

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

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

    【讨论】:

    • 非常感谢您的评论,我学到了很多。在又考虑了 2 天之后,我想我得出了同样的结论,那就是把这个写出来是不值得的。要么我切换到一个类,要么只是接受,有时当我确定不会有任何类型不匹配时,any 转换更实用。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-04-04
    • 1970-01-01
    • 2012-03-11
    • 2020-02-09
    • 2020-06-09
    • 2011-07-23
    相关资源
    最近更新 更多