可以用函数类型表达式表示一个函数的类型:
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.
参考资料

文章评论