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.value
和 block.config
必须是匹配对的方式来保护类型。
您当前的 isConfigurableBlock
检查仅在此 configureBlock
功能中有用,作为运行时保障。众所周知,DocumentBlock
联盟的所有成员都可以配置为 IImageBlock
和 ITitleBlock
扩展 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
,但它实际查看的唯一属性是 type
和 config
。它可以配置一个块,而不管该块是否具有 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 一些值,例如 null
或 undefined
稍后检查。
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);
}
现在我们几乎完成了,除了我不断收到 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
的工作。而且我什至不能让那个守卫工作所以现在我正式放弃。
我有以下文档结构:
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.value
和 block.config
必须是匹配对的方式来保护类型。
您当前的 isConfigurableBlock
检查仅在此 configureBlock
功能中有用,作为运行时保障。众所周知,DocumentBlock
联盟的所有成员都可以配置为 IImageBlock
和 ITitleBlock
扩展 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
,但它实际查看的唯一属性是 type
和 config
。它可以配置一个块,而不管该块是否具有 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 一些值,例如 null
或 undefined
稍后检查。
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);
}
现在我们几乎完成了,除了我不断收到 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
的工作。而且我什至不能让那个守卫工作所以现在我正式放弃。