在参数名后面加上 ?,它就变成了可选参数。调用者可以不传它,而在函数内部,它的类型会包含 undefined。
TypeScript 会检查实参的个数,所以如果没有 ?,第一次调用就会编译出错:Expected 2 arguments, but got 1.(TS2554)。
可选参数可能是 undefined
由于调用者可以省略它,可选参数在函数内部的类型是 T | undefined。在 strictNullChecks 下,你必须先处理 undefined 的情况,才能把它当作 T 使用。
可选链(?.)和空值合并(??)是这里的常用工具。如果后备值是一个固定值,用默认参数会更简洁。
默认参数值
默认值会让参数对调用者来说变成可选,同时让它在函数内部的类型不含 undefined。类型会根据默认值推断出来,所以通常不需要写类型注解。
调用者看到的签名是 repeat(text: string, times?: number, separator?: string)。默认值遵循 JavaScript 的规则:实参为 undefined 时生效,无论是省略还是显式传入,而实参为 null 时不生效。默认值表达式可以使用前面的参数:function range(start: number, end = start + 10)。
参数顺序规则
| 声明 | 能否编译 | 说明 |
|---|---|---|
(a: number, b?: number) | 能 | 可选参数放在最后 |
(a?: number, b: number) | 不能 | TS1016:A required parameter cannot follow an optional parameter. |
(a = 0, b: number) | 能 | 但调用者必须写成 f(undefined, 5) 才能使用默认值 |
(a: number, ...rest: number[]) | 能 | 剩余参数永远放在最后 |
(a?: number, ...rest: number[]) | 能 | 可选参数可以放在剩余参数之前 |
把带默认值的参数放在必选参数之前是合法的,但用起来很别扭。对调用者来说它的类型变成了 number | undefined,没有人喜欢写 undefined 来占位。如果你需要一个灵活的开头参数,改用选项对象或函数重载。
省略与 undefined
x?: number 和 x: number | undefined 看起来很像,在函数内部的类型也相同。它们对调用者来说不同:前者可以省略,后者必须传入。
如果一个实参是必需的,但允许没有值,就用 | undefined,这样每个调用者都得考虑这个问题。如果省略它是正常的调用方式,就用 ?。(错误信息里确实写的是“1 arguments”:这是 TypeScript 自己的措辞。)
剩余参数
剩余参数 ...name: T[] 会把任意数量的实参收集到一个数组里。它必须是最后一个参数。
把数组展开到固定参数上要更严格。剩余参数接受任何 number[] 的展开,但声明为 (a: number, b: number) 的函数只接受元组的展开,因为 TypeScript 必须知道长度:
function point(x: number, y: number) { return { x, y }; }
const list = [3, 4]; // number[]
point(...list);
// error TS2556: A spread argument must either have a tuple type or be passed to a rest parameter.
const pair = [3, 4] as const; // readonly [3, 4]
point(...pair); // fine
剩余参数也可以是元组类型,从而为每个位置指定类型:...args: [name: string, age?: number]。
选项对象
一旦函数有两三个以上的可选参数,调用者就记不清各个位置了。带默认值的选项对象可以提供有名字、不分顺序的参数。
末尾的 = {} 让整个对象变成可选。没有它的话,fetchData("/a") 会编译出错(Expected 2 arguments, but got 1.,TS2554),而在纯 JavaScript 中,同样的调用会在运行时抛出 TypeError,因为解构需要一个可供读取的对象。
回调类型中的可选参数
在函数类型中,? 的意思是“这个回调的调用者可能不传它”。它的意思并不是“回调可以不接收它”:回调总是可以忽略末尾的参数。所以不要仅仅为了让处理函数少接收几个参数,就把回调参数标记为可选。
// Too loose: every handler must now cope with index being undefined
type Visit = (item: string, index?: number) => void;
// Right: the caller always passes both; handlers may use only item
type VisitStrict = (item: string, index: number) => void;
const log: VisitStrict = (item) => console.log(item);
常见问题
TypeScript 中如何让参数变成可选?
在参数名后面加 ?:function greet(name?: string)。调用者可以不传它,而在函数内部它的类型是 string | undefined,所以使用前要先检查。给参数一个默认值,比如 name = "there",同样会让它变成可选,并且函数内部不再有 undefined。
TypeScript 中可选参数能放在必选参数前面吗?
用 ? 不行:(a?: number, b: number) 会报错 TS1016:“A required parameter cannot follow an optional parameter.”带默认值的参数可以放在前面,但调用者想用默认值时必须显式传入 undefined,所以实际中可选参数和带默认值的参数都放在最后。
x?: number 和 x: number | undefined 有什么区别?
在函数内部,两者的类型都是 number | undefined。区别在调用处:x?: number 的实参可以省略,而 x: number | undefined 的实参必须传入,哪怕值是 undefined。不传就会报错 TS2554。
传入 null 会使用参数的默认值吗?
不会。JavaScript 只在实参为 undefined(省略或显式传入)时才使用默认值。null 是一个值,所以会被保留。况且在 strictNullChecks 下,TypeScript 本来就不允许把 null 传给 number 类型的参数。
TypeScript 中如何把数组作为多个独立参数传入?
展开它:fn(...args)。对于参数个数固定的函数,数组必须是元组类型,比如 [number, number],或者来自 as const;展开一个 number[] 会报错 TS2556,因为它的长度未知。展开到剩余参数(...values: number[])则始终可行。