2

Trying to create a type for a fairly simple, but unpredictable object. Some examples:

{
  "title": "Foo",
  "x": ["A", "B"]
}
{
  "title": "Bar",
  "y": ["C", "D"]
}

So, I know the objects will have a title: string, and I know it will have one other property, that will be of type string[], but the name of that property can be any string.

Is there a way to make a type in Typescript that works with this? I.e. is there a way to type a single property with an unknown name?


I have tried the following:

  • Complains title is not of type string[], and also allows more than one key:
type SomeType= {
  title: string;
  [key: string]: string[];
};
  • Works, but is incorrect, since key should only be string[], and also allows more than one key:
type SomeType= {
  title: string;
  [key: string]: string | string[];
};
Svish
  • 152,914
  • 173
  • 462
  • 620
  • It seems this question https://stackoverflow.com/questions/45258216/property-is-not-assignable-to-string-index-in-interface is also wants to solve it. – Onikur Mar 04 '20 at 16:49

2 Answers2

1

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

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Definitely would've preferred to have a less fragile data type, but they're are coming from old insurance applications stored in a database as JSON-objects, and the only thing I can know for sure is `type Application = { [key: string]: Something }`. Where `Something` theoretically could be anything, but I'm trying to narrow it down to at least cover the patterns I do know. Looking at these answers, I'm thinking of maybe just sticking with `any` and instead run everything through a normalizer function of some sort. – Svish Mar 04 '20 at 18:27
0

Possible using a generic type:

type TitleAndArray<K extends string>
    = K extends 'title'
    ? never
    : { title: string } & { [k in K]: string[] };

Note that this isn't quite as useful as you might like. Object types don't forbid the presence of other properties, so the presence of a property named x doesn't guarantee that its value is an array of strings. However, if you take both the object and its property name, like below, then Typescript will check that K is correct at the call-site:

function test<K extends string>(key: K, obj: TitleAndArray<K>): void {
    // ok
    let arr: string[] = obj[key];
}

That said, Typescript does some unsound inference here, so if you use a different property, it will still think the property exists and has type string[]. So, use with caution.

function unsoundTest<K extends string>(obj: TitleAndArray<K>): void {
    let arr: string[] = obj.somethingElse;
}

Playground Link

kaya3
  • 47,440
  • 4
  • 68
  • 97