TypeScript - בדיקת תקפות של סוג איחוד של מחרוזת בזמן ריצה?
יש לי סוג איחוד פשוט של מחרוזות ואז אני צריך לבדוק את התקפות שלה בגלל קריאות FFI ל-JavaScript "רגיל". האם יש דרך לוודא שמשתנה מסוים הוא מופע של אחת מאותן מחרוזות בזמן ריצה? משהו בסגנון של
type MyStrings = "A" | "B" | "C";
MyStrings.isAssignable("A"); // true
MyStrings.isAssignable("D"); // false
תשובה #1
נכון לגרסת TypeScript 3.8.3 אין שיטה מומלצת ברורה בנושא זה. נראה שיש שלוש פתרונות שאינם תלויים בספריות חיצוניות. בכל המקרים תצטרך לאחסן את המחרוזות באובייקט הזמין בזמן ריצה (למשל, במערך).
למטרות הדוגמאות הללו, נניח שאנחנו צריכים פונקציה לבדוק בזמן ריצה אם מחרוזת היא אחד משמות הכבשים הקנוניים, שכולנו יודעים שהם Capn Frisky, Mr. Snugs, Lambchop. הנה שלוש דרכים לעשות זאת באופן שהקומפילר של Typescript יבין.
1: הצהרת טיפוס (קל יותר)
הסר את הקסדה שלך, אמת את הסוג בעצמך, והשתמש באסרט.
const sheepNames = ['Capn Frisky', 'Mr. Snugs', 'Lambchop'] as const;
type SheepName = typeof sheepNames[number]; // "Capn Frisky" | "Mr. Snugs" | "Lambchop"
// This string will be read at runtime: the TS compiler can't know if it's a SheepName.
const unsafeJson = '"Capn Frisky"';
/**
* Return a valid SheepName from a JSON-encoded string or throw.
*/
function parseSheepName(jsonString: string): SheepName {
const maybeSheepName: unknown = JSON.parse(jsonString);
// This if statement verifies that `maybeSheepName` is in `sheepNames` so
// we can feel good about using a type assertion below.
if (typeof maybeSheepName === 'string' && sheepNames.includes(maybeSheepName)) {
return (maybeSheepName as SheepName); // type assertion satisfies compiler
}
throw new Error('That is not a sheep name.');
}
const definitelySheepName = parseSheepName(unsafeJson);
יתרון: פשוט, קל להבנה.
CON: עדין. TypeScript סומך רק על המילה שלך בכך שבדקת במידה מספקת את maybeSheepName. אם תסיר את הבדיקה בטעות, TypeScript לא יגן עליך מפני עצמך.
2: שמירות סוג מותאמות אישית (יותר לשימוש חוזר)
זהו גרסה יוקרתית יותר, כללית יותר של הצהרת הסוג למעלה, אבל זו עדיין רק הצהרת סוג.
const sheepNames = ['Capn Frisky', 'Mr. Snugs', 'Lambchop'] as const;
type SheepName = typeof sheepNames[number];
const unsafeJson = '"Capn Frisky"';
/**
* Define a custom type guard to assert whether an unknown object is a SheepName.
*/
function isSheepName(maybeSheepName: unknown): maybeSheepName is SheepName {
return typeof maybeSheepName === 'string' && sheepNames.includes(maybeSheepName);
}
/**
* Return a valid SheepName from a JSON-encoded string or throw.
*/
function parseSheepName(jsonString: string): SheepName {
const maybeSheepName: unknown = JSON.parse(jsonString);
if (isSheepName(maybeSheepName)) {
// Our custom type guard asserts that this is a SheepName so TS is happy.
return (maybeSheepName as SheepName);
}
throw new Error('That is not a sheep name.');
}
const definitelySheepName = parseSheepName(unsafeJson);
יתרון: ניתן לשימוש חוזר יותר, פחות שביר במעט, ניתן לטעון שהוא קריא יותר.
חסרון: TypeScript עדיין סומך רק על מה שאתה אומר. זה נראה כמו הרבה קוד למשהו כל כך פשוט.
3: השתמש ב-Array.find (הכי בטוח, מומלץ)
זה לא דורש הצהרות טיפוס, במקרה שאתה (כמוני) לא סומך על עצמך.
const sheepNames = ['Capn Frisky', 'Mr. Snugs', 'Lambchop'] as const;
type SheepName = typeof sheepNames[number];
const unsafeJson = '"Capn Frisky"';
/**
* Return a valid SheepName from a JSON-encoded string or throw.
*/
function parseSheepName(jsonString: string): SheepName {
const maybeSheepName: unknown = JSON.parse(jsonString);
const sheepName = sheepNames.find((validName) => validName === maybeSheepName);
if (sheepName) {
// `sheepName` comes from the list of `sheepNames` so the compiler is happy.
return sheepName;
}
throw new Error('That is not a sheep name.');
}
const definitelySheepName = parseSheepName(unsafeJson);
יתרון: לא דורש הצהרות טיפוס, הקומפיילר עדיין מבצע את כל האימות. זה חשוב לי, לכן אני מעדיף את הפתרון הזה.
חיסרון: זה נראה קצת מוזר. קשה יותר לאופטימיזציה של הביצועים.
אז זהו. אתה יכול לבחור בהגיון בכל אחת מהאסטרטגיות הללו, או להשתמש בספרייה של צד שלישי שאחרים המליצו עליה.
קנאים יצביעו נכון על כך שהשימוש במערך כאן אינו יעיל. ניתן ליעל את הפתרונות האלה על ידי המרת המערך sheepNames לקבוצה עבור חיפושים ב-O(1). זה כדאי אם אתם מתמודדים עם אלפי שמות כבשים אפשריים (או כל דבר אחר).
תשובה #2
מאז TypeScript 2.1, אתה יכול לעשות זאת בכיוון ההפוך עם המפעיל keyof.
הרעיון הוא כדלקמן. מכיוון שמידע על סוג מחרוזת מילולית אינו זמין בזמן ריצה, תגדיר אובייקט רגיל עם מפתחות כמחרוזות המילוליות שלך, ואז תיצור סוג מהמפתחות של אותו אובייקט.
להלן:
// Values of this dictionary are irrelevant
const myStrings = {
A: "",
B: ""
}
type MyStrings = keyof typeof myStrings;
isMyStrings(x: string): x is MyStrings {
return myStrings.hasOwnProperty(x);
}
const a: string = "A";
if(isMyStrings(a)){
// ... Use a as if it were typed MyString from assignment within this block: the TypeScript compiler trusts our duck typing!
}
תשובה #3
אם יש לך מספר הגדרות איגוד מיתרים בתוכנית שלך שתרצה להיות מסוגל לבדוק בזמן ריצה, אתה יכול להשתמש בפונקציה כללית StringUnion כדי ליצור את סוגיהם הסטטיים ואת שיטות בדיקת הסוגים שלהם יחד.
פונקציית תמיכה כללית
// TypeScript will infer a string union type from the literal values passed to
// this function. Without `extends string`, it would instead generalize them
// to the common string type.
export const StringUnion = <UnionType extends string>(...values: UnionType[]) => {
Object.freeze(values);
const valueSet: Set<string> = new Set(values);
const guard = (value: string): value is UnionType => {
return valueSet.has(value);
};
const check = (value: string): UnionType => {
if (!guard(value)) {
const actual = JSON.stringify(value);
const expected = values.map(s => JSON.stringify(s)).join(' | ');
throw new TypeError(`Value '${actual}' is not assignable to type '${expected}'.`);
}
return value;
};
const unionNamespace = {guard, check, values};
return Object.freeze(unionNamespace as typeof unionNamespace & {type: UnionType});
};
הגדרה לדוגמה
אנחנו גם צריכים שורת קוד בסיסית כדי להוציא את הסוג שנוצר ולמזג את ההגדרה שלו עם אובייקט המרחב השם שלו. אם הגדרה זו מיוצאת ומיובאת למודול אחר, הם יקבלו את ההגדרה הממוזגת אוטומטית; הצרכנים לא יצטרכו להוציא את הסוג מחדש בעצמם.
const Race = StringUnion(
"orc",
"human",
"night elf",
"undead",
);
type Race = typeof Race.type;
שימוש לדוגמה
בזמן הידור, סוג Race פועל באותה צורה כאילו הגדרנו איחוד מחרוזות באופן רגיל עם "orc" | "human" | "night elf" | "undead". יש לנו גם פונקציה .guard(...) שמחזירה אם ערך הוא חבר באיחוד או לא וניתן להשתמש בה כמגן סוג, ופונקציה .check(...) שמחזירה את הערך שהועבר אם הוא חוקי או אחרת זורקת TypeError.
let r: Race;
const zerg = "zerg";
// Compile-time error:
// error TS2322: Type '"zerg"' is not assignable to type '"orc" | "human" | "night elf" | "undead"'.
r = zerg;
// Run-time error:
// TypeError: Value '"zerg"' is not assignable to type '"orc" | "human" | "night elf" | "undead"'.
r = Race.check(zerg);
// Not executed:
if (Race.guard(zerg)) {
r = zerg;
}
פתרון כללי יותר: runtypes
גישה זו מבוססת על ספריית runtypes, שמספקת פונקציות דומות להגדרת כמעט כל סוג ב-TypeScript וקבלת בודקי סוג בזמן ריצה באופן אוטומטי. זה יהיה קצת יותר מפורש למקרה הספציפי הזה, אבל כדאי לשקול לבדוק זאת אם אתם צריכים משהו גמיש יותר.
הגדרה לדוגמה
import {Union, Literal, Static} from 'runtypes';
const Race = Union(
Literal('orc'),
Literal('human'),
Literal('night elf'),
Literal('undead'),
);
type Race = Static<typeof Race>;
השימוש בדוגמה יהיה זהה כמו למעלה.
תשובה #4
אתה יכול להשתמש ב-enum, ואז לבדוק אם המחרוזת נמצאת ב-Enum
export enum Decisions {
approve = 'approve',
reject = 'reject'
}
export type DecisionsTypeUnion =
Decisions.approve |
Decisions.reject;
if (decision in Decisions) {
// valid
}
