在TypeScript中,可选参数和可以为undefined的参数有什么区别吗?

8
我想知道这两段代码有什么不同:

我想知道这两段代码有什么不同:

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

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

我知道在'name'之后,不能再放置一个非可选参数,因为它必须是最后一个或者是最后几个中的一个。

今天早些时候我思考过这个问题,我觉得对我而言最重要的区别在于你所说的函数的消费者。

第一个更多地暗示着可选性,像是“你不需要传递给我这个,但如果你想的话可以传递” 第二个则表示“传递给我一个字符串,我不关心它是否未定义,我可以处理它”。

类似的情况也会在接口和类型中出现。

interface Foo {
   thing?: string;
}

vs

interface Foo {
   thing: string | undefined;
}

我在这条路上走对了吗?还有其他我需要知道的事情吗?


它适用于对象(以及符合接口的对象),但也许不适用于方法的内容? - Sam Jarman
1个回答

14

您走在正确的轨道上,基本上是正确的。


接下来,我假设您正在使用--strict或至少--strictNullChecks编译器选项,以便undefinednull不总是隐式允许的。
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

但是这样的可选参数/字段也可以被 存在但未定义

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;
} */

从函数的实现者或对象类型的消费者的角度来看,可选元素可以被视为始终存在,但可能是未定义的:

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?: stringx: 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,要么完全不存在。他们认为,如果您有一个类型为Obj的值o,那么只有在"x" in ofalse时,o.x === undefined才会发生。
但这不是默认情况下TypeScript中发生的。在大多数编译器配置中,o.x === undefined不能让你知道属性是存在但是undefined还是不存在。因此,"x" in o无法确定o.x是一个string还是undefined
在TypeScript 4.4中将发布一个新的--exactOptionalPropertyTypes编译器标志,当启用时,将不会将undefined添加到可选属性的域中,因此o.x === undefined意味着x属性不存在。但是这个编译器选项不会默认启用,而且它仅适用于属性而不是函数参数。
无论如何,我建议避免出现缺失和undefined之间的差异很重要的情况。

玩耍链接到代码


你可能想要更新你的回答:https://github.com/microsoft/TypeScript/pull/43947 - Inigo
答案已经有一个部分,开头是“有一个新的--exactOptionalPropertyTypes编译器标志...”,你是指其他的吗? - jcalz
1
在如此长的答案中很容易被忽略,特别是当它悄悄地溜过一个与此无关的“finally”时。 - Inigo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接