typescript - strictFunctionTypes จำกัดประเภททั่วไป
ปัญหาดูเหมือนจะเฉพาะเจาะจงกับวิธีที่ strictFunctionTypes ส่งผลต่อประเภทคลาสทั่วไป
นี่คือคลาสที่จำลองสิ่งที่เกิดขึ้นได้อย่างใกล้ชิดและไม่สามารถทำให้ง่ายกว่านี้ได้เนื่องจากข้อกำหนด, any ถูกใช้เพื่อกำหนดส่วนที่ไม่จำกัดเพิ่มเติม (สนามเด็กเล่น):
class Foo<T> {
static manyFoo(): Foo<any[] | { [s: string]: any }>;
static manyFoo(): Foo<any[]> {
return ['stub'] as any;
}
barCallback!: (val: T) => void;
constructor() {
// get synchronously from elsewhere
(callback => {
this.barCallback = callback;
})((v: any) => {});
}
baz(callback: ((val: T) => void)): void {}
}
ประเภททั่วไป T ในลายเซ็น barCallback ทำให้เกิดข้อผิดพลาดของประเภท:
(method) Foo<T>.manyFoo(): Foo<any[]>
This overload signature is not compatible with its implementation signature.(2394)
ปัญหาจะปรากฏขึ้นเฉพาะเมื่อ T ถูกใช้เป็นชนิด val ในชนิดฟังก์ชัน barCallback
มันจะหายไปหากทั้ง barCallback หรือ baz ไม่ใช้ T เป็นประเภทพารามิเตอร์:
barCallback!: (val: any) => void | T;
มันจะหายไปถ้าไม่มีการโอเวอร์โหลดเมธอด manyFoo หรือซิกเนเจอร์มีความหลากหลายน้อยกว่า
มันจะไม่ปรากฏถ้า barCallback มีลายเซ็นของเมธอดในคลาส แต่สิ่งนี้จะป้องกันไม่ให้มันถูกกำหนดในภายหลัง:
barCallback!(val: T): void;
ในกรณีนี้ ประเภท val แบบเข้มงวดไม่สำคัญมากและสามารถละทิ้งได้ เนื่องจาก barCallback ไม่สามารถถูกแทนที่ด้วยลายเซ็นของเมธอดในคลาส การรวมอินเทอร์เฟซดูเหมือนจะเป็นวิธีหนึ่งที่จะลดข้อผิดพลาดโดยไม่ต้องทำให้ประเภทหลวมลงไปอีก:
interface Foo<T> {
barCallback(val: T): void;
}
ในกรณีที่คล้ายกัน มีวิธีแก้ไขอื่นที่เป็นไปได้หรือไม่?
ฉันจะขอบคุณถ้ามีคำอธิบายว่าทำไม val: T ในชนิดของฟังก์ชันถึงมีผลต่อชนิดของคลาสในลักษณะนี้
คำตอบ #1
นี่โดยพื้นฐานแล้วเป็นปัญหาเกี่ยวกับความแปรปรวน ดังนั้นก่อนอื่นเรามาทำความเข้าใจเกี่ยวกับความแปรปรวนกันก่อน:
เกี่ยวกับความแปรปรวน
เมื่อมีชนิดทั่วไป Foo<T> และสองชนิดที่เกี่ยวข้องกัน Animal และ Dog extends Animal มีความสัมพันธ์ที่เป็นไปได้สี่แบบระหว่าง Foo<Animal> และ Foo<Dog>:
- ความแปรผันร่วม - ลูกศรของการสืบทอดชี้ไปใน ทิศทางเดียวกัน สำหรับ
Foo<Animal>และFoo<Dog>เช่นเดียวกันกับAnimalและDogดังนั้นFoo<Dog>เป็นประเภทย่อยของFoo<Animal>ซึ่งยังหมายความว่าFoo<Dog>สามารถกำหนดค่าให้กับFoo<Animal>ได้
type CoVariant<T> = () => T
declare var coAnimal: CoVariant<Animal>
declare var coDog: CoVariant<Dog>
coDog = coAnimal; // 🚫
coAnimal = coDog; // ✅
- ความแปรย้อน - ลูกศรของการสืบทอดชี้ไปทาง ตรงข้าม สำหรับ
Foo<Animal>และFoo<Dog>เช่นเดียวกับที่เป็นสำหรับAnimalและDog, ดังนั้นFoo<Animal>จึงเป็นชนิดย่อยของFoo<Dog>จริง ๆ ซึ่งยังหมายความว่าFoo<Animal>สามารถกำหนดค่าให้กับFoo<Dog>
type ContraVariant<T> = (p: T) => void
declare var contraAnimal: ContraVariant<Animal>
declare var contraDog: ContraVariant<Dog>
contraDog = contraAnimal; // ✅
contraAnimal = contraDog; // 🚫
- ความไม่ผันแปร - แม้ว่า
DogและAnimalจะมีความสัมพันธ์กัน แต่Foo<Animal>และFoo<Dog>ไม่มีความสัมพันธ์ใด ๆ ระหว่างกัน ดังนั้นจึงสามารถกำหนดค่าให้กันและกันไม่ได้
type InVariant<T> = (p: T) => T
declare var inAnimal: InVariant<Animal>
declare var inDog: InVariant<Dog>
inDog = inAnimal; // 🚫
inAnimal = inDog; // 🚫
- ความแปรผันสองทิศทาง - หาก
DogและAnimalมีความสัมพันธ์กัน ทั้งFoo<Animal>เป็นชนิดย่อยของFoo<Dog>และFoo<Animal>เป็นชนิดย่อยของFoo<Dog>หมายความว่าทั้งสองชนิดสามารถกำหนดค่าให้แก่กันได้ ในระบบชนิดที่เข้มงวดกว่า นี่จะถือว่าเป็นกรณีผิดปกติ ซึ่งTอาจไม่ได้ถูกใช้งานจริง แต่ใน TypeScript ตำแหน่งพารามิเตอร์ของเมธอดจะถูกพิจารณาเป็นสองทิศทาง
class BiVariant<T> { m(p: T): void {} }
declare var biAnimal: BiVariant<Animal>
declare var biDog: BiVariant<Dog>
biDog = biAnimal; // ✅
biAnimal = biDog; // ✅
ตัวอย่างทั้งหมด - ลิงก์สนามเด็กเล่น
ดังนั้นคำถามคือ การใช้ T ส่งผลต่อความแปรปรวนอย่างไร? ใน TypeScript ตำแหน่งของตัวแปรประเภทกำหนดความแปรปรวน ตัวอย่างบางประการ:
- โคแวเรียนต์ -
Tถูกใช้เป็นฟิลด์หรือเป็นชนิดข้อมูลที่ฟังก์ชันส่งคืน - ต่อต้านความแปรผัน -
Tถูกใช้เป็นพารามิเตอร์ของลายเซ็น ฟังก์ชัน ภายใต้strictFunctionTypes - ไม่เปลี่ยนรูป -
Tถูกใช้ทั้งในตำแหน่งโคแวเรียนต์และคอนตราแวเรียนต์ - แบบไบ-แวเรียนต์ -
Tถูกใช้เป็นพารามิเตอร์ของการกำหนด เมธอด ภายใต้strictFunctionTypesหรือเป็นประเภทพารามิเตอร์ของเมธอดหรือฟังก์ชันหากstrictFunctionTypesปิดอยู่
เหตุผลสำหรับพฤติกรรมที่แตกต่างกันของพารามิเตอร์ของเมธอดและฟังก์ชันใน strictFunctionTypes อธิบายไว้ที่นี่:
การตรวจสอบที่เข้มงวดมากขึ้นจะใช้กับทุกประเภทฟังก์ชัน ยกเว้นฟังก์ชันที่มาจากการประกาศเมธอดหรือคอนสตรัคเตอร์ เมธอดถูกยกเว้นโดยเฉพาะเพื่อให้แน่ใจว่าคลาสและอินเทอร์เฟซเจเนอริกรวมถึง (เช่น Array) ยังคงสอดคล้องในทางโคแวนเรียนต์ ผลกระทบของการตรวจสอบเมธอดอย่างเข้มงวดจะเป็นการเปลี่ยนแปลงที่รุนแรงมากกว่าเนื่องจากจำนวนมากของประเภทเจเนอริกจะกลายเป็นไม่สามารถเปลี่ยนรูปได้ (ถึงอย่างนั้น เราอาจยังคงสำรวจโหมดเข้มงวดนี้ต่อไป)
กลับไปที่คำถาม
ดังนั้นลองมาดูกันว่า การใช้งานของ T มีผลต่อความแปรปรวนของ Foo อย่างไร
barCallback!: (val: T) => void;- ใช้เป็นพารามิเตอร์ในสมาชิกที่เป็นฟังก์ชัน -> ตำแหน่งแบบคอนตราแวเรียนท์baz(callback: ((val: T) => void)): void- ใช้เป็นพารามิเตอร์ในพารามิเตอร์ callback ของฟังก์ชันอื่น นี่เป็นเรื่องที่ซับซ้อนเล็กน้อย, สปอยล์หน่อย, สิ่งนี้จะกลายเป็น covariant ลองพิจารณาตัวอย่างที่เรียบง่ายนี้:
type FunctionWithCallback<T> = (cb: (a: T) => void) => void
// FunctionWithCallback<Dog> can be assigned to FunctionWithCallback<Animal>
let withDogCb: FunctionWithCallback<Dog> = cb=> cb(new Dog());
let aliasDogCbAsAnimalCb: FunctionWithCallback<Animal> = withDogCb; // ✅
aliasDogCbAsAnimalCb(a => a.animal) // the cb here is getting a dog at runtime, which is fine as it will only access animal members
let withAnimalCb: FunctionWithCallback<Animal> = cb => cb(new Animal());
// FunctionWithCallback<Animal> can NOT be assigned to FunctionWithCallback<Dog>
let aliasAnimalCbAsDogCb: FunctionWithCallback<Dog> = withAnimalCb; // 🚫
aliasAnimalCbAsDogCb(d => d.dog) // the cb here is getting an animal at runtime, which is bad, since it is using `Dog` members
ลิงก์สนามเด็กเล่น
ในตัวอย่างแรก คอลแบ็กที่เราส่งให้กับ aliasDogCbAsAnimalCb คาดว่าจะรับ Animal ดังนั้นมันจึงใช้เฉพาะสมาชิกของ Animal เท่านั้น การใช้งาน withDogCb จะสร้าง Dog และส่งไปยังคอลแบ็ก แต่เรื่องนี้ไม่เป็นปัญหา คอลแบ็กจะทำงานตามที่คาดไว้โดยใช้เพียงคุณสมบัติของคลาสฐานที่มันคาดว่าจะมี
ในตัวอย่างที่สอง callback ที่เราส่งไปให้กับ aliasAnimalCbAsDogCb คาดว่าจะได้รับ Dog ดังนั้นมันจึงใช้สมาชิกของ Dog แต่การใช้งาน withAnimalCb จะส่งอินสแตนซ์ของสัตว์ให้กับ callback ซึ่งอาจนำไปสู่ข้อผิดพลาดในเวลารันไทม์เนื่องจาก callback ใช้สมาชิกที่ไม่มีอยู่
ดังนั้นเมื่อพิจารณาว่าปลอดภัยที่จะกำหนด FunctionWithCallback<Dog> ให้กับ FunctionWithCallback<Animal> เพียงเท่านั้น เราก็จะได้ข้อสรุปว่าการใช้งาน T ในลักษณะนี้เป็นตัวกำหนดความร่วมตัวเชิงพ้อง
ข้อสรุป
ดังนั้นเราจึงมี T ที่ใช้งานทั้งในตำแหน่งโคแวเรียนต์และคอนทราแวเรียนต์ใน Foo ซึ่งหมายความว่า Foo เป็นอินแวเรียนต์ใน T ซึ่งหมายความว่า Foo<any[] | { [s: string]: any }> และ Foo<any[]> จริง ๆ เป็นประเภทที่ไม่เกี่ยวข้องกันตามที่ระบบประเภทพิจารณา และในขณะที่โอเวอร์โหลดมีการตรวจสอบที่ยืดหยุ่นกว่า แต่พวกเขาก็ยังคาดหวังว่าประเภทการคืนค่าของโอเวอร์โหลดและการใช้งานจริงจะต้องเกี่ยวข้องกัน (ไม่ว่าจะเป็นค่าการคืนค่าของการใช้งานหรือค่าการคืนค่าของโอเวอร์โหลดต้องเป็นประเภทย่อยของอีกฝ่ายหนึ่ง ตัวอย่าง)
ทำไมการเปลี่ยนแปลงบางอย่างจึงทำให้มันใช้งานได้:
- การปิด
strictFunctionTypesจะทำให้ไซต์barCallbackสำหรับTเป็นแบบสองค่าพหุภาค ดังนั้นFooจะเป็นแบบโคแวนเรียนท์ - การแปลง
barCallbackเป็นเมธอด ทำให้ไซต์สำหรับTเป็น bivariant ดังนั้นFooจะเป็น covariant - การลบ
barCallbackจะลบการใช้งานแบบคอนทราแวเรียนต์และดังนั้นFooจะเป็นโคแวเรียนต์ - การลบ
bazจะลบการใช้งานโควาเรียนต์ของTทำให้Fooเป็นคอนทราแวเรียนต์ในT
วิธีแก้ปัญหาชั่วคราว
คุณสามารถเปิด strictFunctionTypes ไว้และสร้างข้อยกเว้นเพียงสำหรับ callback นี้เพื่อให้มันเป็น bivariant โดยใช้วิธี hack แบบ bivariant (อธิบายไว้ที่นี่สำหรับกรณีการใช้งานที่แคบกว่า แต่หลักการเดียวกันใช้ได้)
type BivariantCallback<C extends (... a: any[]) => any> = { bivarianceHack(...val: Parameters<C>): ReturnType<C> }["bivarianceHack"];
class Foo<T> {
static manyFoo(): Foo<any[] | { [s: string]: any }>;
static manyFoo(): Foo<any[]> {
return ['stub'] as any;
}
barCallback!: BivariantCallback<(val: T) => void>;
constructor() {
// get synchronously from elsewhere
(callback => {
this.barCallback = callback;
})((v: any) => {});
}
baz(callback: ((val: T) => void)): void {}
}
ลิงก์สนามเด็กเล่น
