关于javascript:TypeScript-之-More-on-Functions

4次阅读

共计 14226 个字符,预计需要花费 36 分钟才能阅读完成。

前言

TypeScript 的官网文档早已更新,但我能找到的中文文档都还停留在比拟老的版本。所以对其中新增以及订正较多的一些章节进行了翻译整顿。

本篇整顿自 TypeScript Handbook 中「More on Functions」章节。

本文并不严格依照原文翻译,对局部内容也做了解释补充。

注释

函数是任何利用的根底组成部分,无论它是部分函数(local functions),还是从其余模块导入的函数,亦或是类中的办法。当然,函数也是值 (values),而且像其余值一样,TypeScript 有很多种形式用来形容,函数能够以怎么的形式被调用。让咱们来学习一下如何书写形容函数的类型(types)。

函数类型表达式(Function Type Expressions)

最简略形容一个函数的形式是应用 函数类型表达式(function type expression)。它的写法有点相似于箭头函数:

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,示意的其实是一个函数有一个类型是 any,名为 string 的参数。

当然了,咱们也能够应用类型别名(type alias)定义一个函数类型:

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

调用签名(Call Signatures)

在 JavaScript 中,函数除了能够被调用,本人也是能够有属性值的。然而上一节讲到的函数类型表达式并不能反对申明属性,如果咱们想形容一个带有属性的函数,咱们能够在一个对象类型中写一个调用签名(call signature)。

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

留神这个语法跟函数类型表达式稍有不同,在参数列表和返回的类型之间用的是 : 而不是 =>

结构签名(Construct Signatures)

JavaScript 函数也能够应用 new 操作符调用,当被调用的时候,TypeScript 会认为这是一个构造函数(constructors),因为他们会产生一个新对象。你能够写一个结构签名,办法是在调用签名后面加一个 new 关键词:

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

一些对象,比方 Date 对象,能够间接调用,也能够应用 new 操作符调用,而你能够将调用签名和结构签名合并在一起:

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

泛型函数(Generic Functions)

咱们常常须要写这种函数,即函数的输入类型依赖函数的输出类型,或者两个输出的类型以某种模式互相关联。让咱们思考这样一个函数,它返回数组的第一个元素:

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

留神此时函数返回值的类型是 any,如果能返回第一个元素的具体类型就更好了。

在 TypeScript 中,泛型就是被用来形容两个值之间的对应关系。咱们须要在函数签名里申明一个 类型参数 (type parameter)

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([]);

推断(Inference)

留神在下面的例子中,咱们没有明确指定 Type 的类型,类型是被 TypeScript 主动推断进去的。

咱们也能够应用多个类型参数,举个例子:

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 的类型(从传入的 string 数组),又能够依据函数表达式的返回值推断出 Output 的类型。

束缚(Constraints)

有的时候,咱们想关联两个值,但只能操作值的一些固定字段,这种状况,咱们能够应用 束缚(constraint)对类型参数进行限度。

让咱们写一个函数,函数返回两个值中更长的那个。为此,咱们须要保障传入的值有一个 number 类型的 length 属性。咱们应用 extends 语法来束缚函数参数:

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;}'.

TypeScript 会推断 longest 的返回类型,所以返回值的类型推断在泛型函数里也是实用的。

正是因为咱们对 Type 做了 {length: number} 限度,咱们才能够被容许获取 a b参数的 .length 属性。没有这个类型束缚,咱们甚至不能获取这些属性,因为这些值兴许是其余类型,并没有 length 属性。

基于传入的参数,longerArraylongerString 中的类型都被推断进去了。记住,所谓泛型就是用一个雷同类型来关联两个或者更多的值。

泛型束缚实战(Working with Constrained Values)

这是一个应用泛型束缚常呈现的谬误:

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' gets value {length: 6}
const arr = minimumLength([1, 2, 3], 6);
// and crashes here because arrays have
// a 'slice' method, but not the returned object!
console.log(arr.slice(0));

申明类型参数(Specifying Type Arguments)

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

写一个好的泛型函数的一些倡议

只管写泛型函数很有意思,但也容易翻车。如果你应用了太多的类型参数,或者应用了一些并不需要的束缚,都可能会导致不正确的类型推断。

类型参数下移(Push Type Parameters Down)

上面这两个函数的写法很类似:

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

第一眼看上去,两个函数可太类似了,然而第一个函数的写法可比第二个函数好太多了。第一个函数能够推断出返回的类型是 number,但第二个函数推断的返回类型却是 any,因为 TypeScript 不得不用束缚的类型来推断 arr[0] 表达式,而不是等到函数调用的时候再去推断这个元素。

对于本节原文中的 push down 含意,在《重构》里,就有一个函数下移(Push Down Method)的优化办法,指如果超类中的某个函数只与一个或者少数几个子类无关,那么最好将其从超类中挪走,放到真正关怀它的子类中去。即只在超类保留共用的行为。这种将超类中的函数本体复制到具体须要的子类的办法就能够称之为 “push down”,与本节中的去除 extend any[],将其具体的推断交给 Type 本身就相似于 push down

Rule: 如果可能的话,间接应用类型参数而不是束缚它

应用更少的类型参数 (Use Fewer Type Parameters)

这是另一对看起来很类似的函数:

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 什么也没做,却导致函数更难浏览和推断。

Rule: 尽可能用更少的类型参数

类型参数应该呈现两次(Type Parameters Should Appear Twice)

有的时候咱们会遗记一个函数其实并不需要泛型

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

其实咱们能够如此简略的写这个函数:

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

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

Rule: 如果一个类型参数仅仅呈现在一个中央,强烈建议你重新考虑是否真的须要它

可选参数(Optional Parameters)

JavaScript 中的函数常常会被传入非固定数量的参数,举个例子:numbertoFixed 办法就反对传入一个可选的参数:

function f(n: number) {console.log(n.toFixed()); // 0 arguments
  console.log(n.toFixed(3)); // 1 argument
}

咱们能够应用 ? 示意这个参数是可选的:

function f(x?: number) {// ...}
f(); // OK
f(10); // OK

只管这个参数被申明为 number类型,x 实际上的类型为 number | undefiend,这是因为在 JavaScript 中未指定的函数参数就会被赋值 undefined

你当然也能够提供有一个参数默认值:

function f(x = 10) {// ...}

当初在 f 函数体内,x 的类型为 number,因为任何 undefined 参数都会被替换为 10。留神当一个参数是可选的,调用的时候还是能够传入 undefined

declare function f(x?: number): void;
// cut
// All OK
f();
f(10);
f(undefined);

回调中的可选参数(Optional Parameters in Callbacks)

在你学习过可选参数和函数类型表达式后,你很容易在蕴含了回调函数的函数中,犯上面这种谬误:

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

index?作为一个可选参数,本意是心愿上面这些调用是非法的:

myForEach([1, 2, 3], (a) => console.log(a));
myForEach([1, 2, 3], (a, i) => console.log(a, i));

但 TypeScript 并不会这样认为,TypeScript 认为想表白的是回调函数可能只会被传入一个参数,换句话说,myForEach 函数也可能是这样的:

function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {for (let i = 0; i < arr.length; i++) {
    // I don't feel like providing the index today
    callback(arr[i]);
  }
}

TypeScript 会依照这个意思了解并报错,只管实际上这个谬误并无可能:

// 冴羽注:最新的 TypeScript 版本中并不会报错
myForEach([1, 2, 3], (a, i) => {console.log(i.toFixed());
  // Object is possibly '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, i) => {console.log(a);
});

在 JavaScript 中,如果你调用一个函数的时候,传入了比须要更多的参数,额定的参数就会被疏忽。TypeScript 也是同样的做法。

当你写一个回调函数的类型时, 不要写一个可选参数, 除非你真的打算调用函数的时候不传入实参

函数重载(Function Overloads)

一些 JavaScript 函数在调用的时候能够传入不同数量和类型的参数。举个例子。你能够写一个函数,返回一个日期类型 Date,这个函数接管一个工夫戳(一个参数)或者一个 月 / 日 / 年 的格局 (三个参数)。

在 TypeScript 中,咱们能够通过写重载签名 (overlaod signatures) 阐明一个函数的不同调用办法。咱们须要写一些函数签名 (通常两个或者更多),而后再写函数体的内容:

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

// No overload expects 2 arguments, but overloads do exist that expect either 1 or 3 arguments.

在这个例子中,咱们写了两个函数重载,一个承受一个参数,另外一个承受三个参数。后面两个函数签名被称为重载签名 (overload signatures)。

而后,咱们写了一个兼容签名的函数实现,咱们称之为实现签名 (implementation signature),但这个签名不能被间接调用。只管咱们在函数申明中,在一个必须参数后,申明了两个可选参数,它仍然不能被传入两个参数进行调用。

重载签名和实现签名(Overload Signatures and the Implementation Signature)

这是一个常见的困惑。大家常会这样写代码,然而又不了解为什么会报错:

function fn(x: string): void;
function fn() {// ...}
// Expected to be able to call with zero arguments
fn();
Expected 1 arguments, but got 0.

再次强调一下,写进函数体的签名是对外部来说是“不可见”的,这也就意味着外界“看不到”它的签名,天然不能依照实现签名的形式来调用。

实现签名对外界来说是不可见的。当写一个重载函数的时候,你应该总是须要来两个或者更多的签名在实现签名之上。

而且实现签名必须和重载签名必须兼容(compatible),举个例子,这些函数之所以报错就是因为它们的实现签名并没有正确的和重载签名匹配。

function fn(x: boolean): void;
// Argument type isn't right
function fn(x: string): void;
// This overload signature is not compatible with its implementation signature.
function fn(x: boolean) {}
function fn(x: string): string;
// Return type isn't right
function fn(x: number): boolean;
This overload signature is not compatible with its implementation signature.
function fn(x: string | number) {return "oops";}

写一个好的函数重载的一些倡议

就像泛型一样,也有一些倡议提供给你。遵循这些准则,能够让你的函数更不便调用、了解。

让咱们构想这样一个函数,该函数返回数组或者字符串的长度:

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

这个函数代码性能实现了,也没有什么报错,但咱们不能传入一个可能是字符串或者是数组的值,因为 TypeScript 只能一次用一个函数重载解决一次函数调用。

len(""); // OK
len([0]); // OK
len(Math.random() > 0.5 ? "hello" : [0]);
No overload matches this call.
  Overload 1 of 2, '(s: string): number', gave the following error.
    Argument of type 'number[] |"hello"'is not assignable to parameter of type'string'.
      Type 'number[]' is not assignable to type 'string'.
  Overload 2 of 2, '(arr: any[]): number', gave the following error.
    Argument of type 'number[] |"hello"'is not assignable to parameter of type'any[]'.
      Type 'string' is not assignable to type 'any[]'.

因为两个函数重载都有雷同的参数数量和雷同的返回类型,咱们能够写一个无重载版本的函数代替:

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

这样函数就能够传入两个类型中的任意一个了。

尽可能的应用联结类型代替重载

在函数中申明 this (Declaring this in a Function)

TypeScript 会通过代码流剖析函数中的 this 会是什么类型,举个例子:

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

TypeScript 可能了解函数 user.becomeAdmin 中的 this 指向的是外层的对象 user,这曾经能够应酬很多状况了,但还是有一些状况须要你明确的通知 TypeScript this 到底代表的是什么。

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

这个写法有点相似于回调格调的 API。留神你须要应用 function 的模式而不能应用箭头函数:

interface DB {filterUsers(filter: (this: User) => boolean): User[];}
 
const db = getDB();
const admins = db.filterUsers(() => this.admin);
// The containing arrow function captures the global value of 'this'.
// Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature.

其余须要晓得的类型(Other Types to Know About)

这里介绍一些也会经常出现的类型。像其余的类型一样,你也能够在任何中央应用它们,但它们常常与函数搭配应用。

void

void 示意一个函数并不会返回任何值,当函数并没有任何返回值,或者返回不了明确的值的时候,就应该用这种类型。

// The inferred return type is void
function noop() {return;}

在 JavaScript 中,一个函数并不会返回任何值,会隐式返回 undefined,然而 voidundefined 在 TypeScript 中并不一样。在本文的最初会有更具体的介绍。

void 跟 undefined 不一样

object

这个非凡的类型 object 能够示意任何不是原始类型(primitive)的值 (stringnumberbigintbooleansymbolnullundefined)。object 不同于空对象类型 {},也不同于全局类型 Object。很有可能你也用不到 Object

object 不同于 Object,总是用 object!

留神在 JavaScript 中,函数就是对象,他们能够有属性,在他们的原型链上有 Object.prototype,并且 instanceof Object。你能够对函数应用 Object.keys 等等。因为这些起因,在 TypeScript 中,函数也被认为是 object

unknown

unknown 类型能够示意任何值。有点相似于 any,然而更平安,因为对 unknown 类型的值做任何事件都是不非法的:

function f1(a: any) {a.b(); // OK
}
function f2(a: unknown) {a.b();
  // Object is of type 'unknown'.
}

有的时候用来形容函数类型,还是蛮有用的。你能够形容一个函数能够承受传入任何值,然而在函数体内又不必到 any 类型的值。

你能够形容一个函数返回一个不晓得什么类型的值,比方:

function safeParse(s: string): unknown {return JSON.parse(s);
}
 
// Need to be careful with 'obj'!
const obj = safeParse(someRandomString);

never

一些函数从来不返回值:

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

never 类型示意一个值不会再被察看到 (observed)。

作为一个返回类型时,它示意这个函数会丢一个异样,或者会完结程序的执行。

当 TypeScript 确定在联结类型中曾经没有可能是其中的类型的时候,never 类型也会呈现:

function fn(x: string | number) {if (typeof x === "string") {// do something} else if (typeof x === "number") {// do something else} else {x; // has type 'never'!}
}

Function

在 JavaScript,全局类型 Function 形容了 bindcallapply 等属性,以及其余所有的函数值。

它也有一个非凡的性质,就是 Function 类型的值总是能够被调用,后果会返回 any 类型:

function doSomething(f: Function) {f(1, 2, 3);
}

这是一个无类型函数调用 (untyped function call),这种调用最好被防止,因为它返回的是一个不平安的 any类型。

如果你筹备承受一个黑盒的函数,然而又不打算调用它,() => void 会更平安一些。

残余参数(Rest Parameters and Arguments)

parametersarguments

argumentsparameters 都能够示意函数的参数,因为本节内容做了具体的辨别,所以咱们定义 parameters 示意咱们定义函数时设置的名字即形参,arguments 示意咱们理论传入函数的参数即实参。

残余参数(Rest Parameters)

除了用可选参数、重载能让函数接管不同数量的函数参数,咱们也能够通过应用残余参数语法(rest parameters),定义一个能够传入数量不受限制的函数参数的函数:

残余参数必须放在所有参数的最初面,并应用 ... 语法:

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

在 TypeScript 中,残余参数的类型会被隐式设置为 any[] 而不是 any,如果你要设置具体的类型,必须是 Array<T> 或者 T[]的模式,再或者就是元祖类型(tuple type)。

残余参数(Rest Arguments)

咱们能够借助一个应用 ... 语法的数组,为函数提供不定数量的实参。举个例子,数组的 push 办法就能够承受任何数量的实参:

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

留神个别状况下,TypeScript 并不会假设数组是不变的(immutable),这会导致一些意外的行为:

// 类型被推断为 number[] -- "an array with zero or more numbers",
// not specifically two numbers
const args = [8, 5];
const angle = Math.atan2(...args);
// A spread argument must either have a tuple type or be passed to a rest parameter.

修复这个问题须要你写一点代码,通常来说, 应用 as const 是最间接无效的解决办法:

// Inferred as 2-length tuple
const args = [8, 5] as const;
// OK
const angle = Math.atan2(...args);

通过 as const 语法将其变为只读元组便能够解决这个问题。

留神当你要运行在比拟老的环境时,应用残余参数语法兴许须要你开启 [downlevelIteration](https://www.typescriptlang.org/tsconfig#downlevelIteration),将代码转换为旧版本的 JavaScript。

参数解构(Parameter Destructuring)

你能够应用参数解构不便的将作为参数提供的对象解构为函数体内一个或者多个局部变量,在 JavaScript 中,是这样的:

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

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

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

这个看起来有些繁琐,你也能够这样写:

// 跟下面是有一样的
type ABC = {a: number; b: number; c: number};
function sum({a, b, c}: ABC) {console.log(a + b + c);
}

函数的可赋值性(Assignability of Functions)

返回 void

函数有一个 void 返回类型,会产生一些意料之外,情理之中的行为。

当基于上下文的类型推导(Contextual Typing)推导出返回类型为 void 的时候,并不会强制函数肯定不能返回内容。换句话说,如果这样一个返回 void 类型的函数类型 (type vf = () => void)
当被利用的时候,也是能够返回任何值的,但返回的值会被疏忽掉。

因而,上面这些() => void 类型的实现都是无效的:

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

而且即使这些函数的返回值赋值给其余变量,也会维持 void 类型:

const v1 = f1();
 
const v2 = f2();
 
const v3 = f3();

正是因为这个个性的存在,所以接下来的代码才会是无效的:

const src = [1, 2, 3];
const dst = [0];
 
src.forEach((el) => dst.push(el));

只管 Array.prototype.push 返回一个数字,并且 Array.prototype.forEach 办法期待一个返回 void 类型的函数,但这段代码仍然没有报错。就是因为基于上下文推导,推导出 forEach 函数返回类型为 void,正是因为不强制函数肯定不能返回内容,所以下面这种 return dst.push(el) 的写法才不会报错。

另外还有一个非凡的例子须要留神,当一个函数字面量定义返回一个 void 类型,函数是肯定不能返回任何货色的:

function f2(): void {
  // @ts-expect-error
  return true;
}
 
const f3 = function (): void {
  // @ts-expect-error
  return true;
};

TypeScript 系列

冴羽的全系列文章地址:https://github.com/mqyqingfeng/Blog

TypeScript 系列是一个我都不晓得要写什么的系列文章,如果你对于 TypeScript 有什么困惑或者想要理解的内容,欢送与我交换,微信:「mqyqingfeng」,公众号:「冴羽的 JavaScript 博客」或者「yayujs」

如果有谬误或者不谨严的中央,请务必给予斧正,非常感激。如果喜爱或者有所启发,欢送 star,对作者也是一种激励。

正文完
 0