typescript - วิธีตรวจสอบ Type Guard บนคุณสมบัติที่มีดัชนี
ฉันมีประเภท nestedObj ซึ่งใช้ลายเซ็นดัชนีดังนี้:
type nestedObj = {[key: string]: nestedObj} | {[key: string]: number}
ฉันจะสร้างตัวป้องกันประเภทสำหรับประเภท nestedObj นี้ได้อย่างไร?
const isNestedObj = (obj: any): obj is nestedObj => {
if (obj === null || obj === undefined)
return false;
??????
return true;
}
========= แก้ไข ==========
ขอโทษ นี่คือข้อมูลเพิ่มเติม Obj ที่ซ้อนอยู่มีดังต่อไปนี้
const testObj1 = { a: { b: { c: 42 } } };
const testObj2 = { ab: { bc: { cd: { d: 2 } } } };
const testObj3 = { xy: 3};
วัตถุควรมีเพียงแค่คีย์เดียวเท่านั้น
เราจำเป็นต้องตรวจสอบด้วยว่า obj เป็นวัตถุที่ไม่ใช่ค่า null
ฉันลองอีกครั้งและได้สิ่งที่ออกมาแบบนี้
const isNestedObj = (obj: any): obj is nestedObj => {
// terminating conditions
if (typeof obj !== 'object' || obj === null || obj === undefined)
return false
// if number of keys is not exactly 1
const objKeys = Object.keys(obj);
if (objKeys.length !== 1)
return false
// if the next nested object is a number, return true
if (typeof obj[objKeys[0]] === 'number')
return true
else return isNestedObj(obj[objKeys[0]]);
}
ดูเหมือนว่าจะใช้ได้ผลสำหรับฉัน แต่ไม่แน่ใจว่านี่เป็นวิธีแก้ปัญหาที่ดีที่สุดหรือไม่
คำตอบ #1
ประเภท
type NestedObj = { [key: string]: NestedObj } | { [key: string]: number }
เป็นสหภาพของประเภทวัตถุสองประเภท แต่ละประเภทมีลายเซ็นดัชนีเป็นสตริง ซึ่งหมายความว่า NestedObj ต้องเป็นวัตถุที่คุณสมบัติทั้งหมดต้องมีประเภทเป็น number หรือทั้งหมดต้องมีประเภทเป็น NestedObj เอง จำนวนคุณสมบัติไม่ได้จำกัด ซึ่งทำให้เกิดพฤติกรรมดังต่อไปนี้:
let n: NestedObj;
n = {}; // okay
n = { a: 0, b: 1, c: 2 }; // okay
n = { a: {}, b: {}, c: {} }; // okay
n = { a: { d: 0 }, b: { e: 1 }, c: { f: {} } }; // okay
n = { a: 0, b: 1, c: "abc" }; // error, string is not allowed
n = { a: 0, b: 1, c: {} }; // error, mix of numbers and objects
n = { a: { d: 0 }, b: { e: 1 }, c: { f: { g: "abc" } } }; // error, nested string
โปรดทราบว่าข้อกำหนดที่ระบุว่า "วัตถุควรมีเพียงกุญแจเดียวเท่านั้น" ไม่ได้ ถูกบังคับใช้โดยประเภทนี้
ดังนั้นเราอาจต้องยอมรับประเภทของคุณตามค่าที่เห็นและเขียนฟังก์ชันตัวป้องกันประเภทสำหรับมัน หรือ เราต้องเขียนฟังก์ชันที่ตรวจสอบข้อกำหนดของคุณแต่ ไม่ ถือว่ามันเป็นฟังก์ชันตัวป้องกันประเภทสำหรับประเภทนั้น ฉันจะทำแบบแรก
ข้อกำหนดที่ว่าวัตถุต้องมีคีย์เพียงหนึ่งอย่างไม่สามารถแสดงได้ง่ายใน TypeScript เว้นแต่ว่าจะมีกรณีการใช้งานที่สำคัญอย่างยิ่ง การผ่อนปรนข้อกำหนดหรือเปลี่ยนโครงสร้างข้อมูลให้เป็นสิ่งที่สามารถบังคับใช้ได้ เช่น [string, ...string[], number] จะดีกว่า และเขียนเป็น ["a", "b", "c", 0] แทน {a:{b:{c:0}}}
อีกครั้ง ฉันกำลังจะเขียนฟังก์ชันตรวจชนิดแบบกำหนดเองที่ตรวจสอบว่าข้อมูลนำเข้าเป็น NestedObj ที่ถูกต้องหรือไม่โดยไม่สนใจจำนวนคีย์ นี่คือตัวอย่างวิธีหนึ่ง:
const isNestedObj = (obj: any): obj is NestedObj => {
if (!obj) return false;
if (typeof obj !== "object") return false;
const values = Object.values(obj);
if (values.every(v => typeof v === "number")) return true;
return values.every(isNestedObj);
}
ที่นี่เราจะตรวจสอบให้แน่ใจว่า obj เป็นอ็อบเจ็กต์ที่ไม่ใช่ค่า null ก่อน จากนั้นเราใช้ Object.values() เพื่อดึงค่าของคุณสมบัติทั้งหมดของอ็อบเจ็กต์ หาก ทุก คุณสมบัติเป็นตัวเลข การทดสอบก็จะผ่าน มิฉะนั้นเราจะทำการเรียกซ้ำเพื่อตรวจสอบว่าทุกคุณสมบัติเป็น NestedObj หรือไม่
สิ่งนี้ให้พฤติกรรมที่สอดคล้องกับการทดสอบข้างต้น:
console.log(isNestedObj({})) // true
console.log(isNestedObj({ a: 0, b: 1, c: 2 })) // true
console.log(isNestedObj({ a: {}, b: {}, c: {} })) // true
console.log(isNestedObj({ a: { d: 0 }, b: { e: 1 }, c: { f: {} } })) // true
console.log(isNestedObj({ a: 0, b: 1, c: "abc" })) // false
console.log(isNestedObj({ a: 0, b: 1, c: {} })) // false
console.log(isNestedObj({ a: { d: 0 }, b: { e: 1 }, c: { f: { g: "abc" } } })); // false
โปรดทราบว่าฟังก์ชันข้างต้นจะไม่สามารถทำงานได้หากวัตถุที่ส่งเข้ามาเป็นแบบวงจรจริง เช่น:
const o: NestedObj = { a: {} };
o.a = o;
console.log(isNestedObj(o)) // 💥 TOO MUCH RECURSION;
ถ้าคุณต้องคำนึงถึงเรื่องนี้ คุณจะต้องทำให้ isNestedObj() ซับซ้อนขึ้นเพื่อให้ติดตามวัตถุที่มันเคยเห็นแล้ว ดังนั้นมันจะสามารถออกได้เร็วถ้ามันเข้าสู่ลูป แต่ฉันจะไม่ทำแบบนั้นที่นี่
โปรดทราบว่าหากคุณพยายามบังคับใช้กฎ "มีเพียงหนึ่งคีย์เท่านั้น" ในฟังก์ชัน type guard ในขณะที่ยังคงชนิดข้อมูลเดิมอยู่ คุณอาจพบพฤติกรรมที่แปลกได้:
const isNestedObj = (obj: any): obj is NestedObj => {
if (!obj) return false;
if (typeof obj !== "object") return false;
const values = Object.values(obj);
if (values.length !== 1) return false; // exactly one key
if (values.every(v => typeof v === "number")) return true;
return values.every(isNestedObj);
}
const o = Math.random() < 0.99 ? { a: 1, b: 2 } : "abc";
if (!isNestedObj(o)) {
o // "abc"
console.log(o.toUpperCase()) // 99% chance of a runtime error
}
ค่าของ {a: 1, b: 2} เป็น NestedObj ที่ถูกต้องสมบูรณ์ตามประเภท แต่ isNestedObject({a: 1, b: 2}) ตอนนี้จะคืนค่า false ถ้า isNestedObject เป็นฟังก์ชัน type guard แล้ว TypeScript จะเข้าใจว่านั่นหมายความว่า !isNestedObject(o) บ่งบอกว่า o ไม่ใช่ NestedObj และดังนั้นในตัวอย่างข้างต้น TypeScript จะสรุปผิดพลาดว่าเมื่อ o ต้องเป็น "abc".
มีวิธีที่คุณสามารถลองใช้เพื่อแก้ไขสถานการณ์นี้ได้โดยการทำให้ฟังก์ชัน type guard เป็น "ด้านเดียว" ตามที่อธิบายไว้ใน microsoft/TypeScript#15048 หรือคุณอาจลองต่อสู้กับระบบ type เพื่อสร้าง type ที่ยอมรับเฉพาะวัตถุที่มีหนึ่ง property เท่านั้น ตามที่พูดถึงใน typescript restrict count of object's properties แต่สิ่งนั้นจะทำให้ซับซ้อนมากขึ้นและฉันจะถือว่าอยู่นอกขอบเขตที่นี่
ลิงก์ Playground ไปยังโค้ด
คำตอบ #2
คุณสามารถค้นหาวัตถุโดยการวนผ่านคีย์ของวัตถุได้:
type NestedObj = {[key: string]: NestedObj} | {[key: string]: number}
const isNestedObj = (obj: any): obj is NestedObj => {
if (obj === null || obj === undefined) {
return false;
}
return Object.keys(obj).find(k => typeof obj[k] === "object") !== undefined;
}
function test(obj: any) {
if (isNestedObj(obj)) {
type t = typeof obj['something']; // NestedObj | number
} else {
type t = typeof obj['something']; // any
}
}
บางทีอาจจะดีกว่าถ้าจะหาว่าเมื่อไหร่ที่ไม่ได้ซ้อนกัน:
type NotnestedObj = {[key: string]: number};
type NestedObj = {[key: string]: NestedObj} | NotnestedObj;
const isNotNestedObj = (obj: any): obj is NotnestedObj => {
if (obj === null || obj === undefined) {
return false;
}
const keys = Object.keys(obj);
const allKeysAreNumbers = keys.filter(k => typeof obj[k] === "number").length === keys.length;
return allKeysAreNumbers;
}
function test(obj: any) {
if (isNotNestedObj(obj)) {
type t = typeof obj['something']; // number
} else {
type t = typeof obj['something']; // any
}
}
