TypeScript 函数类型定义

函数参数类型定义/返回值类型定义

function fn(name: string): void{
  console.log(name);
  // return name; // 报错
}

fn('zs'); //ok
// fn(1) //报错 

可选参数/参数默认值

function getInfo(name: string, age?: number, gender: string = '男'):void {
  console.log(name + age + gender);
}

getInfo('zs'); //ok zsundefined男
getInfo('zs', 18); //ok zs18男
getInfo('zs', 18, '女'); //ok zs18女

函数的参数展开

function sum(...numbers: number[]): number{
  return numbers.reduce((pre, next) => pre + next);
}
console.log(sum(1,2,3));

函数重载

在TypeScript中,我们可以通过写重载签名(overlaod signatures)说明一个函数的不同调用方法。我们需要写一些函数签名(通常两个或者更多),然后再写函数具体的内容:

// 可以规定多种函数传参的方式  灵活复用函数
function add(x: string, y: string): void //重载签名
function add(x: number, y: number): void //重载签名
function add(x: string | number, y: string | number): void { // 实现签名
  // do something
}
add(1, 2); // ok
add('11', '22') // ok
// add(10, 'zs'); //报错

需要强调一下,写进函数体的签名,也就是实现签名,对外部来说是"不可见"的,这也意味着不能按照实现签名的方式来调用:

function fn(x: string): void;
function fn() {
  console.log('11');
}

fn();// 报错: 应有 1 个参数,但获得 0 个。

注意:实现签名和重载签名必须兼容! 另一个注意点就是,TypeScript中,每一次函数调用都只能用一个函数重载处理,举个例子:

function len(s: string): number;
function len(arr: any[]): number;
function len(x: any) {
  return x.length;
}

len(''); // ok
len([2]); // ok
len(Math.random() > 0.5 ? 'hello' : [3]); // error

// 没有与此调用匹配的重载。
//   第 1 个重载(共 2 个),“(s: string): number”,出现以下错误。
//     类型“number[] | "hello"”的参数不能赋给类型“string”的参数。
//       不能将类型“number[]”分配给类型“string”。
//   第 2 个重载(共 2 个),“(arr: any[]): number”,出现以下错误。
//     类型“number[] | "hello"”的参数不能赋给类型“any[]”的参数。
//       不能将类型“string”分配给类型“any[]”。

因为这两个函数重载都有相同的参数数量和相同的返回类型,则可以用一个无重载函数的版本替代:

function len(x: any[] | string): number {
  return x.length;
}

尽可能的使用联合类型替代重载。

函数类型表达式

描述一个函数的方式是使用函数类型表达式,它的写法有点类似于箭头函数:

function greeter(fn: (a: string) => void) {
  fn('hello world');
}

function printConsole(s: string) {
  console.log(s)
}

greeter(printConsole);

// fn: (a: string) => void
// 这里表示函数有一个名为a且类型为字符串的参数,这个函数没有任何返回值。
// 如果一个函数参数的类型没有明确给出,它会被隐式设置成any

使用类型别名定义一个函数类型

type GreetFn = (a: string) => void;
function greeter(fn: GreetFn) {
  fn('hello world');
}

调用签名

在JavaScript中,函数除了可以被调用,还可以有自己的属性,但是上面的函数类型表达式并不能支持声明属性,如果想描述一个带属性的函数,我们可以在一个对象类型中写一个调用签名(call signature)

type DescribableFn = {
  desc?: string;
  (someArg: number): boolean;
}
function doSomething(fn: DescribableFn) {
  console.log(fn.desc + ':' + fn(7))
}
function Fn(n: number): boolean {
  return n % 2 === 0;
}
Fn.desc = 'number is even?'
doSomething(Fn);

//console: number is even?:false

构造签名

JavaScript中函数也可以使用new操作符调用,当被调用的时候,TypeScript会认为这是一个构造函数(constructors),因为他们会产生一个新对象。构造签名的书写方法是在调用签名前加一个new关键词:

class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

type SomeConstructor = {
  new (s: string): Person;
}

function fn(ctor: SomeConstructor) {
  return new ctor('zs');
}

console.log(fn(Person)); // Person { name: 'zs' }

泛型函数

在TypeScript中,泛型就是被用来描述两个值之间的对应关系。我们需要在函数签名里声明一个**类型参数,**当我们调用它的时候,一个更具体的类型就会被推断出来。

function firstEle<Type>(arr: Type[]): Type | undefined {
  return arr[0];
}

// s: string | undefined
const s = firstEle(['a', 'b', 'c']);
// n: number | undefined
const n = firstEle([1, 2, 3]);
// u: undefined
const u = firstEle([]);

在上面的例子中,我们没有明确指定Type的类型,类型是被TypeScript自动推断出来的。 举个使用多个类型参数的例子:

function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[]{
  return arr.map(func)
}

// parsed: number[]
const parsed = map(['1', '2', '3'], (n) => parseInt(n));

在这个例子中,TypeScript既可以判断出Input的类型(从传入的string[]),又可以根据函数表达式的返回值推断出OutPut的类型。

约束

有些时候,我们想关联两个值,但是只能操作值的一些固定字段,这种情况我们可以使用约束对类型参数进行限制。

interface Obj {
  length: number;
}

function longest<Type extends Obj>(a: Type, b: Type): Type {
  if (a.length > b.length) {
    return a
  } else {
    return b
  }
}

const la = longest([1, 2], [1, 2, 3]);
const ls = longest('hello','');
// const ln = longest(323,456); //报错

泛型约束实战

这是一个使用泛型约束常出现的问题:

interface Obj {
  length: number;
}

function longest<Type extends Obj>(a: Type, minimum: number): Type {
  if (a.length > minimum) {
    return a
  } else {
    return { length: minimum }; 
    // 报错:不能将类型“{ length: any; }”分配给类型“Type”。"{ length: any; }" 可赋给 "Type" 类型的约束,但可以使用约束 "Obj" 的其他子类型实例化 "Type"。
  }
}

这个问题我理解为:函数应返回与入参相同类型的对象,而不仅仅是返回符合约束的对象。

声明类型参数

TypeScript通常能够自动推断泛型调用中传入的类型参数,但也并不能总是推断出。举个例子,有这样一个合并两个数组的函数:

function combine<T>(arr1: T[], arr2: T[]): T[] {
  return arr1.concat(arr2);
}

如果像下面这样调用就会出现错误:

// 报错:不能将类型“string”分配给类型“number”
const arr = combine([1,2,3], ['hello']);

而如果你执意要这样做,你可以手动指定T

const arr = combine<string | number>([1,2,3], ['hello']);

在实际开发中,尽可能的直接使用类型参数而不是约束它。当只关联了一个值的类型参数出现时,则考虑需不需要使用类型参数。

记住:类型参数是用来关联多个值之间的类型。如果一个类型参数只在函数签名里出现了一次,那它就没有跟任何东西产生关联。

在函数中声明this

TypeScript会根据代码流分析函数中的this是什么类型,举个例子:

const user = {
  id: 1,
  admin: false,
  becomeAdmin: function () {
    this.admin = true;
   }
}

TypeScript可以理解函数user.becomeAdmin中的this指向的是外层的对象user,这已经可以应付很多情况了,但还有一些情况你要明确的告诉TypeScriptthis到底代表什么。 在JavaScript中,this是保留字,所以不能当作参数使用,但是在TypeScript中,可以允许你在函数体内声明 this的类型。

interface DB {
  filterUsers(filter: (this: User) => boolean): User[];
}
 
const db = getDB();
const admins = db.filterUsers(function (this: User) {
  return this.admin;
});
// 注意: 不能使用箭头函数 因为箭头函数的this指向声明时的上下文
// const admins = db.filterUsers(() => this.admin); 

剩余参数(Rest Parameters and Arguments)

parametersarguments都可以表示函数的参数,本节内容中,我们定义parameters表示定义函数时设置的形参,arguments表示实际传入函数的实参。

剩余参数(Rest Parameters)

我们可以用剩余参数语法定义一个传参数量不受限制的函数:

function multiply(n: number, ...m: number[]) { // 剩余参数必须放在所有参数的最后面,使用...语法
  return m.map(x => n * x);
}

const a = multiply(10, 1, 2, 3, 4);
console.log(a) //[ 10, 20, 30, 40 ]

在TypeScript中,剩余参数的类型会被隐式设置成any[]而不是any,如果你要设置具体的类型,必须是Array<T>或者T[]的形式,再或者就是元组类型。

剩余参数(Rest Arguments)

我们可以借助一个使用...语法的数组,为函数提供不定数量的实参。例如数组的push方法:

const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
arr1.push(...arr2);

要注意的是,一般情况下,TypeScript并不会假定数组是不变的(也就是会认为数组的长度会变化),在调用一些规定了函数参数数量的时候,会导致一些意外行为:

const args = [8, 5];
const angle = Math.atan2(...args) // 报错:扩张参数必须具有元组类型或传递给 rest 参数。

修复这个问题需要写一点代码,通常使用as const 是最直接有效的解决方法:


const args = [8, 5] as const;
const angle = Math.atan2(...args) // ok

参数结构(Parameter Destructuring)

我们可以使用参数解构方便的将传入函数的参数对象结构为函数体内一个或多个局部变量,在JavaScript中是这样的:

function sum({a, b, c}) {
  console.log(a + b + c);
}
sum({ a: 10, b: 5, c: 3 });

在TypeScript中,解构语法后要写上对象类型的注释:

type ABC = {
  a: number,
  b: number,
  c: number
}

function sum({a, b, c}: ABC) {
  console.log(a + b + c);
}
sum({ a: 10, b: 5, c: 3 });

函数的可赋值性

返回void 函数有一个void返回类型,会产生一些意料之外,情理之中的行为。 当基于上下文的类型推导,推导出返回类型为void的时候,并不会强制函数一定不能返回内容。 说人话就是一个函数类型返回void类型(type vf = () => void),在被应用时,是可以返回任何值的,但返回的值会被忽略掉(可以拿到 但类型是void)。

type voidFunc = () => void;
 
const f1: voidFunc = () => {
  return true;
};
 
const f2: voidFunc = () => true;
 
const f3: voidFunc = function () {
  return true;
};

const ad = f1(); // ad的type是void
console.log(ad); //true

需要特殊注意的是,当一个函数字面量定义返回一个void类型,函数是一定不能返回任何东西的。 注意:此段是官方文档给出的例子,但实际练习时我没有任何报错及说明,依然可以返回值并且拿到

function f2(): void {
  // @ts-expect-error
  return true;
}
 
const f3 = function (): void {
  // @ts-expect-error
  return false;
};
const a = f3();
console.log(a); // 依然可以拿到值,而且没有任何报错?