javascript 类型脚本从泛型嵌套键创建类型

bcs8qyzn  于 2023-01-08  发布在  Java
关注(0)|答案(1)|浏览(64)

我正在通过重新创建一个类似于CVA库的CSS类构建器函数来自学如何使用typescript泛型。然而,我被如何从泛型中提取键所困扰。我有以下代码(为了简洁而简化):

type TConfig = {
    base?: string;
    variants?: Record<string, Record<string, string>>;
};

type TInput<T extends TConfig> = Record<keyof T["variants"], string>;

export const classBuilder = <T extends TConfig>(config: T) => {
    ///...
    return (input: TInput<T>) => {
        // return ...
    };
};

const builder = classBuilder({
    base: "text-white rounded-sm",
    variants: {
        color: {
            red: "bg-red",
            blue: "bg-blue",
        },
        size: {
            sm: "text-sm",
            lg: "text-lg",
        },
    },
});

// this should then take an object using the variant keys provided in the config
const classes = builder({ color: "blue", size: "sm" })

我期望返回函数的类型为:

const builder: (input: {
    color: string;
    size: string;
}) => string

然而,当鼠标悬停在VS代码中的builder函数上时,我得到了以下内容:

const builder: (input: TInput<{
    base: string;
    variants: {
        size: {
            sm: string;
            md: string;
            lg: string;
        };
    };
}>) => string

这似乎是输出泛型,而实际上没有提取变体,我不能为我的生活工作为什么。
如果任何人对此有任何指导,将不胜感激。

4nkexdtk

4nkexdtk1#

需要说明的是,类型TInput<{ base: string; variants: { color: { red: string; blue: string; }; size: { sm: string; lg: string; }; }; }>{color: string; size: string}是相同的类型,就像(5×3)+2与17是相同的数字一样。您的问题不在于编译器没有正确地计算类型;只是它没有通过IntelliSense的快速信息以您期望的方式 * 显示 * 类型。
TypeScript使用一堆启发式规则来决定如何显示类型。在保留类型别名以供显示和消除它们以供显示之间存在权衡,并且没有一种“正确”的方式来做到这一点,使每个人都满意,特别是有时候两个人会使用结构类似的代码,但想要不同的东西。正如microsoft/TypeScript#50941中提到的:
快速信息没有合同义务显示一个特定的形式或其他,也没有 * 总是 * 显示一个形式或其他(事实上,它有可能编写类型,* 不能 * 显示在某些形式)。
因此,如果您不喜欢显示TInput<...>,可以调整用法和定义以尝试获得不同的行为,但请记住,这种行为只是启发式的。
到目前为止,最简单的方法是不要引入任何你不想在显示中看到的类型别名。用TInput<T>的定义替换它。如果你不想在显示中看到Record<K, V>,也可以用它的定义替换它的使用。编译器认为你定义TInput<T>是因为你想看到它,这并不是不合理的,在任何情况下,编译器肯定不会使用它不知道的东西。
这就给了你这个:

export const classBuilder = <T extends TConfig>(config: T) => {
  ///...
  return (input: { [P in keyof T["variants"]]: string }) => {
    // return ...
  };
};

它会给出你想要的输出:

const builder = classBuilder({
  base: "text-white rounded-sm",
  variants: {
    color: {
      red: "bg-red",
      blue: "bg-blue",
    },
    size: {
      sm: "text-sm",
      lg: "text-lg",
    },
  },
});

/* const builder: (input: {
  color: string;
  size: string;
}) => void */

如果你想保留TInput<T>的定义,那么你可以开始调整它的定义。How can I see the full expanded contract of a Typescript type?和microsoft/TypeScript#28508中介绍了其中的一些内容,但是一个简单的方法是给类型添加一个“no-op”并集或交集,这不会改变实际的输出类型,但是会鼓励编译器更完整地计算类型。例如:

type TInput<T extends TConfig> =
  { [P in keyof T["variants"]]: string } & {};    
// or { [P in keyof T["variants"]]: string } & unknown;
// or { [P in keyof T["variants"]]: string } | never;

这里我们仍然没有使用Record(因为我们不想看到),但是现在我们与空对象类型{}相交,任何与{}相交的对象类型都将还原为对象类型,如果与unknown类型相交或与never类型并集,也会发生同样的情况。编译器通常更喜欢像这样急切地折叠交集/并集,而不是像TInput<{...}> & {}那样保留它。因此,您也可以在此处获得所需的行为:

/* const builder: (input: {
  color: string;
  size: string;
}) => void */

Playground代码链接

相关问题