红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 前端学习笔记
  3. 正文

TypeScript 学习笔记 3:函数

2026年2月14日 11点热度 0人点赞 0条评论

函数类型表达式

可以用函数类型表达式表示一个函数的类型:

function greeter(fn: (a: string) => void) {
    fn("Hello, World");
}
​
function printToConsole(s: string) {
    console.log(s);
}
​
greeter(printToConsole);

这里的函数类型表达式(a: string) => void表示一个函数,它接收一个string类型名称为a的参数,且没有返回值。

需要注意的是,这里的参数名是必须的,参数类型是可选的,因此(string) => void表示这个函数接收的是名为string且类型为any的参数。

此外,函数表达式也可以使用类型别名:

type GreetFunction = (a: string) => void;
function greeter2(fn: GreetFunction) { 
    fn("Hello, World");
}
greeter2(printToConsole);

调用签名

JavaScript 的函数更接近 Python,可以看作是可以调用的对象,因此它们可以拥有属性。

函数类型表达式只能表示普通的函数,如果要表示一个拥有属性的函数(可调用对象),可以在类型定义中添加调用签名表示:

type RandomIDGenerator = {
    prefix: string;
    (): string;
}
​
function printId(idGenerator: RandomIDGenerator) {
    console.log(idGenerator.prefix + idGenerator());
}
function generateID(): string {
    return Math.random().toString(16).slice(2);
}
​
generateID.prefix = "id-";
printId(generateID)

类型中的(): string是调用签名,表示这个对象被调用时候被执行的方法的类型,与普通的函数类型表达式不同,这里没有=>。

构造签名

在 JavaScript 中,构造函数意味着可以使用new关键字调用类,并返回一个对象。可以用构造签名表示构造函数的类型:

type PersonConstructor = {
    new (firstName: string, lastName: string): Person;
}
​
class Person{
    firstName: string;
    lastName: string;
    fullName(): string {
        return this.firstName + " " + this.lastName;
    }
    constructor(theFirst: string, theLast: string) {
        this.firstName = theFirst;
        this.lastName = theLast;
    }
}
​
function makePerson(ctor: PersonConstructor, firstName: string, lastName: string): Person {
    return new ctor(firstName, lastName);
}
​
let p = makePerson(Person, "Bob", "Smith");
console.log(p.fullName());

对于某些类,既可以通过构造函数的方式调用,也可以直接调用,比如Date,此时可以同时使用调用签名和构造签名定义这类类型:

type DateType = {
    (n?:number): string;
    new (s: string): Date;
}
​
function printDate(date : DateType){
    console.log(date());
    console.log(new date("2015-12-25"));
}
​
printDate(Date);

泛型函数

看一个例子:

function firstElement(array: any[]){
    return array[0];
}
​
const e1 = firstElement([1,2,3]);
const e2 = firstElement(["a","b","c"]);
console.log(e1, e2);
// 这里推断出的类型都是 any

firstElement返回数组的第一个元素,它可以返回任意类型的数组,但缺点是这里 TS 没法根据实际调用时候使用的数组类型推断出确切的返回值类型,因此所有推断出的返回值类型都是any。

可以使用泛型函数解决这个问题:

function firstElement2<Type>(array: Type[]){
    return array[0];
}
​
const e3 = firstElement2([1,2,3]);
const e4 = firstElement2(["a","b","c"]);
console.log(e3, e4);
// e3 推断出的类型是 number|undefined
// e4 推断出的类型是 string|undefined

这里有undefined是因为在 JavaScript 中,如果试图获取一个不存在的数组下标,并不会报错,而是返回undefined:

const e5 = firstElement2([]);
console.log(e5);

推断

和其他编程语言一样,可以指定多个泛型参数:

function map<Input, Output>(array: Input[], func: (arg: Input) => Output): Output[] {
    return array.map(func);
}
​
const parsed = map(["1", "2", "3"], (n) => parseInt(n));
console.log(parsed);

TS 都可以根据实际情况推断出合理的类型。

约束

像 Java 中的泛型一样,TS 的泛型参数也可以划定范围,比如:

function longest<Type extends {length: number}>(a: Type, b: Type) {
    if (a.length >= b.length) {
        return a;
    } else {
        return b;
    }
}
​
const l = longest("abcd", "efg");
console.log(l);
// 这里推断出的类型是 "abcd"|"efg"
const l2 = longest([1,2,3], [4,5]);
console.log(l2);
// 这里推断出的类型是 number[]
// const l3 = longest(new Date(), new Date());
// 报错,Date 对象没有 length 属性

这里通过Type extends {length: number}指定了泛型参数Type必须拥有一个number类型的length属性。

处理受约束的值

function minimumLength<Type extends {length: number}>(
    obj: Type,
    minLength: number
): Type {
    if (obj.length >= minLength) {
        return obj;
    } else {
        return { length: minLength };
        // 报错,不能将 { length: minLength } 赋给 Type
    }
}

上面的代码看上去合乎语法,返回的{ length: minLength }满足Type的约束定义。但是这里的泛型参数还有一个隐含的限制,即参数obj的的类型与返回值的类型是同一种类型。因此上面的代码不成立,对于参数obj,它可能是任意一种拥有length属性的类型,而返回值是固定的{ length: minLength }。

指定类型参数

看一个示例:

function combine<Type>(arr1: Type[], arr2: Type[]){
    return arr1.concat(arr2);
}
​
const arr = combine([1,2,3], [4,5]);
console.log(arr);

下面的调用会报错:

const arr2 = combine<string>([1,2,3], ["d","e"]);
// 报错,不能将类型 "string" 赋给类型 "number"

因为 TS 根据泛型推断出的返回值arr2的类型是number[],显然string不能赋值给number。如果一定要调用,可以这样:

const arr3 = combine<string|number>([1,2,3], ["d","e"]);

将类型参数下推

看一个示例:

function firstElement3<Type>(arr: Type[]) {
    return arr[0];
}
​
const a = firstElement3([1, 2, 3]);
console.log(a);
// 推断出的类型是 number | undefined
​
function firstElement4<Type extends any[]>(arr: Type) {
    return arr[0];
}
​
const a2 = firstElement4([1, 2, 3]);
console.log(a2);
// 推断出的类型是 any

从语法上看,两个函数的泛型定义都是可以说的通的,但实际上第二种定义方式(Type extends any[])并不能准确推断出返回值类型。

因此 TS 有一条泛型参数使用规则:在可能的情况下,使用泛型参数本身而不是限制它。

使用更少的类型参数

function filter<Type>(array: Type[], func: (arg: Type) => boolean): Type[] {
    return array.filter(func);
}
function filter2<Type, Func extends (arg: Type) => boolean>(
    array: Type[],
    func: Func
): Type[] {
    return array.filter(func);
}

这两个函数都是有效的,但filter2的类型注解更繁琐。

TS 中有一条规则:尽可能少使用类型参数。

类型参数应该出现两次

有时候我们会忘记本不应该使用类型参数:

function greet<Str extends string>(s: Str) {
  console.log("Hello, " + s);
}
 
greet("world");

这里的类型参数是多余的,完全可以:

function greet2(s: string) {
  console.log("Hello, " + s);
}
​
greet2("world");

泛型参数本质上是用于说明存在这样一种类型,它既可以用于参数A,也可以用于参数B,或者是用于返回值。换言之,泛型参数的价值在于说明多个类型之间的连系。如果某个泛型参数仅使用了一次,那这个泛型参数很可能是多余的。

因此 TS 中存在一条规则:如果一个类型参数只出现在一个位置,强烈考虑你是否真的需要它。

可选参数

TS 中可以使用?定义可选参数:

function printNumber(num?: number){
    console.log(num);
}
​
printNumber(); // 输出 undefined
printNumber(42);  // 输出 42

如果没有传递参数,接收到的参数是undefined,因此这里num的实际类型是number|undefined。

可以为参数指定默认值:

function printNumber2(num: number = 11){
    console.log(num);
}
printNumber2(15) // 输出 15
printNumber2() // 输出 11
printNumber2(undefined) // 输出 11

可以看到,未传参时会使用默认参数。此外值得注意的是,可以显式传递undefined,其效果和未传参是一样的。

回调函数的可选参数

回调函数中也可以使用可选参数:

function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {
  for (let i = 0; i < arr.length; i++) {
    callback(arr[i], i);
  }
}
myForEach([1, 2, 3], (a) => console.log(a));
myForEach([1, 2, 3], (a, i) => console.log(a, i));
// 1
// 2
// 3
// 1 0
// 2 1
// 3 2

但如果使用了可选参数但没有使用,就可能产生奇怪的结果:

function myForEach2(arr: any[], callback: (arg: any, index?: number) => void) {
  for (let i = 0; i < arr.length; i++) {
    callback(arr[i]);
  }
}
​
myForEach2([1, 2, 3], (a) => console.log(a));
myForEach2([1, 2, 3], (a, i) => console.log(a, i));
// 1
// 2
// 3
// 1 undefined
// 2 undefined
// 3 undefined

因此 TS 中存在一条规则:为回调函数编写类型时,如果不打算使用某个参数,就不要将该参数设置为可选参数。

函数重载

TypeScript 中可以定义函数重载:

function add(a: number, b: number): number;
function add(arr: number[]): number;
function add(aOrArr: number | number[], b?: number) { 
    if (typeof aOrArr === "number" && typeof b === "number") {
        return aOrArr + b;
    } else if (Array.isArray(aOrArr)) {
        return aOrArr.reduce((sum, item) => sum + item, 0);
    }
    throw new Error("Parameters type error");
​
}
​
console.log(add(1, 2));
console.log(add([1, 2, 3]));

与其他语言中的函数重载定义有很大区别,TS 中的函数重载分为函数类型定义与函数实现两部分。这里的函数定义部分:

function add(a: number, b: number): number;
function add(arr: number[]): number;

函数实现部分:

function add(aOrArr: number | number[], b?: number) { 
    if (typeof aOrArr === "number" && typeof b === "number") {
        return aOrArr + b;
    } else if (Array.isArray(aOrArr)) {
        return aOrArr.reduce((sum, item) => sum + item, 0);
    }
    throw new Error("Parameters type error");
​
}

函数定义部分必须要有函数实现部分,否则会报错,此外函数实现也必须与函数定义匹配。比如这里函数实现考虑到前面的函数定义,就使用了可选参数,表示可能有两个参数也可能有一个参数。此外第一个参数的类型是number|number[]。在函数实现中,使用类型守卫区分不同调用时的情况。

编写良好的重载

看一个例子:

function len(str: string):number;
function len(arr: any[]):number;
function len(strOrArr: any):number{
    if(typeof strOrArr === 'string'){
        return strOrArr.length;
    }
    else if (Array.isArray(strOrArr) && strOrArr){
        return strOrArr.length;
    }
    else{
        throw new Error("Invalid input: expected string or array");
    }
}
​
console.log(len("hello"));
console.log(len([1,2,3]));

通过重载,这里实现了一个函数len用于返回字符串或数组的长度。虽然这么实现没错,但完全可以更直接一些:

function len2(strOrArr: string|any[]):number{
    if(typeof strOrArr === 'string'){
        return strOrArr.length;
    }
    else if (Array.isArray(strOrArr)){
        return strOrArr.length;
    }
    else{
        throw new Error("Invalid input: expected string or array");
    }
}
console.log(len2("hello"));
console.log(len2([1,2,3]));

因此 TS 有一条规则:尽可能使用联合类型的参数而不是重载。

其他类型

void

在 TS 中,如果一个函数没有返回,或者返回空,其返回值类型被视为void:

function void1():void{
    // do something
}
​
function void2():void{
    // do something
    return; 
}

需要注意的是,在 JavaScript 中,一个不返回任何值的函数会隐式返回 undefined,但在 TS 中,void与undefined是不同的。

object

在 TS 中,object指任何非原始值(string、number、bigint、boolean、symbol、null、undefined)。它与空对象{}不同,也与全局类型Object不同。

在 TS 中,使用object而不是Object。

此外,在 TS 中,函数类型被认为是object。

unknow

unknow与any类似,用于表示任意类型,与any不同的是,表示未知类型的unknow上执行任何操作都会报错:

function func1(a: unknown){
    a.length; // 报错,“a”的类型为“未知”
}
​
function func2(a: any){
    a.length; // 不会报错
}

你可以用unknow定义一个返回未知类型的函数:

function safeParse(a: string): unknown {
    return JSON.parse(a);
}
const obj1 = safeParse("{a:123}")

never

如果函数永远不会返回,其返回值类型就是never:

function neverReturn():never{
    throw new Error();
}

就像上面的示例,如果函数必然会导致程序抛出异常或终止执行,这个函数返回值类型就是never。

此外,如果不会被执行的条件分支,也会导致类型推断为never,比如:

function neverExample(a:string|number){
    if(typeof a === 'string'){
        console.log(a) // 推断类型为 string
    }
    else if (typeof a === 'number'){
        console.log(a) // 推断类型为 number
    }
    else{
        console.log(a) // 推断类型为 never
    }
}

never还有一个特性,never类型的变量只能被never类型的值赋值,如果不是,就会报错。可以利用这一点检查有没有未被正确处理的类型:

function neverExample2(a:string|number|any[]){
    if(typeof a === 'string'){
        console.log(a) // 推断类型为 string
    }
    else if (typeof a === 'number'){
        console.log(a) // 推断类型为 number
    }
    else{
        const typeDealCheck: never = a; // 报错,不能将类型“any[]”分配给类型“never”
        console.log(a) // 推断类型为 never
    }
}

变长参数列表

TS 可以定义变长参数:

function multiply(n: number, ...m: number[]): number[] {
    return m.map((x) => x * n);
}
​
const numbers = multiply(10, 2, 3, 4, 5);
console.log(numbers);

与 Python 类似,对于变长参数列表定义的函数,也可以通过结构赋值的方式传参:

const numbers2 = multiply(10, ...[2,3,4,5]);
console.log(numbers2);

结构参数

在 JavaScript 中,可以结构参数:

function sum({ a, b, c }) {
    return a + b + c;
}
console.log(sum({a:1,b:2,c:3}));

在 TS 中,上面的代码会有提示,结构后的a、b、c都会被推断为类型any。也可以为结构参数添加类型注解:

function sum({ a, b, c }:{a:number, b:number, c:number}):number {
    return a + b + c;
}
console.log(sum({a:1,b:2,c:3}));

或者使用类型别名:

type ABC = {a:number, b:number, c:number};
function sum({ a, b, c }:ABC):number {
    return a + b + c;
}
console.log(sum({a:1,b:2,c:3}));

函数的可分配性

理论上,如果函数类型的返回值是void,那就不应该返回任何值,但在 TS 中下面的写法都是合法的:

type VoidFunc = () => void;
​
const voidFunc1: VoidFunc = ()=>{
    return true;
}
const voidFunc2: VoidFunc = ()=>true;
const voidFunc3: VoidFunc = function (){
    return true;
}

这样做是为了某些情况下兼容一些函数式编程的调用,比如:

const arr5 = [1,2,3];
arr5.forEach(voidFunc1);

这里Array.forEach函数接收的是返回void的函数,上面定义的函数虽然实际上有返回值,但也兼容。

这种兼容是有限的,如果是函数字面量的方式定义函数:

function VoidFunc5():void{
    return true; // 报错,不能将类型“boolean”分配给类型“void”
}

The End.

参考资料

  • TypeScript 官方手册

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: ts
最后更新:2026年2月14日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号