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

การพิมพ์ - "...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"));

มีตารางเปรียบเทียบที่ดีระหว่างประเภทต่าง ๆ ในเอกสาร