การพิมพ์ - "...args: never[]" กับ "...args: unknown[]" ใน TypeScript
ฉันเพิ่งเริ่มใช้ TypeScript ที่นี่ ฉันกำลังพยายามกำหนดอินเตอร์เฟซสำหรับ "สิ่งที่มีเมธอด .toString()" โดยที่ .toString() สามารถรับอาร์กิวเมนต์จำนวนเท่าใดก็ได้ และมีชนิดใดก็ได้
โค้ดด้านล่างเป็นตัวอย่างที่เล็กที่สุด และการใช้ ...args: never[] เป็นอาร์กิวเมนต์ให้กับ toString() ในอินเทอร์เฟซดูเหมือนว่าจะทำงานได้ถูกต้อง คำถามของฉันคือ ทำไม; ใครสามารถอธิบายให้ฉันเข้าใจความแตกต่างระหว่าง unknown และ never ในกรณีนี้ได้ไหม?
interface Stringable {
// toString: (...args: any[]) => string; // works but type-unsafe
// toString: (...args: unknown[]) => string; // causes num and object to error
toString: (...args: never[]) => string; // seems to work
}
const num: Stringable = 4;
const word: Stringable = "hello";
const obj: Stringable = { toString: (text: string) => text + "!!" };
console.log(num.toString());
console.log(word.toString());
console.log(obj.toString("world"));
คำตอบ #1
never คือชนิดที่คุณไม่สามารถกำหนดค่าใด ๆ ให้ได้ แต่คุณสามารถกำหนดค่าจากมันได้เสมอ
let x: never = 1; // can't assign annything to
let o: string = x; // can assign from
ลิงก์สนามเด็กเล่น
unknown เป็นสิ่งตรงข้าม มันคือประเภทที่คุณสามารถกำหนดค่าให้เสมอ แต่คุณไม่สามารถกำหนดค่าจากมันได้
let x: unknown = 1; // can assign anything to
let o: string = x; // can't assign from
ลิงก์สนามเด็กเล่น
ในบริบทของลายเซ็นฟังก์ชัน (...args: never[]) => string จะเป็นลายเซ็นที่ไม่สามารถถูกเรียกใช้งานด้วยอาร์กิวเมนต์ใด ๆ ได้ (เนื่องจากเราไม่สามารถกำหนดค่าใด ๆ ให้กับพารามิเตอร์ประเภท never ได้) ผลลัพธ์ของสิ่งนี้คือ เราสามารถกำหนดการทำงานของฟังก์ชันใด ๆ ให้กับลายเซ็นแบบนี้ได้ เพราะถ้าเราใส่อาร์กิวเมนต์ประเภท never เข้าไป เนื่องจาก never เป็นประเภทที่สามารถกำหนดค่าได้ ค่านั้นของ never จึงสามารถกำหนดค่าให้กับพารามิเตอร์นั้นได้
interface Stringable {
toString: (...args: never[]) => string;
}
// Since this is not a error
const obj: Stringable = { toString: (text: string) => text.toUpperCase() + "!!" };
// This has to be a TS error
console.log(obj.toString(1));
ลิงก์สนามเด็กเล่น
ผลที่ไม่พึงประสงค์คือแม้ว่าเราจะไม่สามารถส่งอาร์กิวเมนต์เข้าไปได้ แต่เรายังคงสามารถเรียกใช้ฟังก์ชันโดยไม่มีอาร์กิวเมนต์ได้ นี่ดูเหมือนจะเป็นความไม่มั่นคงในระบบชนิดข้อมูล
interface Stringable {
toString: (...args: never[]) => string;
}
const obj: Stringable = { toString: (text: string) => text.toLowerCase() + "!!" };
console.log(obj.toString("X")); // ts error
console.log(obj.toString()); // callable, will throw a runtime error
ลิงก์สนามเด็กเล่น
(...args: unknown[]) => string เป็นลายเซ็นของฟังก์ชันที่สามารถรับอาร์กิวเมนต์จำนวนเท่าใดก็ได้ โดยมีค่าของอาร์กิวเมนต์ใดก็ได้ ซึ่งหมายความว่าการใช้งานจริงจะต้องสามารถจัดการกับค่าที่ส่งเข้ามาได้ทุกค่า ซึ่งหมายความว่าเราสามารถกำหนดให้ฟังก์ชันที่ไม่จัดการกับอาร์กิวเมนต์ของมันเลย (เช่น String.toString(): string) แต่เราไม่สามารถกำหนดให้ฟังก์ชันที่ต้องการชนิดเฉพาะสำหรับอาร์กิวเมนต์ใด ๆ ของมันได้ (เช่น Number.toString(radix?: number | undefined): string หรือ obj.toString: (text: string) => string)
โดยสัญชาตญาณเราเห็นว่าเราต้องการข้อผิดพลาด:
interface Stringable {
toString: (...args: unknown[]) => string;
}
// IF this is not an error
const obj: Stringable = { toString: (text: string) => text.toUpperCase() + "!!" };
// this is a runtime error
console.log(obj.toString(1));
ลิงก์สนามเด็กเล่น
ในหลักการแล้วเราไม่สามารถสร้างลายเซ็นของฟังก์ชันที่สามารถรองรับอย่างปลอดภัยกับลายเซ็นต่อไปนี้ได้ทั้งหมด: Number.toString(radix?: number | undefined): string, obj.toString: (text: string) => string, String.toString(): string เราไม่สามารถทำเช่นนี้ได้เพราะว่า Number.toString และ obj.toString ต้องการประเภทของอาร์กิวเมนต์แรกที่ขัดแย้งกัน วิธีเดียวที่ปลอดภัยในการเรียกทั้งสองคือการใช้อาร์กิวเมนต์ที่ตอบสนองได้ทั้งสองลายเซ็น ซึ่งจะต้องเป็น string และ number ซึ่งก็คือ string & number ซึ่งเท่ากับ never.
วิธี toString เดียวที่เราสามารถแทนค่าหนึ่งโดยไม่มีพารามิเตอร์ได้อย่างปลอดภัย:
interface Stringable {
toString: () => string;
}
const num: Stringable = 4;
const word: Stringable = "hello";
const obj: Stringable = { toString: (text?: string) => text + "!!" };
console.log(num.toString());
console.log(word.toString());
console.log(obj.toString());
ลิงก์สนามเด็กเล่น
คำตอบ #2
unknown โดยพื้นฐานแล้วมารยาทดี (ปลอดภัยกว่า) any คุณใช้มันเมื่อชนิดของข้อมูลไม่ทราบ
never ในทางกลับกันคือสิ่งที่ไม่เกิดขึ้นเลย - ฟังก์ชันที่ไม่มีค่าที่คืนกลับจะคืนค่า never ฟังก์ชันที่มี never[] สำหรับอาร์กิวเมนต์แบบกระจายจะไม่รับพารามิเตอร์ใด ๆ
เกิดข้อผิดพลาดเมื่อคุณพยายามเรียกฟังก์ชัน ในตัวอย่างของคุณ:
interface Stringable {
toString: (...args: never[]) => string;
}
const obj: Stringable = { toString: (text: string) => text + "!!" };
console.log(obj.toString("world")); // -> Argument of type 'string' is not assignable to parameter of type 'never'.
สำหรับกรณีการใช้งานของคุณ ฉันคิดว่าการใช้ประเภททั่วไปเป็นสิ่งที่ดีที่สุด:
interface Stringable<T> {
toString: (...args: T[]) => string;
}
const obj: Stringable<string> = { toString: (text) => text + "!!" };
console.log(obj.toString("world"));
มีตารางเปรียบเทียบที่ดีระหว่างประเภทต่าง ๆ ในเอกสาร
