Hur man skriver ett objekt med en känd och en okänd nyckel i Typescript?


Försöker skapa en typ för ett ganska enkelt, men oförutsägbart objekt. Några exempel:

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

Så, jag vet att objekten kommer att ha en title: string, och jag vet att de kommer att ha en annan egenskap, som kommer att vara av typen string[], men namnet på den egenskapen kan vara vilken sträng som helst.

Finns det ett sätt att skapa en typ i Typescript som fungerar med detta? Dvs. finns det ett sätt att typa en enkel egenskap med ett okänt namn? 🤔

Jag har försökt följande:

  • Klagar på att title inte är av typen string[], och tillåter också mer än en key:
type SomeType= {
  title: string;
  [key: string]: string[];
};
  • Fungerar, men är felaktigt, eftersom key endast ska vara string[], och tillåter dessutom mer än en key:
type SomeType= {
  title: string;
  [key: string]: string | string[];
};

Svar #1

Detta är en duplicering av denna fråga. Jag kommer att anpassa mitt svar där för detta innan jag röstar för att stänga. Observera att detta är otroligt skört och man kan verkligen känna hur man kämpar med kompilatorn. Mitt råd är att ändra din datastruktur till något mer tydligt, som interface SomeType {title:string; unknownPropName: string; unknownPropVal: string[]} och bara konvertera till din version där du behöver. Du kommer att bli gladare.

Här är den fula, galna, generiska lösningen som försöker upptäcka hur många extra tangenter du har:

// 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

Lekplatslänk

Svar #2

Möjligt att använda en generisk typ:

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

Observera att detta inte är riktigt lika användbart som du kanske önskar. Objekt-typer förbjuder inte förekomsten av andra egenskaper, så förekomsten av en egenskap med namnet x garanterar inte att dess värde är en array av strängar. Om du däremot tar både objektet och dess egenskapsnamn, som nedan, så kommer Typescript att kontrollera att K är korrekt vid anropsstället:

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

Det sagt, Typescript gör en viss osäker inferens här, så om du använder en annan egenskap, kommer det ändå att tro att egenskapen existerar och har typen string[]. Så, använd med försiktighet.

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

Lekplatslänk