在被这个问题困扰了一年后,我正在重新审视这个问题.我不知道如何简洁地框定这个问题,所以请告诉我.

问题:我想通过其kind属性的值来缩小嵌套对象的联合类型.然而,kind字段是可选的,因为默认情况下它应该是"string".

TField型(简化):

type TField = {
  field: string;
  name: string;
  placeholder?: string;
  required?: boolean;
}& (
  | { kind: "string"; value?: string; block?: boolean }
  | { kind: "password"; value?: string }
  | { kind: "email"; value?: string }
  | { kind: "number"; value?: number }
  | { kind: "boolean"; value?: boolean; inline?: boolean; toggle?: boolean }
)

// ----- EDIT: I want the "name","field","kind" to be optional
// ----------- By default: "kind" should be 'string'

type PartialBy<T,K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
type TLabeledField = PartialBy<TField, "name"|"field"|"kind">

type TFieldCluster = {
    [key: string]: TLabeledField;
  };

function makeFields(cluster: TFieldCluster){
  const fieldMap = Object.entries(cluster).map(([str, path]) => {
    let label = str;
    let name,field;
    let required = false;

    if (label.endsWith("*")) {
      required = true
      label = label.slice(0,-1)
    }
    if (label.startsWith("$")){
      field = label.slice(1);
      name = label.replace(/([A-Z])/g, " $1").toLowerCase();
    } else {
      field = label // <-- will fix
      name = label
    }
    

    return [
      field,
      {
        type: "string",
        name,
        field,
        required,
        ...path,
      },
    ];
  });
  return Object.fromEntries(fieldMap);
}

const fields = makeFields({
      title: {},
      subtitle: {kind:"boolean", toggle: true}, // <-- SHOULD BE VALID BECAUSE I SPECIFIED 'KIND'
      about: { block: true },
      count: { kind:"number", block: true }, // <-- INVALID: 'BLOCK' ISN'T ON NUMBER!
})

推荐答案

问题的核心是,正如所写的那样,

type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>

does not distribute over unions in T. You clearly intend that PartialBy<T1 | T2 | T3, K> will evaluate to PartialBy<T1, K> | PartialBy<T2, K> | PartialBy<T3, K>. But it doesn't. Neither the Pick nor the Omit utility types distribute over unions. (See Why doesn't discriminated union work when I omit/require props?) Instead they act on unions at once, collapsing them to single object types:

这意味

type TLabeledField = PartialBy<TField, "name" | "field" | "kind">

计算结果为

type TLabeledField = Omit<TField, "name" | "field" | "kind"> &
  Partial<Pick<TField, "name" | "field" | "kind">>

这相当于

type TLabeledField = {
    placeholder?: string;
    required?: boolean;
    value?: string | number | boolean;
    name?: string;
    field?: string;
    kind?: "string" | "number" | "boolean" | "password" | "email";
}

这不是你希望TLabeledField人的行为方式.


您可以采用任何generic类型,并通过将其包装在distributive conditional type中使其分布在类型参数上. 如果NonDistrib<T>不是分配的,则可以写type Distrib<T> = T extends unknown ? NonDistrib<T> : never. 您并不是真的想写check T extends unknown(目的是它应该始终为真,所以您也可以写T extends anyT extends T或其他始终为真的东西).只是这样做使得类型具有分配性.

才能为您提供您

type PartialBy<T, K extends keyof T> = T extends unknown ?
    Omit<T, K> & Partial<Pick<T, K>> :
    never;

现在

type TLabeledField = PartialBy<TField, "name" | "field" | "kind">

计算结果为

type TLabeledField = (Omit<{
    field: string; name: string;
    placeholder?: string; required?: boolean;
} & {
    kind: "string"; value?: string; block?: boolean;
}, "name" | "field" | "kind"> & Partial<Pick<{
    field: string; name: string;
    placeholder?: string; required?: boolean;
} & {  
    kind: "string"; value?: string; block?: boolean;
}, "name" | "field" | "kind">>) | (Omit<{ ? */

扩张至

type TLabeldField = {
    placeholder?: string; required?: boolean; value?: string;
    block?: boolean;
    name?: string; field?: string; kind?: "string";
} | {
    placeholder?: string; required?: boolean; value?: string;
    name?: string; field?: string; kind?: "password";
} | {
    placeholder?: string; required?: boolean; value?: string;
    name?: string; field?: string; kind?: "email";
} | {
    placeholder?: string; required?: boolean; value?: number;
    name?: string; field?: string; kind?: "number";
} | {
    placeholder?: string; required?: boolean; value?: boolean;
    inline?: boolean;
    toggle?: boolean;
    name?: string; field?: string; kind?: "boolean";
} */

因此,您的代码现在可以按照预期的方式运行.

Playground link to code

Typescript相关问答推荐

当类型不能undefined时,如何使编译器抛出错误?

Typescript问题和缩减器状态不会在单击时添加用途.属性'状态和调度'不存在于userContextType类型上'|null'

如何使打包和拆包功能通用?

当类型断言函数返回假时,TypScript正在推断从不类型

角形标题大小写所有单词除外

如何在第一次加载组件时加载ref数组

为什么TypeScript失go 了类型推断,默认为any?''

在对象中将缺省值设置为空值

具有自引用属性的接口

如何正确调用创建的类型?

避免混淆两种不同的ID类型

使用或属性编写无限嵌套的接口

如何键入派生类的实例方法,以便它调用另一个实例方法?

类型实例化过深,可能是无限的&TypeScrip中的TupleUnion错误

编剧- Select 动态下拉选项

我可以将一个类型数组映射到TypeScrip中的另一个类型数组吗?

基于闭包类型缩小泛型类型脚本函数的范围

为什么`map()`返回类型不能维护与输入相同数量的值?

我如何键入它,以便具有字符串或数字构造函数的数组可以作为字符串或数字键入s或n

必须从注入上下文调用Angular ResolveFn-inject()