What I want to accomplish:

假设有一个类型如下的配置:

type ExmapleConfig = {
    A: { Component: (props: { type: "a"; a: number; b: number }) => null };
    B: { Component: (props: { type: "b"; a: string; c: number }) => null };
    C: { Component: () => null };
};

所以,一般说来,形状是这样的:

type AdditionalConfigProps = {
    additionalConfigProp?: string;
    // + more additional props that don't have to be optional
};

type ReservedComponentProps = {
    reservedComponentProp: string;
};

type ComponentProps = ReservedComponentProps & Record<string, any>;

type Config = {
    [key: string]: {
        Component: (props: PropsShape) => JSX.Element;
    } & AdditionalConfigProps;
};

我想改变这样的配置,但是:

  • 为关键点保留硬类型('A' | 'B' | 'C'而不是string)
  • 为props 保留硬类型({ type: "a"; a: number; b: number }而不是Record<string, any>)
  • make sure that transform function only accepts correct configs, that is:
    • 它有Component个属性,以及AdditionalConfigProps个具有正确类型的所有其他属性,
    • 它不接受在定义的ComponentAdditionalConfigProps中的属性之上的任何附加属性,
    • Component函数必须能够接受类ComponentProps对象作为第一个参数,

转换可能如下所示:


const config = {
    A: { Component: (props: { type: "a"; a: number; b: number }) => <div>abc</div> };
    B: { Component: (props: { type: "b"; a: string; c: number }) => <div>abc</div>  };
    C: { Component: () => <div>abc</div>  };
};

/*
    Let's say that it will extract Components, and wrap them
    with additional function so void will be returned instead of JSX
*/
const transformedConfig = transformConfig(config);

// typeof transformedConfig
type ResultType = {
    A: (props: { type: "a"; a: number; b: number }) => void;
    B: (props: { type: "b"; a: string; c: number }) => void;
    C: () => void;
};

请注意:

  • 保留了键‘A’|‘B’|‘C’的硬类型
  • 坚硬的props 类型被保留下来

Approach I've tried:

import React from "react";

type AdditionalConfigProps = {
    additionalConfigProp?: string;
};

type ReservedComponentProps = {
    reservedComponentProp: string;
};

const CORRECT_CONFIG = {
    A: {
        Component: (props: { type: "a"; a: number; b: number }) => null,
        additionalConfigProp: "abc"
    },
    B: { Component: (props: { type: "b"; a: string; c: number }) => null },
    C: { Component: (props: { reservedComponentProp: "c"; a: string }) => null },
    D: { Component: (props: {}) => null },
    E: { Component: () => null }
};

const BAD_CONFIG = {
    // Missing Component or other required config prop
    A: {},
    // Bad additionalConfigProp
    B: { Component: () => null, additionalConfigProp: 123 },
    // Bad Component
    C: { Component: 123 },
    // Bad component props type
    D: { Component: (props: boolean) => null },
    // Unexpected 'unknownProp'
    E: { Component: () => null, unknownProp: 123 },
    // Bad 'reservedProp'
    F: { Component: (props: { reservedProp: number }) => null }
};

function configParser<
    Keys extends string,
    ComponentPropsMap extends {
        [Key in Keys]: ReservedComponentProps & Record<string, any>;
    }
>(config: {
    [Key in Keys]: {
        Component: (props?: ComponentPropsMap[Keys]) => React.ReactNode;
    } & AdditionalConfigProps;
}) {
    /*
        TODO: Transform config.
        For now we want to make sure that TS is even able to 'see' it correctly.
    */
    return config;
}

/*
    ❌ Throws unexpected type error
*/
const result = configParser(CORRECT_CONFIG);

// Expected typeof result (what I'd want)
type ExpectedResultType = {
    A: {
        Component: (props: { type: "a"; a: number; b: number }) => null;
        additionalConfigProp: "abc";
    };
    B: { Component: (props: { type: "b"; a: string; c: number }) => null };
    C: { Component: (props: { reservedComponentProp: "c"; a: string }) => null };
    D: { Component: (props: {}) => null };
    E: { Component: () => null };
};

/*
    ❌ Should throw type errors, but not the ones it does
*/
configParser(BAD_CONFIG);

我当然可以这样做:

function configParser<
    Config extends {
        [key: string]: {
            Component: (componentProps: any) => React.ReactNode;
        };
    }
>(config: Config) {
    return config;
}

// No type error, result type as expected
const result = configParser(CORRECT_CONFIG);

但它:

  • 不会验证componentProps(也许componentProps: Record<string, any> & ReservedComponentProps会,但出于某种原因,它不会接受CORRECT_CONFIG)
  • 将允许任何其他配置属性

推荐答案

这里有一种可能的方法:

type VerifyConfigElement<T extends AdditionalConfigProps &
{ Component: (props: any) => void }> =
  { [K in Exclude<keyof T, "Component" | keyof AdditionalConfigProps>]: never } &
  {
    Component: (
      props: Parameters<T["Component"]>[0] extends ComponentProps ? any : ComponentProps
    ) => void
  }

declare function transformConfig<
  T extends Record<keyof T, AdditionalConfigProps & { Component: (props: any) => void }>>(
    config: T & { [K in keyof T]: VerifyConfigElement<T[K]> }
  ): { [K in keyof T]: (...args: Parameters<T[K]["Component"]>) => void }

我们的 idea 是:

  • config参数的类型T中使transformConfig() generic
  • constrain T变成一个相对容易编写的类型,它不拒绝好的输入,在本例中是AdditionalConfigProps & {Component: (props: any) => void}>
  • 更彻底地判断推断出的T的每个属性,将其从自身T[K]到相关类型VerifyConfigElement<T[K]>(其中T[K] extends VerifyConfigElement<T[K]>当且仅当它是一个好的输入)判断mapping
  • 通过将T的每个属性映射到一个函数类型来计算T的返回类型,该函数类型的参数由对应的Component属性indexing into确定.

VerifyConfigElement<T>型会判断两件事:

  • T没有在AdditionalConfigProps(当然是"Component")中明确提到的任何性质……它通过将任何这样的额外属性映射到never类型来做到这一点,这几乎肯定不会进行类型判断;
  • 那个TComponent方法的第一个参数可以赋值给ComponentProps...它通过映射到any(将成功)和ComponentProps(如果不成功)来实现这一点(probably将失败?函数类型在其输入参数中是逆变量,因此这里可能存在一些边缘情况).

让我们来测试一下:

const config = {
  A: { Component: (props: { type: "a"; a: number; b: number }) => <div>abc</div> },
  B: { Component: (props: { type: "b"; a: string; c: number }) => <div>abc</div> },
  C: { Component: () => <div>abc</div> }
};
// typeof transformedConfig
type ResultType = {
  A: (props: { type: "a"; a: number; b: number }) => void;
  B: (props: { type: "b"; a: string; c: number }) => void;
  C: () => void;
};////
const transformedConfig: ResultType = transformConfig(config);

看上go 不错!对于CORRECT_CONFIGBAD_CONFIG,编译器分别接受和拒绝它们:

const okay = transformConfig(CORRECT_CONFIG); // okay
const bad = transformConfig(BAD_CONFIG); // error

如你所愿.

Playground link to code

Javascript相关问答推荐

我可以使用CSS有效地实现最大宽度=100%和最大高度=100%,而无需父母有明确的/固定的宽度和高度,替代方法吗?

被CSS优先级所迷惑

Chrome是否忽略WebAuthentication userVerification设置?""

如何修复我的js构建表每当我添加一个额外的列作为它的第一列?

分层树视图

为什么按钮会随浮动属性一起移动?

如何在Angular17 APP中全局设置AXIOS

如何强制Sphinx中的自定义js/css文件始终加载更改而不缓存?

rxjs插入延迟数据

闭包是将值复制到内存的另一个位置吗?

如何在ASP.NET项目中使用Google Chart API JavaScript将二次轴线值格式化为百分比

从页面到应用程序(NextJS):REST.STATUS不是一个函数

JavaScript:如果字符串不是A或B,则

JAVASCRIPT SWITCH CASE语句:当表达式为';ALL';

为什么我不能使用其同级元素调用和更新子元素?

如何在Web项目中同步语音合成和文本 colored颜色 更改

如何向内部有文本输入字段的HTML表添加行?

VITE版本中的内联SVG

查找函数句柄的模块/文件

FiRestore按字段的提取顺序