typescript - Kan inte skapa användardefinierad typvakt för strängenum


Jag skapar en användardefinierad typvakt för att avgöra om en (osäker) sträng hämtad vid körning är en medlem av ett sträng-enum:

enum Vendor {
  ACME = "Acme Co.",
  FOOBAR = "Foo Bar Industries"
}

export let isVendor = (x: string | Vendor): x is Vendor => {
  return !!Vendor[x];
};

När jag försöker kompilera koden ovan får jag följande:

[ts] Element implicitly has an 'any' type because expression is not of type 'number'.
(parameter) x: string

En möjlig lösning är som följer:

export let isVendor = (x: any): x is Vendor => {
  return !!Vendor[x];
};

...men jag föredrar att undvika any när jag vet att typen åtminstone är en string.

Jag kan undvika att använda any genom att ändra typesignaturen för x till number | string | Vendor, men detta är fortfarande mindre än idealiskt.

Finns det något sätt att utföra åtkomst med hakparenteser på en enum med en sträng?

Svar #1

Jag får det felet när jag har 'no implicit any' aktiverat. Jag misstänker att detta kan vara ett kompilatorproblem.

Det fungerar om du bara kastar x till any:

enum Vendor {
   A = "a",
   B = "b"
}

const isVendor = (x: string | Vendor): x is Vendor => {
   return !!Vendor[<any>x];
};

console.info(isVendor("A")); // true
console.info(isVendor("a")); // false
console.info(isVendor("C")); // false

Jag märker att i exempel på denna funktion, har de antingen liknande typomvandlingar, eller så är parametern av typen any.

Kolla även här för en viss förklaring av detta problem.

Jag tycker fortfarande att du behöver bestämma om du letar efter namn eller värden. Baserat på din implementation tror jag att du egentligen kanske letar efter isVendorValue snarare än isVendorName. (Jag tycker inte ens att det är meningsfullt att fråga om namnet är en Vendor, eftersom det inte är det; du vill veta om värdet är en Vendor.)

enum Vendor {
   A = "a",
   B = "b"
}

// doesn't make sense. the name cannot be a Vendor.
const isVendorName = (name: string | Vendor): name is Vendor => {
   return !!Vendor[<any>name];
};

// requires ES7, see below
const isVendorValue = (value: string | Vendor): value is Vendor => {
   return Object.values(Vendor).some((x: any) => x === value);
};

console.info(isVendorName("A"));       // true
console.info(isVendorName("a"));       // false
console.info(isVendorName("C"));       // false
console.info(isVendorName(Vendor.A));  // false

console.info(isVendorValue("A"));      // false
console.info(isVendorValue("a"));      // true
console.info(isVendorValue("C"));      // false
console.info(isVendorValue(Vendor.A)); // true

Object.values är uppenbarligen ES7 så här är en alternativ implementation. Eftersom värdena inte skulle ändras vid körning kan du troligen också dra nytta av att cache:a värdena.

const isVendorValue = (value: string | Vendor): value is Vendor => {
   return Object
      .keys(Vendor)
      .map((key: any) => Vendor[key])
      .some((x: any) => x === value);
};

En annan sak jag skulle föreslå nu när jag tänker på det är att tvärtemot ditt förslag bör parametern förmodligen faktiskt vara av typen any ändå. Du säger "Jag vet att typen är åtminstone en string" men det begränsar bara onödigt användningen av denna metod till att testa strängar.

Och du vet egentligen inte det, beroende på var du använder metoden. Vad du vet är att om det inte är en sträng, då är det definitivt inte en Vendor.

Idén är, givet ett objekt eller ett värde (string eller inte), att avgöra om det är en medlem av detta enum, och om det är det, behandla det som en sådan i typ systemet. En mer allmänt tillämplig metod är bättre än en onödigt snäv.

const isVendorValue = (value: any): value is Vendor => {
   return typeof value === "string" &&
      Object
      .keys(Vendor)
      .map((key: any) => Vendor[key])
      .some((x: any) => x === value);
};