typy - Môže TypeScript odvodiť vlastnosti objektu?
Snažím sa definovať vlastnú ochranu typu, ktorá overí, či premenná typu unknown má vlastnosť data. Môj kód je uvedený nižšie.
type DataObject = {
data: string
};
function hasDataProperty(myObject: unknown): myObject is DataObject {
return typeof myObject === "object" &&
!!myObject &&
typeof myObject.data === "string";
}
Vyskúšajte to na Typescript Playground.
Ak otvoríte ihrisko, uvidíte chybu na riadku 8 uvádzajúcu Property 'data' does not exist on type 'object'., čo dáva zmysel, pretože som nepotvrdil, že vlastnosť data existuje na myObject.
Skúsil som použiť ako "data" in myObject, tak aj myObject.hasOwnProperty("data") na kontrolu, či vlastnosť existuje, ale ani jedno nevplyvnilo na odvodzovaný typ myObject v TypeScripte tak, aby zahŕňal data, stále ide o obyčajný typ object.
Mohol by som zmeniť podpis funkcie na hasDataProperty(myObject: any) alebo použiť typovú aserciu na zmenu typu z unknown, ale obe tieto možnosti ignorujú skutočné vlastnosti objektu, čo by mohlo viesť k chybám v logike kontrolóra typu.
Existuje spôsob, ako zistiť, či myObject má vlastnosť data, bez použitia typových tvrdení alebo any?
Odpoveď č. 1
Problém, ktorý máte, je v tom, že časť typeof myObject === "object" hovorí kompilátoru, že inštancia myObject je v podstate Objekt. Takže TypeScript predpokladá v zvyšku výrazu, že myObject je typu {}, čo očividne nie je.
V tejto metóde by som pustil unknown a aplikoval princíp KISS:
function isDataObject(myObject: any): myObject is DataObject {
return myObject && typeof myObject.data === "string";
}
Upraviť po rozsiahlych diskusiách so zerkms a Adamom. No, som celkom tvrdohlavý, takže som pokračoval v kritizovaní riešení zerkms. Ukazuje sa, že by mohol existovať bezpečnejší spôsob, ako budovať Type Guards, a chcem ho navrhnúť v nasledujúcom texte.
Môj nápad je použiť kontroly počas zostavovania kompilátorom na kontrolu „plánu“ typu strážcu. Definujem ValidatorDefinition nasledovne:
export type ValidatorTypes =
| "string"
| "boolean"
| "number"
| "array"
| "string?"
| "boolean?"
| "number?";
export type ValidatorDefinition<T> = {
[key in keyof T]: T[key] extends string
? "string" | "string?"
: T[key] extends number
? "number" | "number?"
: T[key] extends boolean
? "boolean" | "boolean?"
: (T[key] extends Array<infer TArrayItem>
? Array<ValidatorDefinition<TArrayItem>>
: ValidatorTypes);
};
Tak každá vlastnosť jednoduchého typu musí byť spojená s reťazcom, ktorý presne definuje typ validácie.
Potom vytvorím továreň TypeGuard:
function typeGuardFactory<T>(
reference: ValidatorDefinition<T>
): (value: any) => value is T {
const validators: ((propertyValue: any) => boolean)[] = Object.keys(
reference
).map(key => {
const referenceValue = (<any>reference)[key];
switch (referenceValue) {
case "string":
return v => typeof v[key] === "string";
case "boolean":
return v => typeof v[key] === "boolean";
case "number":
return v => typeof v[key] === "number";
case "string?":
return v => v[key] == null || typeof v[key] === "string";
case "boolean?":
return v => v[key] == null || typeof v[key] === "boolean";
case "number?":
return v => v[key] == null || typeof v[key] === "number";
default:
// we are not accepting null/undefined for empty array... Should decide how to
// handle/configure the specific case
if (Array.isArray(referenceValue)) {
const arrayItemValidator = typeGuardFactory<any>(referenceValue[0]);
return v => Array.isArray(v[key]) && v[key].every(arrayItemValidator);
}
// TODO: handle default case
return _v => false;
}
});
return (value: T): value is T =>
(value && validators.every(validator => validator(value))) || false;
}
Príklad použitia:
type DataObject = {
data: string
};
const hasDataObject = typeGuardFactory<DataObject>({ data: "string" });
Pekné je, že ak nesprávne napíšete definíciu validátora, dostanete chyby pri kompilácii.
Jednoduchý kód na testovanie jednotiek:
const validatorForDataObject = typeGuardFactory({ data: "string" });
const testCases: { value: any; expected: boolean }[] = [
{ value: null, expected: false },
{ value: undefined, expected: false },
{ value: { data: 0 }, expected: false },
{ value: { data: "0" }, expected: true }
];
testCases.forEach(testCase => {
const testResult = validatorForDataObject(testCase.value);
if (testResult === testCase.expected) {
console.info(`Success (with value ${testResult})`, testCase.value);
} else {
console.error(`Fail (with value ${testResult})`, testCase.value);
}
});
interface AnotherType {
stringProp: string;
numberProp: number;
booleanProp: boolean;
nullableStringProp?: string;
nullableNumberProp?: number;
nullableBooleanProp?: boolean;
arrayProp: {
data: string;
}[];
}
const validatorForAnotherType = typeGuardFactory<AnotherType>({
stringProp: "string",
numberProp: "number",
booleanProp: "boolean",
nullableStringProp: "string?",
nullableNumberProp: "number?",
nullableBooleanProp: "boolean?",
arrayProp: [
{
data: "string"
}
]
});
const testCases2: { value: any; expected: boolean }[] = [
{ value: null, expected: false },
{ value: undefined, expected: false },
{ value: { data: 0 }, expected: false },
{
value: {
stringProp: "string",
numberProp: 1,
booleanProp: true,
nullableStringProp: "string",
nullableNumberProp: 1,
nullableBooleanProp: false,
arrayProp: [{ data: "" }]
},
expected: true
},
{
value: {
stringProp: "string",
numberProp: 1,
booleanProp: true,
nullableStringProp: null,
nullableNumberProp: null,
nullableBooleanProp: null,
arrayProp: []
},
expected: true
},
{
value: {
stringProp: "string",
numberProp: 1,
booleanProp: true,
nullableStringProp: "string",
nullableNumberProp: null,
nullableBooleanProp: null,
arrayProp: [{ data: "" }, { data: "" }, { data: 0 }]
},
expected: false
},
{
value: {
stringProp: "string",
numberProp: 1,
booleanProp: true,
nullableStringProp: "string",
nullableNumberProp: null,
nullableBooleanProp: null,
arrayProp: [{ data: "" }, { data: "" }, { data: 0 }]
},
expected: false
},
{
value: {
stringProp: null,
numberProp: 1,
booleanProp: true,
nullableStringProp: null,
nullableNumberProp: null,
nullableBooleanProp: null,
arrayProp: []
},
expected: false
}
];
testCases2.forEach(testCase => {
const testResult = validatorForAnotherType(testCase.value);
if (testResult === testCase.expected) {
console.info(`Success (with value ${testResult})`, testCase.value);
} else {
console.error(`Fail (with value ${testResult})`, testCase.value);
}
});
Zábavné je, že validačný kód stále veľa používa any, ale všetko ostatné vo svete porcelánu mimo ochrany sa zdá byť celkom pevné. Je to len náčrt, ale myslím, že ukazuje určitý potenciál.
Celý úryvok.
Odpoveď č. 2
Dobre, po googlení a konzultácii s niekým iným som dospel k tomuto riešeniu:
type DataObject = {
data: string
};
function isDataObjectLike(v: unknown): v is { [K in keyof DataObject]: unknown } {
return typeof v === 'object'
&& v !== null
&& 'data' in v;
}
function hasDataProperty(v: unknown): v is DataObject {
return isDataObjectLike(v)
&& typeof v.data === 'string';
}
typescript/hranie
Nie že by bolo výrazne lepšie ako to s any, ale poskytuje väčšiu bezpečnosť od kompilátora.
