TypeScript union types - เกิดข้อผิดพลาดเฉพาะเมื่อเรียกใช้ฟังก์ชัน
ฉันพบพฤติกรรมแปลก ๆ เมื่อใช้ประเภทยูเนียนใน TypeScript เมื่อส่งการอ้างอิงของฟังก์ชันโดยไม่เรียกใช้ จะไม่มีข้อผิดพลาดเกี่ยวกับประเภท:
type A = boolean;
type B = string;
type C = number;
type BC = B | C;
interface Props { onChange(expr: A | BC): void }
const SomeFunction = (_props: Props) => {}
const handler = (el: A | B) => { console.log(el) }
SomeFunction({onChange: handler}) // no error here
SomeFunction({onChange: e => handler(e)}) // <- TS Error: Argument of type 'boolean | BC' is not assignable to parameter of type 'string | boolean'.
คำอธิบายเดียวที่ฉันสามารถหาได้คือพฤติกรรมที่ดีขึ้นในการเรียกประเภทยูเนียนใน TypeScript 3.3
ฉันสงสัยเกี่ยวกับเหตุผลว่าทำไมจึงไม่มีข้อผิดพลาดเมื่อเรียก SomeFunction({onChange: handler}) - เป็นเพราะมีการทับซ้อนกันระหว่างประเภทยูเนียนหรือไม่?
ลิงก์ไปยัง TypeScript Playground
คำตอบ #1
ความแตกต่างคือ handler เป็นฟังก์ชันที่ไม่ใช่เมธอด ขณะที่ onChange ถูกประกาศใน Props ให้เป็นเมธอด และเมื่อคุณเปิดใช้งานตัวเลือกคอมไพเลอร์ --strictFunctionTypes (ซึ่งเป็นส่วนหนึ่งของชุดฟังก์ชันการทำงานของคอมไพเลอร์ --strict) พารามิเตอร์ของฟังก์ชันที่ไม่ใช่เมธอดจะถูกตรวจสอบอย่างเข้มงวดมากขึ้น
เพื่อให้ปลอดภัยทางชนิดข้อมูล พารามิเตอร์ของฟังก์ชันควรถูกตรวจสอบแบบ contravariantly ซึ่งได้อธิบายไว้ในคำถาม/คำตอบนี้ แต่สรุปสั้น ๆ คือ พารามิเตอร์ของฟังก์ชันสามารถ จำกัดชนิด ได้อย่างปลอดภัย แต่ไม่สามารถ ขยายชนิด ได้ เนื่องจาก Expr | Primitive มีความกว้างมากกว่า Expr | StringOrBool จึงไม่ปลอดภัยที่จะถือว่า handler เป็น onChange ดังแสดงที่นี่เมื่อต้องการเพิ่มฟังก์ชันบางอย่าง:
const SomeFunction = (_props: Props) => { _props.onChange(3) }
const handler = (el: Expr | StringOrBool) => {
if ((typeof el !== "boolean") && (typeof el !== "object")) {
console.log(el.toUpperCase()); // has to be a string, right?
}
console.log(el)
}
SomeFunction({ onChange: handler }) // runtime error: el.toUpperCase is not a function
ฟังก์ชัน handler สมมติว่าพารามิเตอร์ที่ไม่ใช่บูลีนและไม่ใช่อ็อบเจ็กต์ต้องเป็นสตริง แต่ SomeFunction เรียก _props.onChange() พร้อมกับ เลขจำนวน โอ๊ะ!
ดังนั้น ณ จุดนี้ควรชัดเจนแล้วว่า ถ้าสิ่งที่เราสนใจคือความปลอดภัยของชนิดข้อมูล ควรจะมีข้อผิดพลาดใน ทั้งสอง การเรียกใช้งานของ SomeFunction จริง ๆ หากเราทำการเขียนใหม่ Props ให้ใช้ไวยากรณ์ที่ไม่ใช่เมธอดสำหรับ onChange:
interface Props {
onChange: (expr: Expr | Primitive) => void // call expression, not method
}
ก็จะเห็นข้อผิดพลาดทั้งสอง:
SomeFunction({ onChange: handler }) // error!
SomeFunction({ onChange: e => handler(e) }) // error!
แต่เมธอด (และจริง ๆ แล้วฟังก์ชันทั้งหมดที่ไม่ได้เปิดใช้งาน --strictFunctionTypes) อนุญาตทั้งการจำกัดอย่างปลอดภัย (contravariance) และการขยายอย่างไม่ปลอดภัย (covariance) นั่นคือพารามิเตอร์ของเมธอดจะถูกตรวจสอบแบบ bivariantly ทำไม? ก็ปรากฏว่าแม้ว่าจะไม่ปลอดภัย แต่มันค่อนข้างมีประโยชน์ คุณสามารถอ่านรายการ FAQ นี้หรือเอกสารนี้เพื่อดูรายละเอียด แต่โดยสรุป มันช่วยให้สามารถใช้แนวปฏิบัติการเขียนโค้ดยอดนิยมบางอย่าง (เช่น การจัดการเหตุการณ์) ผู้คน ชอบ ที่จะปฏิบัติต่อ Dog[] ราวกับว่าเป็น Animal[] แม้ว่าทางเทคนิคแล้วจะไม่ปลอดภัย หากเมธอดถูกจัดการอย่างปลอดภัย การมีอยู่ของเมธอด push() ของ Animal[] จะทำให้ไม่มีใครสามารถใช้ Dog[] เป็น Animal[] ดู Q/A นี้เพื่อข้อมูลเพิ่มเติม
เพราะเรามีทั้งรูปแบบไวยากรณ์ของเมธอดและรูปแบบไวยากรณ์ของฟังก์ชันลูกศรให้เราใช้ เราจึงสามารถมีทั้งสองแบบได้ถ้าเราต้องการ; ทุกครั้งที่คุณใส่ใจเรื่องความปลอดภัยของประเภทของพารามิเตอร์ฟังก์ชัน ควรหลีกเลี่ยงการใช้ไวยากรณ์ของเมธอด
ลิงก์ Playground ไปยังโค้ด
คำตอบ #2
วิธีแก้ปัญหาหนึ่งสำหรับเรื่องนี้คือการใช้ยูเนียนเข้มงวด:
type UnionKeys<T> = T extends T ? keyof T : never;
type StrictUnionHelper<T, TAll> = T extends any
? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>>
: never;
type StrictUnion<T> = StrictUnionHelper<T, T>;
ดังนั้นถ้าคุณประกาศประเภท Primitive เป็น type Primitive=StrictUnion<StringOrBool | Num> คุณจะได้รับข้อผิดพลาดที่ถูกต้องสำหรับทั้งสองกรณี ผมคิดว่านี่เป็นวิธีแก้ปัญหาหนึ่งสำหรับปัญหาที่ @jcalz กล่าวถึงในความคิดเห็นว่าการ ประกาศไวยากรณ์เมธอด ไม่รองรับในลักษณะนี้ StrictUnion ก็รองรับไวยากรณ์เมธอดเช่นกัน การดำเนินการนี้ถูกกล่าวถึงที่นี่
