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

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>:

  1. ความแปรผันร่วม - ลูกศรของการสืบทอดชี้ไปใน ทิศทางเดียวกัน สำหรับ 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; // ✅
  1. ความแปรย้อน - ลูกศรของการสืบทอดชี้ไปทาง ตรงข้าม สำหรับ 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; // 🚫
  1. ความไม่ผันแปร - แม้ว่า 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; // 🚫
  1. ความแปรผันสองทิศทาง - หาก 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 ตำแหน่งของตัวแปรประเภทกำหนดความแปรปรวน ตัวอย่างบางประการ:

  1. โคแวเรียนต์ - T ถูกใช้เป็นฟิลด์หรือเป็นชนิดข้อมูลที่ฟังก์ชันส่งคืน
  2. ต่อต้านความแปรผัน - T ถูกใช้เป็นพารามิเตอร์ของลายเซ็น ฟังก์ชัน ภายใต้ strictFunctionTypes
  3. ไม่เปลี่ยนรูป - T ถูกใช้ทั้งในตำแหน่งโคแวเรียนต์และคอนตราแวเรียนต์
  4. แบบไบ-แวเรียนต์ - 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 {}
}

ลิงก์สนามเด็กเล่น

บทความที่เกี่ยวข้อง