美文网首页
TypeScript 函数 详解

TypeScript 函数 详解

作者: you的日常 | 来源:发表于2021-11-29 16:45 被阅读0次

函数

无论是本地函数,还是从其它模块导入的函数,或者是类上的方法,函数都是任何应用的基本组成部分。它们同样也是值,就和其它值一样,TypeScript 有很多种描述函数如何被调用的方式。接下来,让我们了解如何编写类型去描述函数吧。

函数类型表达式

最简单的描述函数的方式就是使用函数类型表达式。这些类型在语法上和箭头函数非常相似:

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

语法 (a: string) => void 表示“某个函数接受名为 a 的参数,类型为字符串,且该函数没有返回值”。和函数声明一样,如果没有指定参数类型,那么参数会被隐式推断为 any 类型。

注意参数名是必需的。函数类型 (string) => void表示“某个函数接受名为 string 的参数,类型为 any

当然,我们也可以使用类型别名为某个函数类型命名:

type GreetFunction = (a: string) => void;
function greeter(fn: GreetFunction) {
  // ...
}

调用签名

在 JavaScript 中,函数可以拥有属性,以方便进行调用。但是,TypeScript 的函数类型表达式语法不允许声明属性。如果我们想要描述某个可以通过属性被调用的东西,那么我们可以在一个对象类型上编写一个调用签名:

type DescribableFunction = {
  description: string;
  (someArg: number): boolean;
};
function doSomething(fn: DescribableFunction) {
  console.log(fn.description + " returned " + fn(6));
}

注意这个语法和函数类型表达式的语法不太一样。在参数列表和返回值类型之间,它使用的是 : 而不是 =>

构造签名

JavaScript 函数也可以通过 new 运算符进行调用。TypeScript 将这种函数视为构造器,因为它们通常用于创建新对象。你可以在调用签名前面添加 new 关键字,从而编写一个构造签名:

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

有些对象,比如 JavaScript 的 Date 对象,可以直接调用也可以通过 new 调用。你可以在同一类型中任意组合调用签名和构造签名:

interface CallOrConstruct {
  new (s: string): Date;
  (n?: number): number;
}

泛型函数

我们经常需要编写某个函数,它的输入值类型和输出值类型相关联,或者两个输入值的类型在某种程度上相关联。假设现在有一个函数,它需要返回某个数组中的第一个元素:

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

这个函数可以运行,但不幸的是,它的返回值类型为 any。如果返回值类型和数组类型一样,那就更好了。

在 TypeScript 中,当我们想要描述两个值之间的对应关系的时候,可以使用泛型。怎么使用呢?只需要在函数签名中声明一个类型参数:

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

通过添加一个类型参数 Type 到函数中,并在两个地方使用这个参数,我们已经让函数的输入值(数组)和输出值(返回值)建立了一个联系。现在当我们再次调用函数的时候,将会得到一个类型更加具体的返回值:

// s 的类型是 string
const s = firstElement(["a", "b", "c"]);
// n 的类型是 number
const n = firstElement([1, 2, 3]);
// u 的类型是 undefined
const u = firstElement([]);

推断

注意在上面的例子中,我们不需要特殊说明 Type 的类型。因为 TypeScript 可以推断 —— 自动选择它的类型。

我们也可以使用多个类型参数。举个例子,可以像下面这样实现一个独立版本的 map

function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[] {
  return arr.map(func);
}
 
// n 的类型是 string
// parsed 的类型是 number[]
const parsed = map(["1", "2", "3"], (n) => parseInt(n));

注意在这个例子中,TypeScript 可以基于给定的 string 类型数组推断出 Input 类型参数的类型,也可以基于函数表达式的返回值类型(number)推断出 Output 类型参数的类型。

约束

我们目前编写的泛型函数适用于所有类型的值。有时候,我们想要关联两个值,但要求只能对值的某个子集进行操作。这时候,我们可以使用“约束”去限制类型参数可以接受的种类。

我们来编写一个函数,它可以返回两个值长度较长的那个。为了实现这个功能,我们需要一个 number 类型的 length 属性。这里,我们通过 extends 子句将类型参数约束为具有 length 属性的类型:

function longest<Type extends { length: number }>(a: Type, b: Type) {
  if (a.length >= b.length) {
    return a;
  } else {
    return b;
  }
}
 
// longerArray 的类型是 number[]
const longerArray = longest([1, 2], [1, 2, 3]);
// longerString 的类型是 'alice' | 'bob'
const longerString = longest("alice", "bob");
// 报错!number 类型的值没有 length 属性
const notOK = longest(10, 100);
// Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.

在这个例子中,没有什么有趣的事情值得注意。我们允许 TypeScript 推断 longest 函数返回值的类型。返回值的类型推断也适用于泛型函数。

我们将 Type 约束为 {length: number},因此得以访问 a 参数和 b 参数的 length 属性。如果没有类型约束,那么我们是无法访问这个属性的,因为传入的参数可能是其它不具备 length 属性的类型。

longerArraylongerString 的类型是基于函数参数推断出来的。记住,泛型都是将两个或多个值与同一类型相关联!

使用约束值

下面是使用泛型约束的时候常见的一个错误:

function minimumLength<Type extends { length: number }>(
  obj: Type,
  minimum: number
): Type {
  if (obj.length >= minimum) {
    return obj;
  } else {
    return { length: minimum };
/*
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; }'.
*/  
  }
}

这个函数看起来似乎没毛病 —— Type 被约束为 {length: number},函数要么返回 Type,要么返回匹配约束条件的值。但问题在于,函数承诺返回一个与传入参数相同类型的对象,而不是某个匹配约束条件的对象。如果这段代码是合法的,那么你很可能写出下面这样无法正常运行的代码:

// 'arr' 的值是 { length: 6 }
const arr = minimumLength([1, 2, 3], 6);
// 这里会报错,因为 arr 不是数组,没有 slice 方法
console.log(arr.slice(0));

指定类型参数

在一次泛型调用中,TypeScript 通常可以推断出预期的类型参数,但也有例外。举个例子,假设你要编写一个合并两个数组的函数:

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"]);

编写良好泛型函数的指南

编写泛型函数很有意思,并且很容易因为使用类型参数而忘乎所以。使用过多类型参数或者在不需要的时候使用约束条件,会导致类型推断很难成功,对函数的调用者造成困惑。

抑制类型参数

下面两种方式编写的函数很相似:

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] 表达式,而不是在函数调用期间“等着”去解析元素。

规则: 在可能的情况下,请直接使用类型参数,而不是给它设置约束条件

使用更少的类型参数

下面是另一对相似的函数:

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);
}

在第二个函数中,我们创建了一个类型参数 Func,但是它并没有关联两个值。这是一个危险的信号,因为这意味着调用者传入实际的类型参数的时候,必须毫无理由地手动指定一个额外的类型参数。Func 不但没有帮上任何忙,反而破坏了函数的可读性和合理性。

规则: 总是尽可能地使用更少的类型参数

类型参数应该出现两次

有时候我们会忘记某个函数可能是不需要使用泛型的:

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

相关文章

  • TypeScript 函数 详解

    函数 无论是本地函数,还是从其它模块导入的函数,或者是类上的方法,函数都是任何应用的基本组成部分。它们同样也是值,...

  • TypeScript函数详解(五)

    1.函数的类型 2 函数类型的案例 3.函数的可选类型 4.默认参数 5.剩余参数 从ES6开始,JavaScri...

  • TypeScript中的函数详解

    一、写法 声明式 ts在传参时都会规定参数的类型,还有它返回值的类型也会在函数执行之前都已经规定好,如果传参的类型...

  • typescript语法

    参考:typescript参考1 typescript参考2 函数参数类型定义 声明函数参数默认值 ...

  • C/C++的30个冷知识

    数据格式详解 输入输出函数详解 字符串处理函数详解 内存函数详解 类详解 数据格式详解 2^8=256(同样是一个...

  • TypeScript高级用法详解(转载)

    TypeScript高级用法详解 原文链接:https://segmentfault.com/a/11900000...

  • TypeScript函数构造签名

    TypeScript函数构造签名

  • TypeScript 函数-函数类型

    参考网址:函数 · TypeScript中文网 · TypeScript——JavaScript的超集 前言 介绍...

  • typescript函数

    typescript函数的隐式定义 在typescript中的函数并不需要刻意去定义,比如我们实现一个加法函数: ...

  • Typescript函数式编程: 函数异常

    Typescript函数式编程: 函数异常 翻译自Advanced functional programming ...

网友评论

      本文标题:TypeScript 函数 详解

      本文链接:https://www.haomeiwen.com/subject/mdslxrtx.html