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

ใน TypeScript มีความแตกต่างระหว่างพารามิเตอร์ที่เป็นทางเลือกกับพารามิเตอร์ที่สามารถเป็น undefined หรือไม่?


ฉันสงสัยว่ามีความแตกต่างระหว่างโค้ดสองส่วนนี้หรือไม่:

function sayHello(name?: string) {
  if (name) { return 'Hello ' + name; }
  return 'Hello!';
}

และ

function sayHello(name: string | undefined) {
  if (name) { return 'Hello ' + name; }
  return 'Hello!';
}

(ฉันรู้ว่าฉันไม่สามารถใส่อาร์กิวเมนต์ที่สองที่ไม่ใช่ตัวเลือกหลัง 'name' ได้เพราะมันต้องเป็นอาร์กิวเมนต์ตัวสุดท้ายหรือหนึ่งในตัวสุดท้าย)

ฉันกำลังคิดเกี่ยวกับเรื่องนี้เมื่อตอนเช้านี้ และฉันรู้สึกว่าความแตกต่างสำคัญสำหรับฉันก่อนอื่นคือสิ่งที่คุณกำลังพูดถึงผู้บริโภคของฟังก์ชัน

อันแรกสื่อถึงความเป็นตัวเลือกมากกว่า เช่น 'คุณไม่จำเป็นต้องส่งอันนี้ให้ฉัน แต่คุณสามารถส่งได้ถ้าต้องการ' อันที่สองจะบอกว่า 'ส่งสตริงให้ฉัน ฉันไม่สนใจว่ามันจะ undefined หรือไม่ ฉันจัดการได้'

สิ่งที่คล้ายกันสามารถเกิดขึ้นได้กับอินเทอร์เฟซและชนิดข้อมูลเช่นกัน

interface Foo {
   thing?: string;
}

ต่อสู้กับ

interface Foo {
   thing: string | undefined;
}

ฉันกำลังไปถูกทางอยู่หรือเปล่า? มีอะไรอีกที่ฉันควรรู้ไหม?

คำตอบ #1

คุณกำลังอยู่บนทางที่ถูกต้องและเกือบจะถูกต้องแล้ว


ในสิ่งต่อไปนี้ ผมจะสมมติว่าคุณกำลังใช้ตัวเลือกคอมไพเลอร์ --strict หรืออย่างน้อย --strictNullChecks ดังนั้น undefined และ null จะไม่ถูกอนุญาตโดยปริยายเสมอไป:

let oops: string = undefined; // error! 
// Type 'undefined' is not assignable to type 'string'

ใน TypeScript พารามิเตอร์ของฟังก์ชัน/เมธอดหรือฟิลด์ของชนิดวัตถุที่ถูกกำหนดให้เป็น ตัวเลือก ด้วยตัวแก้ไข ? หมายความว่าสามารถ ไม่ปรากฏ ได้:

function opt(x?: string) { }

interface Opt {
    x?: string;
}

const optObj: Opt = {}; // okay
opt(); // okay

แต่พารามิเตอร์/ฟิลด์ตัวเลือกดังกล่าวก็อนุญาตให้ มีอยู่แต่ undefined ได้เช่นกัน:

const optObj2: Opt = { x: undefined } // okay
opt(undefined); // okay

ในความเป็นจริง หากคุณใช้ IntelliSense เพื่อตรวจสอบประเภทของพารามิเตอร์/ฟิลด์ที่เป็นตัวเลือกเหล่านี้ คุณจะเห็นว่าคอมไพเลอร์เพิ่ม undefined เป็นความเป็นไปได้โดยอัตโนมัติ:

function opt(x?: string) { }
// function opt(x?: string | undefined): void

interface Opt {
    x?: string;
}
type AlsoOpt = Pick<Opt, "x">;
/* type AlsoOpt = {
    x?: string | undefined;
} */

จากมุมมองของ ผู้ดำเนินการ ของฟังก์ชันหรือ ผู้ใช้ ของประเภทวัตถุ องค์ประกอบที่เป็นทางเลือกสามารถถูกพิจารณาเหมือนว่ามันมีอยู่เสมอ แต่บางทีอาจเป็น undefined:

function opt(x?: string) {
    // (parameter) x: string | undefined
    console.log(typeof x !== "undefined" ? x.toUpperCase() : "undefined");
}

function takeOpt(v: Opt) {
    const x = v.x;
    // const x: string | undefined
    console.log(typeof x !== "undefined" ? x.toUpperCase() : "undefined");
}

เปรียบเทียบและเปรียบต่างสิ่งนี้กับฟิลด์หรือพารามิเตอร์ที่ต้องมี (ไม่ใช่ตัวเลือก) ซึ่งรวมถึง | undefined:

function req(x: string | undefined) { }

interface Req {
    x: string | undefined
}

เช่นเดียวกับเวอร์ชันที่เป็นทางเลือก ตัวที่จำเป็นพร้อมกับ | undefined จะยอมรับ undefined แบบชัดเจน แต่ต่างจากเวอร์ชันที่เป็นทางเลือก ตัวที่จำเป็นไม่สามารถเรียกหรือต้องสร้างโดยไม่ใส่ค่าได้เลย:

req(); // error, Expected 1 arguments, but got 0!
req(undefined); // okay
const reqObj: Req = {}; // error, property x is missing!
const reqObj2: Req = { x: undefined } // okay

และ เช่นเดียวกับเวอร์ชันที่เป็นทางเลือก ผู้ดำเนินการฟังก์ชันหรือผู้ใช้ประเภทวัตถุจะเห็นสิ่งที่เป็นทางเลือกว่าแน่นอนว่ามีอยู่แต่เป็นไปได้ว่าอาจเป็น undefined:

function req(x: string | undefined) {
    // (parameter) x: string | undefined
    console.log(typeof x !== "undefined" ? x.toUpperCase() : "undefined");
}

function takeReq(v: Req) {
    const x = v.x;
    // const x: string | undefined
    console.log(typeof x !== "undefined" ? x.toUpperCase() : "undefined");
}

สิ่งอื่นที่ต้องสังเกต:


นอกจากนี้ยังมีองค์ประกอบที่เป็นทางเลือกในชนิดทูเพิลซึ่งทำงานในลักษณะเดียวกัน พวกมันเหมือนกับฟิลด์ของออบเจ็กต์ที่เป็นทางเลือก แต่มีข้อจำกัดเดียวกับพารามิเตอร์: หากมีองค์ประกอบทูเพิลใดเป็นทางเลือก องค์ประกอบทั้งหมดที่ตามหลังต้องเป็นทางเลือกด้วยเช่นกัน:

type OptTuple = [string, number?];
const oT: OptTuple = ["a"]; // okay
const oT2: OptTuple = ["a", undefined]; // okay

type ReqTuple = [string, number | undefined];
const rT: ReqTuple = ["a"]; // error! Source has 1 element(s) but target requires 2
const rT2: ReqTuple = ["a", undefined]; // okay

สำหรับพารามิเตอร์ฟังก์ชัน คุณบางครั้งสามารถใช้ประเภท void เพื่อหมายถึง "ขาดหาย" และดังนั้น | void เพื่อหมายถึง "ไม่บังคับ" ตามที่ได้ดำเนินการใน microsoft/TypeScript#27522 ดังนั้น x?: string และ x: string | void จะถูกพิจารณาในลักษณะคล้ายกัน:

function orVoid(x: string | void) {
    console.log((typeof x !== "undefined" ? x.toUpperCase() : "undefined"));
}
orVoid(); // okay

นี่ยังไม่เป็นกรณีสำหรับฟิลด์ของอ็อบเจ็กต์ มันได้ถูกนำไปใช้ใน microsoft/TypeScript#40823 แล้ว แต่ยังไม่ได้ถูกเพิ่มเข้าไปในภาษา (และฉันไม่แน่ใจว่ามันจะเคยเป็นเช่นนั้นหรือไม่):

interface OrVoid {
    x: string | void;
}
const o: OrVoid = {} // error! x is missing

สุดท้าย ผมจะแนะนำให้คุณดูที่ microsoft/TypeScript#13195 ซึ่งเป็นประเด็นที่พูดถึงความสัมพันธ์ทางประวัติศาสตร์ที่ตลกใน TypeScript ระหว่าง “ขาดหายไป” กับ “มีอยู่แต่ undefined” บางครั้งพวกมันถูกปฏิบัติแบบเดียวกัน แต่บางครั้งก็มีการแยกแยะ

หนึ่งในเรื่องใหญ่ที่ผู้คนต้องการความแตกต่างมากขึ้นคือ เมื่อบางสิ่งเป็น ตัวเลือก นักพัฒนาไม่ค่อยพอใจกับความเป็นไปได้ในการ ส่งค่า undefined โดยตรง กล่าวคือ ผู้คนต้องการที่จะบอกว่า interface Opt {x?: string} ควรหมายความว่า x เป็น string หรือหายไปทั้งหมด พวกเขาคิดว่าถ้าคุณมีค่าตัวแปร o ประเภท Obj ดังนั้น o.x === undefined ควรเกิดขึ้นเฉพาะเมื่อ "x" in o เป็น false

แต่นั่นไม่ใช่สิ่งที่เกิดขึ้นตามค่าเริ่มต้นใน TypeScript ในการตั้งค่าคอมไพเลอร์ส่วนใหญ่ o.x === undefined จะไม่บอกคุณว่าคุณสมบัตินั้นมีอยู่แต่เป็น undefined หรือว่าจะหายไป และดังนั้น "x" ใน o จึงไม่สามารถบอกได้อย่างชัดเจนว่า o.x เป็น string หรือ undefined

มีแฟลกคอมไพเลอร์ใหม่ --exactOptionalPropertyTypes ที่มีกำหนดจะออกพร้อมกับ TypeScript 4.4 ซึ่งเมื่อเปิดใช้งาน จะไม่เพิ่ม undefined เข้าไปในโดเมนของคุณสมบัติที่เป็นทางเลือก ดังนั้น o.x === undefined จะบ่งบอกว่าคุณสมบัติ x หายไป ตัวเลือกคอมไพเลอร์นี้จะไม่ถูกเปิดใช้งานโดยค่าเริ่มต้น และจะใช้กับคุณสมบัติเท่านั้น ไม่ใช่พารามิเตอร์ของฟังก์ชัน

ในทุกกรณี ฉันขอแนะนำให้หลีกเลี่ยงสถานการณ์ที่ความแตกต่างระหว่างการขาดและ undefined มีความสำคัญ


ลิงก์ Playground ไปยังโค้ด

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