Typescript 知识点汇总(二) 函数

864 阅读8分钟

关于TypeScript学习笔记汇总

本文承接上面内容做后续总结

函数

1.参数类型注解(Parameter Type Annotations)

在声明函数时,可以在每个参数后面添加类型标注,以声明函数接受哪些类型的参数。当一个参数有一个类型注释时,该函数的参数将被检查。

// Parameter type annotation
function greet(name: string) {
    console.log("Hello, " + name.toUpperCase() + "!!");
}

// Would be a runtime error if executed!
greet(42); // Argument of type 'number' is not assignable to parameter of type 'string'.

2.返回类型注解(Return Type Annotations)

function getFavoriteNumber(): number {
    return 26;
}

2.1 没有返回值的函数定义返回值类型为 void

let foo = function (str:string):void { 
    console.log('str',str); 
} 
foo('hello world');

2.2 函数返回值如果不确定,使用unkown

比如:这里s是 string类型,返回不一定是string,用 unkown 可以让代码更安全。

function safeParse(s: string): unknown {
  return JSON.parse(s);
}

let x = safeParse('123') // 123 number类型

2.3 有些函数充不返回值用 never

Never 类型表示从未观察到的值。在返回类型中,这意味着函数抛出异常或终止程序的执行。

function fail(msg: string): never {
    throw new Error(msg);
}

3.匿名函数(Anonymous Functions)

匿名函数与函数声明略有不同。当一个函数出现在可以决定如何调用它的地方时,该函数的参数将自动给定类型。

// No type annotations here, but TypeScript can spot the bug
const names = ["Alice", "Bob", "Eve"];

// Contextual typing for function
names.forEach(function (s) {
    console.log(s.toUppercase());
    //Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?
});

// Contextual typing also applies to arrow functions
names.forEach((s) => {
    console.log(s.toUppercase());
    // Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?
});

尽管参数 s 没有类型注释,但 TypeScript 使用 forEach 函数的类型以及数组的推断类型来确定类型 s 将具有的类型。这个过程成为 contexture typing(根据上下文猜测匿名函数参数的类型)。例子中会报错,应该是toUpperCase(C大写)。

4.参数

4.1 可选参数

可以将参数标记为可选:

function foo(bar: number, bas?: string): void {
  // ..
}

foo(123);
foo(123, 'hello');

4.2 默认参数

当调用者没有提供该参数时,你可以提供一个默认值(在参数声明后使用 = someValue ):

function foo(bar: number, bas: string = 'hello') {
  console.log(bar, bas);
}

foo(123); // 123, hello
foo(123, 'world'); // 123, world

4.3 剩余参数

如果参数可以输入任意个数的参数,那就无法挨个定义,ES6中的...拓展运算符,可以将 arguments 这个类数组对象进行结构。比如下面这个例子:

function multiply(n: number, ...m: number[]) {
  return m.map((x) => n * x);
}
const result = multiply(10, 1, 2, 3, 4); // [10, 20, 30, 40]

5.重载(Overloaded)

当我们需要根据参数的不同类型,返回不同类型的结果时,可以使用函数重载。为同一个函数提供多个函数类型定义来进行函数重载。编译器会根据这个列表去处理函数的调用。编译器会依次查找重载列表,找到匹配的函数定义。

function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
    if (d !== undefined && y !== undefined) {
        return new Date(y, mOrTimestamp, d);
    } else {
        return new Date(mOrTimestamp);
    }
}

const d1 = makeDate(12345678);
const d2 = makeDate(5, 5, 5);
const d3 = makeDate(1, 3); // Error , No overload expects 2 arguments, but overloads do exist that expect either 1 or 3 arguments.

上面例子中,我们写了两个重载: 一个接受一个参数,另一个接受三个参数。前两个签名称为重载签名。然后,我们编写了一个具有兼容签名的函数实现。

注意: 这三个 makeDate 函数必须挨着写,中间不能加任何其他的东西,否则就会报错

function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;
console.log(111)
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
   //...
}

报错.png

6.函数中声明 this

JavaScript 规范规定不能有一个名为 this 的参数,因此 TypeScript 使用这个语法空间在函数体中声明这个参数的类型。

interface User {
  id: number;
  admin: boolean;
}
declare const getDB: () => DB;
// ---cut---
interface DB {
  filterUsers(filter: (this: User) => boolean): User[];
}

const db = getDB();
// 这样描述函数里有this可以用
const admins = db.filterUsers(function (this: User) {
  return this.admin;
});

注意,这里不能使用箭头函数

const admins = db.filterUsers(() => this.admin); // Error
// The containing arrow function captures the global value of 'this'.
// Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature.

7.函数可转让性

函数的 void 返回类型可以产生一些不寻常但是预期的行为。带有 void 返回类型(type vf = () = > void)的上下文函数类型在实现时可以返回任何其他值,但它将被忽略。

因此,下面示例中都不会报错:

type voidFunc = () => void;

const f1: voidFunc = () => {
    return true;
};

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

const v1 = f1(); // 返回是void v1没办法用
const v2 = f2(); // 返回是void v2没办法用
const v3 = f3(); // 返回是void v3没办法用

注意一点: 我们不能强制定义函数返回值是void,还让函数返回其他类型,这样会报错

function f2(): void {
    return true; // Error
}

8.构造函数的表达

在函数参数中传入一个构造函数,使用new关键字来表示构造函数

type SomeConstructor = {
    new (s: string): String 
}
function fn(ctor: SomeConstructor) {
    return new ctor("hello")
}

const str = fn(String)
console.log(str) // hello

9.泛型函数

通常编写一个函数,其中输入的类型与输出的类型相关,或者两个输入的类型以某种方式相关。让我们暂时考虑一个返回数组第一个元素的函数:

function firstElement(arr: any[]) {
    return arr[0];
}

这个函数的返回类型是 any,我们希望的是如果返回数组类型会更好。所以可以用到泛型

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

通过向这个函数添加类型参数 Type 并在两个地方使用它,我们在函数的输入(数组)和输出(返回值)之间创建了一个链接。现在,当我们调用它的时候,一个更具体的类型出现了:

//s is of type 'string'
const s = firstElement(["a", "b", "c"]);
// n is of type 'number'
const n = firstElement([1, 2, 3]);
// u is of type undefined
const u = firstElement([]);

10. 推导

我们可以不用在示例中指定Type,ts会自动推断

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

// Parameter 'n' is of type 'string'
// 'parsed' is of type 'number[]'
const parsed = map(["1", "2", "3"], (n) => parseInt(n));

在这个示例中,TypeScript 可以根据函数表达式的返回值(数字)推断出 Input 类型参数的类型(来自给定的字符串数组) ,以及 Output 类型参数。

11. 泛型约束

有时候我们想要关联两个值,但是只能对值的某个子集进行操作。在这种情况下,我们可以使用约束来限制类型参数可以接受的类型种类。

下面示例是一个函数,要返回两个值中较长的一个。要做到这一点,我们需要一个length属性,它是一个数字。我们通过写一个 extends 子句将类型参数约束为该类型,约束所有Type都有 length 这个属性。

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

// longerArray is of type 'number[]'
const longerArray = longest([1, 2], [1, 2, 3]);
// longerString is of type 'alice' | 'bob'
const longerString = longest("alice", "bob");
// Error! Numbers don't have a 'length' property
const notOK = longest(10, 100); // Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.

注意下面这个示例会出错:

function minimumLength<Type extends { length: number }>(
    obj: Type,
    minimum: number
): Type {
    if (obj.length >= minimum) {
        return obj;
    } else {
        return { length: minimum }; // Error
        // Type '{ length: number; }' is not assignable to type 'Type'.
        // '{ length: number; }' is assignable to the constraint of type 'Type', but 'Type' could be instantiated with a different subtype of constraint '{ length: number; }'.
    }
}

我们看到 返回{ length: minimum }看起来好像是可以的,但是我们需要的返回值是Type类型,而这里泛型约束 Type 有length属性,但是不是所有有length 属性的都是 Type

12. 指定类型参数

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

// 如果直接用不匹配的数组调用这个函数会报错
const arr = combine([1, 2, 3], ["hello"]);
// Type 'string' is not assignable to type 'number'.

我们可以手动指定Type

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

13.函数编写规范

13.1 下推类型参数

有两种编写类似函数的方法,哪种更好呢?

function firstElement1<Type>(arr: Type[]) {
    return arr[0];
}

function firstElement2<Type extends any[]>(arr: Type) {
    return arr[0];
}

// a: number (good)
const a = firstElement1([1, 2, 3]);
// b: any (bad)
const b = firstElement2([1, 2, 3]);

乍看觉得似乎完全相同,但是 firstElement1 编写的更好,他的推断返回的类型是Type,firstelement2 的推断返回类型是 any,因为 TypeScript 必须使用约束类型解析 arr[0]表达式。尽可能使用参数本身的类型。

13.2 使用更少的类型参数

下面是一对类似的函数:

function filter1<Type>(arr: Type[], func: (arg: Type) => boolean): Type[] {
    return arr.filter(func);
}

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

filter1 更好,泛型参数少,因为 func 依赖Type就可以进行定义,就不需要再额外定义专门的泛型参数或者参数类型。否则会让函数更难阅读和推理!

13.3 类型参数应该出现两次

我们再看下面这个例子:

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

greet("world");

这个例子也不好,因为这里都没有必要使用泛型,我们可以更简化它:

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

所以能不用泛型就不用,如果一个类型参数只出现在一个位置,请重新考虑是否真的需要它

参考文章