ค้นหาเว็บไซต์

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 ก็รองรับไวยากรณ์เมธอดเช่นกัน การดำเนินการนี้ถูกกล่าวถึงที่นี่