This is a duplicate of this question. I'll adapt my answer there for this before voting to close. Do note that this is incredibly fragile and you can just feel yourself fighting with the compiler. My advice is to change your data structure to be more obvious, like interface SomeType {title:string; unknownPropName: string; unknownPropVal: string[]}
and just convert to your version wherever you need to. You'll be happier.
Here's the ugly crazy generic workaround that tries to detect how many extra keys you have:
// detect if T is a union
type IsAUnion<T, Y = true, N = false, U = T> = U extends any
? ([T] extends [U] ? N : Y)
: never;
// detect if T is a single string literal
type IsASingleStringLiteral<
T extends string,
Y = true,
N = false
> = string extends T ? N : [T] extends [never] ? N : IsAUnion<T, N, Y>;
type BaseObject = { title: string };
// if C conforms to desired ComboObject, return C.
type VerifyComboObject<
C,
X extends string = Extract<Exclude<keyof C, keyof BaseObject>, string>
> = BaseObject & Record<
IsASingleStringLiteral<X, X, "!!!ExactlyOneUnknownPropertyRequired!!!">,
string[]
>
// only accept parameters of type C that extend VerifyComboObject<C>
const asComboObject = <C>(x: C & VerifyComboObject<C>): C => x;
// testing
const okayComboObject = asComboObject({
title: "Foo",
unknownName: ["A", "B"]
}); // okay
const wrongExtraKey = asComboObject({
title: "Foo",
unknownName: 3
}); // error, number not assignable to string[]
const missingExtraKey = asComboObject({
title: "Foo",
}); // error, '!!!ExactlyOneUnknownPropertyRequired!!!' is missing
const tooManyExtraKeys = asComboObject({
title: "Foo",
unknownName: ["A", "B"],
anAdditionalName: ["A", "B"]
}); // error, '!!!ExactlyOneUnknownPropertyRequired!!!' is missing
Playground link