ใน 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 ไปยังโค้ด
