ฟังก์ชันประเภทยูเนียนของ TypeScript
ฉันกำลังพยายามสร้างประเภทยูเนียนสำหรับฟังก์ชันลูกศร เป้าหมายคือสามารถอนุมานประเภทของอาร์กิวเมนต์ตัวที่สองโดยขึ้นอยู่กับตัวแรก
ในอุดมคติฉันต้องการให้ y เป็นประเภท number | string และ z เป็น 'first' | 'second' แต่หลังจากจำกัดประเภทของ y หรือ z ให้สามารถคาดเดาประเภทที่จำกัดของพารามิเตอร์อีกตัวโดยอัตโนมัติ
น่าเสียดายที่ TypeScript ดูเหมือนไม่สามารถจัดการกับกรณีซับซ้อนแบบนี้ได้ แต่ฉันสงสัยว่าพวกคุณเคยเจอปัญหาคล้าย ๆ กันบ้างหรือไม่
ในสถานการณ์ที่เรียบง่าย โค้ดของฉันมีลักษณะดังนี้:
type Callback<T1, T2> = (y: T1, z: T2) => void;
const test = (x: Callback<number, 'first'> | Callback<string, 'second'>) => {
return;
}
// Parameter 'y' implicitly has an 'any' type.
// Parameter 'z' implicitly has an 'any' type.
test((y, z) => {
if(typeof y === 'number') {
// y is a number
// so z must be 'first'
} else {
// y is a string
// so z must be 'second'
}
});
ขอบคุณ!
คำตอบ #1
นี่คือสิ่งที่กำลังเกิดขึ้นตามที่ผมเห็น ลองใช้คำนิยามเหล่านี้:
type Callback<T1, T2> = (y: T1, z: T2) => void;
type First = Callback<number, 'first'>;
type Second = Callback<string, 'second'>;
ก่อนอื่น ผมแน่ใจเลยว่าผมสงสัยว่าคุณต้องการสหภาพของฟังก์ชันมากกว่าการตัดกันของฟังก์ชัน สังเกตว่าการสหภาพของฟังก์ชันเช่นนี้แทบจะไร้ประโยชน์:
const unionTest = (x: First | Second) => {
// x is *either* a First *or* it is a Second,
// *but we don't know which one*. So how can we ever call it?
x(1, "first"); // error!
// Argument of type '1' is not assignable to parameter of type 'never'.
x("2", "second"); // error!
// Argument of type '"2"' is not assignable to parameter of type 'never'.
}
ฟังก์ชัน unionTest() เหมือนกับ test() ของคุณ แต่ไม่สามารถทำอะไรกับ x ได้ เพราะ x เป็นเพียง First หรือ Second หากคุณพยายามเรียกใช้ จะเกิดข้อผิดพลาดไม่ว่าจะอย่างไร union ของฟังก์ชันสามารถทำงานได้อย่างปลอดภัยเพียงกับ intersection ของพารามิเตอร์ของพวกมัน เทคโนโลยีบางส่วนสำหรับสิ่งนี้ถูกเพิ่มเข้ามาใน TS3.3 แต่ในกรณีนี้ประเภทของพารามิเตอร์เป็นแบบที่ไม่สามารถเกิดขึ้นพร้อมกันได้ ดังนั้นพารามิเตอร์ที่ยอมรับได้มีเพียงประเภท never ... ดังนั้น x จึงไม่สามารถเรียกใช้ได้
ฉันสงสัยว่าการรวมฟังก์ชันที่ไม่สามารถเข้ากันได้แบบนี้เคยเป็นสิ่งที่ใครต้องการจริง ๆ หรือไม่ ความเป็นคู่ของการรวมและการตัดกัน และความตรงข้ามของประเภทฟังก์ชันเมื่อเทียบกับประเภทของพารามิเตอร์นั้นทำให้สับสนและยากที่จะพูดถึง แต่ความแตกต่างนี้สำคัญ ดังนั้นฉันคิดว่าคุ้มค่าที่จะอธิบายจุดนี้ให้ชัดเจน การรวมนี้เหมือนกับการพบว่าฉันต้องจัดประชุมกับใครบางคนที่ อาจว่างในวันจันทร์ หรือจะว่างในวันอังคาร แต่ฉันไม่รู้ว่าเป็นวันไหน ฉันคงจะคิดว่าถ้าฉันสามารถมีประชุมทั้งในวันจันทร์ และวันอังคารได้นั่นจะใช้ได้ แต่สมมติว่าสิ่งนั้นไม่มีเหตุผล ฉันจึงติดอยู่ คนที่ฉันจะพบคือการรวม และวันที่ฉันจะพบคือการตัดกัน ทำไม่ได้
แทนที่จะเป็นเช่นนั้น สิ่งที่ฉันคิดว่าคุณต้องการคือ สัญจรตัดกัน ของฟังก์ชัน ซึ่งนี่คือสิ่งที่สอดคล้องกับฟังก์ชันที่มีการโอเวอร์โหลด; คุณสามารถเรียกใช้ได้ทั้งสองวิธี มันจะมีลักษณะดังนี้:
const intersectionTest = (x: First & Second) => {
// x is *both* a First *and* a Second, so we can call it either way:
x(1, "first"); // okay!
x("2", "second"); // okay!
// but not in an illegal way:
x(1, "second"); // error, as desired
x("2", "first"); // error, as desired
}
ตอนนี้เรารู้ว่า x เป็นทั้ง First และ Second คุณสามารถเห็นได้ว่าคุณสามารถจัดการมันเหมือนกับ First หรือเหมือนกับ Second และจะไม่มีปัญหา แต่อย่าพยายามจัดการมันเหมือนกับไฮบริดแปลก ๆ อย่าง x(1, "second") เพราะนั่นอาจไม่ใช่สิ่งที่คุณต้องการ ตอนนี้ฉันกำลังนัดประชุมกับคนที่สามารถเข้าร่วมได้ทั้งวันจันทร์ และ วันอังคาร ถ้าฉันถามคนคนนั้นว่านัดประชุมวันไหนดี เขาอาจจะตอบว่า "วันจันทร์หรือวันอังคารก็ได้สำหรับฉัน" วันที่ประชุมเป็นการรวมตัว และคนที่ฉันจะประชุมด้วยเป็นการตัดกัน เรื่องนี้ใช้ได้
ดังนั้นตอนนี้ผมสมมติว่าคุณกำลังจัดการกับการตัดกันของฟังก์ชัน อย่างไรก็ตาม คอมไพเลอร์จะไม่สร้างชนิดพารามิเตอร์รวมให้คุณโดยอัตโนมัติ และคุณก็ยังจะเจอข้อผิดพลาด “implicit any” นั้นอยู่ดี
// unfortunately we still have the implicitAny problem:
intersectionTest((x, y) => { }) // error! x, y implicitly any
คุณสามารถ แปลงด้วยตนเอง การตัดกันของฟังก์ชันให้เป็นฟังก์ชันเดียวที่ทำงานกับสหภาพของประเภทพารามิเตอร์ แต่กับสองพารามิเตอร์ที่ถูกจำกัด วิธีเดียวที่จะแสดงสิ่งนี้คือด้วยอาร์กิวเมนต์ที่เหลือและทูเพิลที่เหลือ นี่คือวิธีที่เราสามารถทำได้:
const equivalentToIntersectionTest = (
x: (...[y, z]: Parameters<First> | Parameters<Second>) => void
) => {
// x is *both* a First *and* a Second, so we can call it either way:
x(1, "first"); // okay!
x("2", "second"); // okay!
// but not in an illegal way:
x(1, "second"); // error, as desired
x("2", "first"); // error, as desired
}
นั่นเหมือนกับ intersectionTest() ในแง่ของพฤติกรรม แต่ตอนนี้พารามิเตอร์มีชนิดที่ทราบแล้วและสามารถกำหนดชนิดตามบริบทให้ดีกว่า any:
equivalentToIntersectionTest((y, z) => {
// y is string | number
// z is 'first' | 'second'
// relationship gone
if (z === 'first') {
y.toFixed(); // error!
}
})
น่าเสียดาย ตามที่คุณเห็นด้านบน หากคุณใช้งาน callback นั้นด้วย (y, z) => {...} ประเภทของ y และ z จะกลายเป็นสหภาพที่แยกออกกัน คอมไพเลอร์จะลืมว่าพวกมันเกี่ยวข้องกันทันที เมื่อคุณพิจารณารายการพารามิเตอร์เป็นพารามิเตอร์แยก คุณจะสูญเสียความสัมพันธ์นั้น ผมเคยเห็นคำถามมากพอที่ต้องการวิธีแก้ปัญหานี้จนผมได้ยื่นข้อร้องเรียนเกี่ยวกับมัน แต่ตอนนี้ยังไม่มีการสนับสนุนโดยตรง
ลองดูว่าจะเกิดอะไรขึ้นหากเราไม่แยกรายการพารามิเตอร์ทันที โดยการกระจายพารามิเตอร์ที่เหลือไปยังอาร์เรย์และใช้มันดังนี้:
equivalentToIntersectionTest((...yz) => {
// yz is [number, "first"] | [string, "second"], relationship preserved!
โอเค นั่นดี ตอนนี้ yz ยังคงติดตามข้อจำกัดอยู่
ขั้นตอนต่อไปที่นี่คือการพยายามจำกัด yz ให้เหลือเป็นขาใดขาหนึ่งของสหภาพผ่านการทดสอบ type guard วิธีที่ง่ายที่สุดในการทำเช่นนี้คือถ้า yz เป็น discriminated union และมัน เป็น แต่ไม่ใช่เพราะ y (หรือ yz[0]) number และ string ไม่ใช่ประเภท literal และไม่สามารถใช้โดยตรงเป็น discriminator ได้:
if (typeof yz[0] === "number") {
yz[1]; // *still* 'first' | 'second'.
}
ถ้าคุณต้องตรวจสอบ yz[0] คุณจะต้องสร้างฟังก์ชัน type guard ของคุณเองเพื่อรองรับสิ่งนั้น แทนที่นั้น ผมขอแนะนำให้สลับไปที่ z (หรือ yz[1]) เนื่องจาก "first" และ "second" เป็นค่า string literal ที่สามารถใช้ในการแยกประเภท union ได้:
if (yz[1] === 'first') {
// you can only destructure into y and z *after* the test
const [y, z] = yz;
y.toFixed(); // okay
z === "first"; // okay
} else {
const [y, z] = yz;
y.toUpperCase(); // okay
z === "second"; // okay
}
});
สังเกตว่า หลังจากที่ yz[1] ถูกเปรียบเทียบกับ 'first' แล้ว ประเภทของ yz ไม่ใช่ union อีกต่อไป ดังนั้นคุณสามารถแยกค่าไปที่ y และ z ในลักษณะที่มีประโยชน์มากขึ้น
โอเค ฟู้วว นั่นเยอะมาก รหัสสรุป:
const test = (
x: (...[y, z]: [number, "first"] | [string, "second"]) => void
) => { }
test((...yz) => {
if (yz[1] === 'first') {
const [y, z] = yz;
y.toFixed();
} else {
const [y, z] = yz;
y.toUpperCase(); // okay
}
});
หวังว่านั่นจะช่วยได้ โชคดีนะ!
ลิงก์ไปยังโค้ด
