For readability sake, I've slices one big object into multiple smaller:

const ABC_ROUTES = {
  main: '/abc',
  details: '/abc/:id'
} as const

const XYZ_ROUTES = {
  mainAbc: '/xyz',
  details: '/xyz/:id'
} as const

export const ROUTES = {
  ...ABC_ROUTES,
  ...XYZ_ROUTES,
} as const 

Neither ESLint's no-dupe-keys (8.57.1) nor Typescript(5.7.3 if that matters) does not detect key match and property is silently overwritten.

Is there a way to detect key collision statically when using spread operator?

3 Replies 3

Hmm, another open-ended question where there's no difference between a comment and an answer. My comment-answer is that I don’t know of a way to do this automatically.

You can write a utility function using utility types to take the place of spread, like

export const ROUTES = noOverwriteSpread(
    ABC_ROUTES, XYZ_ROUTES); // error!
//              ~~~~~~~~~~
// property 'details' has conflicting types in some constituents.

export const OKAYROUTES = noOverwriteSpread(
    ABC_ROUTES, 
    { mainAbc: 'xyz', detailsAbc: '/xyz/:id' } as const); // okay
/* const OKAYROUTES: {
    readonly main: "/abc";
    readonly details: "/abc/:id";
    readonly mainAbc: "xyz";
    readonly detailsAbc: "/xyz/:id";
} */

where noOverwriteSpread() looks possibly like

function noOverwriteSpread<T extends object[]>(
    ...args: T extends NoDupeKeys<T> ? T : NoDupeKeys<T>
) {
    return Object.assign({}, ...args) as {
        [I in keyof T]: (x: T[I]) => void
    }[number] extends (x: infer I) => void ?
        { [K in keyof I]: I[K] } : never
}
type NoDupeKeys<T extends object[], K extends PropertyKey = never, A extends object[] = []> =
    T extends [infer F, ...infer R extends object[]] ?
    NoDupeKeys<R, K | keyof F, [...A, F & Partial<Record<K & keyof F, never>>]> :
    A

Playground link

const ABC_ROUTES = {
  main: '/abc',
  details: '/abc/:id'
} as const

const XYZ_ROUTES = {
  mainAbc: '/xyz',
  details: '/xyz/:id'
} as const

type intersection = keyof typeof ABC_ROUTES & keyof typeof XYZ_ROUTES

detects for key collision. Then you will need to check if intersection is empty(never) or not. Something like that:

type CheckResult = intersection extends never ? true : false
const check: CheckResult = true

That would show type error if there are duplicated keys("Type true is not assignable to type false")

If TS supported conditional compilation or similar, this could be used before the spread operation overwrites the previous key.

Playground

type AnyObject = Record<PropertyKey, unknown>;

// properties of X also present in Y
type Intersection<X extends AnyObject, Y extends AnyObject> = {
    [K in keyof X as K extends keyof Y ? K : never]: X[K];
};

// returns never if there is an intersection between X and Y,
// returns the conjunction of X and Y otherwise.
type NoIntersection<X extends AnyObject, Y extends AnyObject> = keyof Intersection<
    X,
    Y
> extends never
    ? X & Y
    : never;

type StrictMerge<T extends Record<PropertyKey, unknown>[]> = T extends [infer X extends AnyObject]
    ? X
    : T extends [infer X extends AnyObject, infer Y extends AnyObject]
        ? NoIntersection<X, Y>
        : T extends [
                    infer X extends AnyObject,
                    infer Y extends AnyObject,
                    ...infer Z extends AnyObject[],
              ]
            ? StrictMerge<[NoIntersection<X, Y>, ...Z]>
            : never;

// function to mimic spread operator.Z
function strictMerge<Z extends AnyObject[]>(...sources: Z): StrictMerge<Z> {
    // you could implement runtime validation. but this is fine
    // for now since you said just typescript or eslint.
    return Object.assign({}, ...sources) as StrictMerge<Z>;
}

// sample use.
const a = strictMerge(
    {
        x: 'foo',
    },
    {
        y: 'bar',
    },
    {
        z: 'baz',
    },
    {
        i: 'foo',
    },
    {
        j: 'bar',
    },
);

// never
const b = strictMerge(
    {
        x: 'foo',
    },
    {
        y: 'bar',
    },
    {
        y: 'baz',
    },
);

this does it by making sure all the objects does not contain keys of the other objects provided.

Your Reply

By clicking “Post Your Reply”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.