Typescript:如何在嵌套对象之间强制唯一键

cgyqldqp  于 2022-11-26  发布在  TypeScript
关注(0)|答案(3)|浏览(140)

我要求一个对象类型不能在嵌套对象中重复键。例如,如果foo.bar包含键hello,那么foo.baz就不能包含该键。有没有办法在类型级别上强制执行这一点?
一种简化的表述可能如下:

type NestedUniqueKeys<T extends Object> = any // <- what goes here?

interface Something {
  one: string
  two: string
  three: string
  four: string
}

const good: NestedUniqueKeys<Something> = {
  foo: {
    three: 'hi',
    one: 'hi',
  },
  bar: {
    two: 'hiya',
  },
}

// @ts-expect-error
const bad: NestedUniqueKeys<Something> = {
  foo: {
    two: 'hi', // duplicated
    one: 'hi',
  },
  bar: {
    two: 'hiya', // duplicated
  },
}

因此,一个更简单的步骤可能是,NestedUniqueKeys如何被公式化为单层嵌套?
那么,如何将其推广到任意嵌套呢?

const good: NestedUniqueKeys<Something> = {
  foo: {
    three: 'hi',
    baz: {
      one: 'oh',
      bill: {
        four: 'uh',
      },
    },
  },
  bar: {
    two: 'hiya',
  },
}

// @ts-expect-error
const bad: NestedUniqueKeys<Something> = {
  foo: {
    three: 'hi',
    baz: {
      one: 'oh',
      bill: {
        four: 'uh', // duplicated
      },
    },
  },
  bar: {
    two: 'hiya',
    foobar: {
      four: 'hey', // duplicated
    },
  },
}

在最后的公式化中,是否可以推断出完整的键集,从而不需要传入类型参数?

编辑

我尝试了一个初步的解决方案,但这导致了 all 嵌套键被禁止。我猜这是因为当K被传递到递归的NestedUniqueKeys时,它被推断为string?我不知道为什么...

type NestedUniqueKeys<Keys extends string = never> = {
  [K in string]: K extends Keys
    ? never
    : string | NestedUniqueKeys<K|Keys>
}

Playground

编辑2

另一个尝试,我不知道为什么这是不允许任何键在嵌套的对象...

type NestedUniqueKeys<Keys extends string = never> =
  { [K in string]: K extends Keys ? never : string } extends infer ThisLevel
  ? keyof ThisLevel extends string
  ? ThisLevel & {
    [N in string]: N extends Keys ? never : NestedUniqueKeys<keyof ThisLevel|Keys>
  }
  : never
  : never
vwhgwdsa

vwhgwdsa1#

我找到了下面这个简单案例的解决方案:

type NestedUniqueKeys<T extends object> = keyof T[keyof T] extends never ? T : never

它实际上是相当整洁和小。更一般的解决方案稍微复杂一点:

type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
    11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...0[]]
// extracts all keys of the object in the form "A.B.C.D", D limits depth
type AllKeys<T, D extends number = 10> = [D] extends [never] ? never : T extends object ?
    UnionToString<{ [K in keyof T]-?: K extends string | number ?
        `${K}` | AllKeys<T[K], Prev[D]>
        : never
    }[keyof T]> : never

// convert a union to an intersection: X | Y | Z ==> X & Y & Z
type UnionToIntersection<U> =
  (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never

// convert a union to an overloaded function X | Y ==> ((x: X)=>void) & ((y:Y)=>void)     
type UnionToOvlds<U> = UnionToIntersection<U extends any ? (f: U) => void : never>;

// returns true if the type is a union otherwise false
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;

// takes last from union
type PopUnion<U> = UnionToOvlds<U> extends ((a: infer A) => void) ? A : never;

// converts "A" | "B" | "C" ==> "C.B.A"
type UnionToString<U> = IsUnion<U> extends false ? (U extends string ? U : never)
: (PopUnion<U> extends infer P extends string ? `${P}.${UnionToString<Exclude<U, P>>}`: "")

// Checks if "A.B.B.C" has any duplicates between the "."
type Unique<T> = T extends `${infer First}.${infer Rest}` ? Contains<First, Rest> extends true ? false : Unique<Rest> : true

// Checks if "A" is contained in "A.B.C"
type Contains<T, STR> = T extends STR ? true : STR extends `${infer First}.${infer Rest}` ? T extends First ? true : Contains<T, Rest> : false

type NestedUniqueKeys<T extends object> = Unique<AllKeys<T>>

我从各种来源得到了一些助手类型。这对我来说似乎并不是真的有用,但这是一个有趣的挑战。

8tntrjer

8tntrjer2#

这里有另一种方法,我试图保持简单。
我创建了三个泛型类型来进行验证。

type GetLeafPaths<T, K extends keyof T = keyof T> = 
  K extends K
    ? T[K] extends object
      ? `${GetLeafPaths<T[K]> & string}${K & string}.`
      : `${K & string}.`
    : never

GetLeafsPaths接受一个对象T,并将所有到leaf的路径计算为字符串。

// "three.foo." | "one.baz.foo." | "four.bill.baz.foo." | "two.bar." | "four.foobar.bar."

注意,我选择了路径的逆序,这使得以后获取leaf值更容易,因为它只是第一个元素。
ExtractLeafName取一条路径并提取LeafName

type ExtractLeafName<Path extends string> =  
  Path extends `${infer LeafName}.${string}`
    ? LeafName
    : never

type Result1 = ExtractLeafName<"four.bill.baz.foo.">
//   ^? type Result1 = "four"

现在转到主Validation类型。

type Validate<T, Paths = GetLeafPaths<T>> = 
    {
      [K in Paths & string]: 
        ExtractLeafName<K> extends ExtractLeafName<Exclude<Paths, K> & string> 
          ? true 
          : false
    } extends Record<string, false>
      ? true
      : false

这个想法很简单:首先用GetLeafPaths得到所有的Paths,然后在Paths中的每条路径上MapP
对于每个路径P,我们使用Exclude<Paths, P>来获取Paths中所有其他不是P的路径。我们使用ExtractLeafName来获取PExclude<Paths, P>中的叶名称,并将它们与extends进行比较。如果叶名称位于任何其他路径中,我们返回true,如果不是,则返回false
这将生成一个对象类型:

{
    "three.foo.": false;
    "one.baz.foo.": false;
    "four.bill.baz.foo.": true;
    "two.bar.": false;
    "four.foobar.bar.": true;
}

重复的分叶名称具有true类型。
剩下要做的就是检查这个对象类型中是否有任何true值,我们可以使用extends Record<string, false>来检查。
如果任何叶名称重复,Validate类型将返回false
现在我们只需要一个函数,我们可以向它传递一真实的对象。

function nestedUniqueKeys<T>(arg: Validate<T> extends true ? T : never) {
    return arg
}

参数中的一个简单条件类型让我们使用Validate来检查T中是否有重复的叶。

// Error: is not assignable to parameter of type 'never'
nestedUniqueKeys({
    foo: {
        three: 'hi',
        baz: {
            one: 'oh',
            bill: {
                four: 'uh', // duplicated
            },
        },
    },
    bar: {
        two: 'hiya',
        foobar: {
            four: 'hey', // duplicated
        },
    },
})

// OK
nestedUniqueKeys({
  foo: {
    three: 'hi',
    baz: {
      one: 'oh',
      bill: {
        four: 'uh',
      },
    },
  },
  bar: {
    two: 'hiya',
  },
  topLevelLeaf: "asdasd"
})

这个错误消息并没有什么帮助,但是我已经尽力了。
注:这也可以简单地扩展为不仅验证唯一的leaf,而且验证所有属性。您所需要做的就是修改GetLeafPaths以同时构造其他属性的路径:

type GetLeafPaths<T, K extends keyof T = keyof T> = 
  K extends K
    ? T[K] extends object
      ? `${K & string}.` | `${GetLeafPaths<T[K]> & string}${K & string}.`
      : `${K & string}.`
    : never

Playground

2skhul33

2skhul333#

请考虑以下示例:

type Primitives = string | number | symbol;

type Values<T> = T[keyof T]

// https://stackoverflow.com/questions/50374908/transform-union-type-to-intersection-type/50375286#50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
    k: infer I
) => void
    ? I
    : never;

// https://stackoverflow.com/questions/53953814/typescript-check-if-a-type-is-a-union#answer-53955431
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true

/**
 * Compute all possible property combinations
 */
type KeysUnion<T, Cache extends PropertyKey = never> =
    /**
     * If T extends string | number | symbol -> return Cache, this is the end/last call
     */
    T extends PropertyKey
    ? Cache
    : {
        /**
         * Otherwise, iterate through keys of T, because T is an object
         * and unionize Cache 
         */
        [P in keyof T]: KeysUnion<T[P], Cache | P>

    }[keyof T]

type Validate<
    /**
     * Our main data structure
     */
    Obj,
    /**
     * Expected to be  key from the union of all keys
     */
    Key extends PropertyKey,
    /**
     * Result
     */
    Cache extends Record<string, any> = never,
    /**
     * Index is provided to  distinguish same keys on different nesting levels. Because if you unionize same keys, you will end up with one element in the union
     */
    Index extends number[] = [],
    /**
     * Root is provided to make a difference between keys on the same nesting level, because diff leafs might have same keys on same levels
     */
    Root extends string = ''

> =
    /**
     * If provided Obj is a primitive, it is the end of recursion
     */
    Obj extends Primitives
    /**
     * Our result
     */
    ? Exclude<Cache, []>
    : {
        /**
         * Iterate through object keys
         */
        [Prop in keyof Obj]:
        /**
         * Check whether object keys extends argument Key, it will be first encounter of a key
         * Hence, if it will be a second one encounter, we will add to our cache next key: Root-Prop-index
         * Son if Cache contains a union it means that we have more than one match of a key
         */
        Prop extends Key
        ? Validate<Obj[Prop], Key, Record<Key, `${Root}-${Prop & string}-${Index['length']}`>, [...Index, Index['length']], Root extends '' ? Prop : Root>
        : Validate<Obj[Prop], Key, Cache, [...Index, Index['length']], Root extends '' ? Prop : Root>
    }[keyof Obj]

type Structure = {
    foo: {
        three: 'hi',
        bar: {
            one: 'oh',
            bill: {
                four: 'uh', // duplicated
            },
        },
    },
    bar: {
        two: 'hiya',
        foobar: {
            four: 'hey', // duplicated
        },
    },
}


type Distribute<Data, Keys extends PropertyKey = KeysUnion<Data>> =
    Keys extends any ? IsUnion<Validate<Data, Keys>> extends true ? Record<Keys, 'many'> : Record<Keys, 'one'> : never

type Result = keyof UnionToIntersection<Exclude<Distribute<Structure>, Record<string, 'one'>>>

Playground
您可以在注解和my blog中找到解释

相关问题