函数类型表达式
function hello(x: string) {
console.log(x)
}
//greeter函数的参数是一个函数fn,fn也有一个string类型参数,无返回值。
function greeter(fn: (a: string) => void) {
fn('hello')
}
greeter(hello)
也可以把定义参数类型的语句单独提取出来。
function hello(x: string) {
console.log(x)
}
type Hello = (a: string) => void
function greeter(fn: Hello) {
fn('hello')
}
greeter(hello)
调用签名
上述例子描述了函数可调用性,此外,函数还具有属性。函数表达式不能声明函数的属性,如果我们想描述函数可以做什么,可以为函数设置属性,及调用签名。
type DescriptionFunction = {
//函数的属性和属性的类型
description: string
//(函数参数名:参数类型):函数返回值类型
(someArg: number): boolean
}
function doSomething(fn: DescriptionFunction) {
console.log(fn.description + 'returned' + fn(4))
}
function test(someArg: number) {
return someArg > 3
}
test.description = 'haha'
doSomething(test)
同理你可以自己添加其他属性和参数
type DescriptionFunction = {
description: string
total: number
(someArg: number, otherArg: string): string
}
function doSomething(fn: DescriptionFunction) {
console.log(fn.description + fn.total + 'returned' + fn(4, 'other'))
}
function test(someArg: number, other: string) {
return someArg + other
}
test.total = 0
test.description = '函数的描述属性'
doSomething(test)
构造签名
在调用签名中可以使用new关键字来编写构造签名。
type SomeConstructor = {
new (a: string): object//笼统的说,函数类型是new(string类型的参数),返回值是一个object
}
function constructObj(fn: SomeConstructor) {
return new fn('string')//调用时也不能少new关键字。
}
可选参数、以及函数参数不同和返回值不同等情况都会导致函数类型不同,你可以使用这些特性来定义一个复杂的函数类型。
//需要保证每种定义不重合。
interface CallOrConstruct {
(n?: number): string//这个函数可以是不接收参数或接收number参数,并返回string的类型
(x: string): boolean//这个函数可以是接收string参数返回boolean值的类型
new (s: string): Date//这个函数可以是构造函数,并且需要接收一个string参数,返回Date值。
}
function doSomeing(fn: CallOrConstruct) {
//这里只是举例调用,具体能怎么调用,还得看传过来的fn参数带来一些什么样的参数。
fn(1)
fn('string')
new fn('2022-06-01')
}
你可以将上面例子类比于我们经常使用的Date。传时间戳的时候它给我们返回一个时间格式字符串,传某个时间字符串并使用new关键字的时候,他会给我们返回一个日期对象。
泛型函数
当我们的输出类型与输入类型相关时,我们就可以使用泛型函数。
定义方法:使用尖括号定义泛型名称,放在函数名和函数括号中间,现在你可以把定义的泛型类似于一个已知的基础类型来用,用来定义参数和返回值的类型。
function test<Type>(x: Type[]): Type | undefined {
return x[0]
}
const t1 = test(['1', '2', '3'])//const t1: string | undefined
const t2 = test([1, 2, 3])//const t2: number | undefined
const t3 = test([])//const t3: undefined
你可以使用多个泛型变量
下面编写一个map函数。接收一个数组和一个函数,函数内对接收的数据进行操作,返回值是一个数组。
function myMap<Input, Output>(arr: Input[], func: (a: Input) => Output): Output[] {
return arr.map(func)//这里调用的是公共的js中的map函数
}
// const test1: number[]
//在第一个测试案例中,Input类型是number,Output类型是number,返回值类型是number[]
const arr1 = [1, 2, 3]
const test1 = myMap(arr1, (item) => item + 1)
// const test2: string[]
//在第二个测试案例中,Input是object,output是string,返回值类型是string[]
const arr2 = [
{ id: '12', age: 12 },
{ id: '23', age: 34 }
]
const test2 = myMap(arr2, (item) => item.id)
TypeScript 可以根据函数表达式的返回值推断 Input 类型参数的类型以及 Output 类型参数。
第一次调用myMap,myMap的类型推断为:
function myMap<number, number>(arr: number[], func: (a: number) => number): number[]
第二次调用myMap,myMap的类型推断为:
function myMap<{id: string;age: number;}, string>(arr: {id: string;age: number;}[], func: (a: {id: string;age: number;}) => string): string[]
类型约束
泛型函数可以处理任何类型的值。有时候,我们有两个类型相关的值,但是我们只能操作他的子属性,我们就可以使用类型约束。
下面的test1和test2的类型是根据参数推断出来的。泛型就是将两个或多个具有相同类型的值关联起来!
interface hasLength {
length: number
}
function longer<Type extends hasLength>(a: Type, b: Type) {
if (a.length > b.length) return a
else return b
}
const test1 = longer([1, 2], [1, 2, 3])
const test2 = longer('dfs', 'fdjask')
//const test3 = longer(10, 11)//error,number类型没有length属性
使用约束值的错误
泛型定义的是一种类型,光满足约束的条件不行,还得是真真确确的类型。
让我们看下面这个例子,泛型Type进行了约束,返回值也是Type类型。
function minimumLength<Type extends { length: number }>(obj: Type, minimum: number): Type {
if (obj.length >= minimum) {
return obj
} else {
return { length: minimum } //error 不能将类型“{ length: number; }”分配给类型“Type”。
}
}
假设上面的语法合发,将会得到一个完全行不通的代码。
function minimumLength<Type extends { length: number }>(obj: Type, minimum: number): Type {
if (obj.length >= minimum) {
return obj
} else {
return { length: minimum } //error 不能将类型“{ length: number; }”分配给类型“Type”。
}
}
//obj.length<minimum,return { length : 4 }
const arr = minimumLength([1, 2, 3], 4)
const x = arr.slice(0)//行不通
分析得知,尽管对象中有length属性,但是对象根本没有slice方法。
指定类型参数
下面是一个将两个数组合并的函数。
function combine<Type>(a: Type[], b: Type[]): Type[] {
return a.concat(b)
}
combine([1, 2], [3, 4])
combine(['1', '2'], ['3', '4'])
combine([1, 2], ['3', '4'])//error Type已经推断为number类型,后面不能再传string类型的数组了
第三个测试案例中,由于 Type已经推断为number类型,后面不能再传string类型的数组了。
这个时候我们就可以手动指定类型参数的值,而不是让TypeScript给我们默认推断。
//combine([1, 2], ['3', '4'])
combine<number | string>([1, 2], ['3', '4'])
当我们把代码改为上述模样后,可以发现combine的类型推断变为:
实质上这个时候Type的类型就是string|number。
function combine<string | number>(a: (string | number)[], b: (string | number)[]): (string | number)[]
编写良好的泛型函数指南
下推类型参数
如果可以,使用类型参数本身而不是约束它。
function test1<Type>(arr: Type[]) {
return arr[0]
}
function test2<Type extends any[]>(arr: Type) {
return arr[0]
}
//const t1: number
const t1 = test1([1, 2, 3])
//const t2: any
const t2 = test2([1, 2, 3])
显然,第一种写法比第二种写法要好,第一种写法可以推断出返回值的类型,而第二种写法推断的类型是any。
我们尽量直接使用类型,而不做类型约束。
使用更少的类型参数
如果可以,始终使用尽可能少的类型参数。
例如写一个过滤函数时,下面第一种写法只定义了一个类型参数,剩下的都是利用这个类型灵活使用,最终调用也能准确地推断出返回值地类型。
function filter1<Type>(arr: Type[], func: (arg: Type) => boolean): Type[] {
return arr.filter(func)
}
const t1 = filter1([-1, 2, 3], (item) => item > 0)
对于第二种写法,这里多写了一个类型参数Func,还对Func做了类型限制,用起来两者得到的结果是一样的,但是,Func并没有关联两个或多个值的类型,它没有做任何事情,只会让读者更难理解。
function filter2<Type, Func extends (arg: Type) => boolean>(arr: Type[], func: Func): Type[] {
return arr.filter(func)
}
const t2 = filter2([-1, 2, 3], (item) => item > 0)
类型参数应该出现两次
如果一个类型参数只出现在一个位置,强烈重新考虑是否真的需要它
有时候我们可能忘了函数是不需要泛型的。
function greet<Str extends string>(s: Str) {
console.log("Hello, " + s);
}
greet("world");
我们可以更容易地编写一个更简单的版本。
function greet(s: string) {
console.log("Hello, " + s);
}
泛型用于关联多个值的类型。如果一个泛型只在函数签名中使用一次,它就没有任何关系。这包括推断的返回类型;
例如,如果 Str 是 greet 的推断返回类型的一部分,它将关联参数和返回类型,因此尽管在书面代码中只出现一次,但它会被使用两次。
可选参数
//问号,可传可不传,可传undefined
function f1(n?: number): void {
console.log(n)
}
f1()
f1(1)
f1(undefined)
//指定默认值
function f2(x: number = 10) {
console.log(x)
}
f2(1)
回调中的可选参数
为回调编写函数类型时,切勿编写可选参数,除非你打算在不传递该参数的情况下调用该函数。
如果你在函数类型中声明了一个参数为可选参数,那么在使用这个函数类型时,传递该参数是可选的。如果你不打算在调用函数时传递该参数,但是却在函数类型中将其声明为可选参数,可能会导致一些问题。
这会导致:你有可能传递了可选参数,但是函数里面没有没有调用。或者,你没传递可选参数,但是函数里面调用了。这带来了一些难以查找的错误。
function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {
for (let i = 0; i < arr.length; i++) {
// callback(arr[i], i)
callback(arr[i])
}
}
myForEach([1, 2, 3], (item) => console.log(item))
myForEach([1, 2, 3], (item, index) => console.log(item, index))
函数重载
我们希望可以以各种参数、参数个数、参数类型来调用函数。这可以使用函数重载来实现。
函数签名一定多于两个。
函数只有一个实现签名,但是这个签名不能直接调用。
函数的调用是按照函数的重载签名定义来的,下面两个函数签名中,只有传递一个参数的,或者传递三个参数的。最后一种调用,传递了两个参数,是不可行的。
//函数的重载签名
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(1798329749)
const d2 = makeDate(6, 11, 2022)
//error没有需要 2 参数的重载,但存在需要 1 或 3 参数的重载
//const d3 = makeDate(1, 2)
重载签名和实现签名
重载签名:没有方法体,只有函数名、函数参数及类型、函数返回值及类型。
实现签名:兼容所有函数签名,有方法体。
从外部看不到实现的签名。在编写重载函数时,你应该始终在函数实现之上有两个或多个签名。
实现签名还必须与重载签名兼容。
例如,下面这个例子,就是实现签名与重载签名在函数参数类型不兼容。
function fn(x: boolean): void;
function fn(x: string): void;//error此重载签名与其实现签名不兼容
function fn(x: boolean) {}
稍微改动一下实现签名的函数参数即可:
function fn(x: boolean | string) {}
下面这个例子是实现签名与重载签名在返回值类型部分不兼容。
function fn(x: string): string;
function fn(x: number): boolean;
function fn(x: string | number) {
return "oops";
}
同样,改动一下实现签名的返回值类型即可:
function fn(x: string | number): string | boolean {
return 'oops'
}
需要注意的是,上面的改动,能使函数正常调用,但是函数调用的时候,不能传递一个不确定类型的值。例如,fn(Math.random() > 0.5 ? 'dfs' : true)
这将会导致报错,原因:TypeScript只会将函数调用解析为一种重载。传递可能值,会混淆TypeScript的判读。
编写好的重载
尽可能使用联合类型的参数而不是重载。
下面是一个返回字符串或数组长度的函数。
function len(s: string): number
function len(arr: any[]): number
function len(x: any) {
return x.length
}
len('')
len([0])
len(Math.random() > 0.5 ? 'hello' : [0]) //error没有与此调用匹配的重载。
我们不能使用可能是字符串或数组的值来调用函数重载,因为TypeScript只能将函数调用解析为单个重载。
我们将函数改为非重载版本即可。
function len(x: any[] | string) {
return x.length;
}
在函数中声明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 而不是箭头函数来获得此行为。
其他需要了解的类型
void
void 表示不返回值的函数的返回值。只要函数没有任何 return 语句,或者没有从这些返回语句返回任何显式值,它就是推断类型。
function test1() {
return
} //function test1(): void
function test2() {
console.log('')
} //function test2(): void
在 JavaScript 中,不返回任何值的函数将隐式返回值 undefined。但是,void 和 undefined 在 TypeScript 中不是一回事。
object
特殊类型 object 指的是任何非基础值(string、number、bigint、boolean、symbol、null 或 undefined)。
object和{}不同。
object和Object也不同,在TypeScript中,你可能永远用不到Object。
在JavaScript中,函数值是对象:它们有属性,在它们的原型链中有 Object.prototype,是 instanceof Object,你可以在它们上调用 Object.keys,等等。
在 TypeScript 中,函数类型被视为 object。
unknown
unknown类型代表任何值,作用类似于any类型,但是比any类型安全,因为使用unknown做任何事情都是不合法的。
你可以描述一个返回未知类型值的函数,而不需要使用any。
function safeParse(s: string): unknown {
return JSON.parse(s)
}
never
有些函数从不返回值,可以使用never定义返回值类型。
function fail(msg: string): never {
throw new Error(msg)
}
never 类型表示从未观察到的值。在返回类型中,这意味着函数抛出异常或终止程序的执行。
TypeScript确定联合中没有任何类型时,也会返回never。
function fail(msg: string): never {
throw new Error(msg)
}
function fn(x: string | number) {
if (typeof x === 'string') {
console.log(x) //(parameter) x: string
} else if (typeof x === 'number') {
console.log(x) // (parameter) x: number
} else {
console.log(x) // (parameter) x: never
}
}
Function
全局类型 Function 描述了 bind、call、apply 等属性,以及 JavaScript 中所有函数值上的其他属性。
它还具有 Function 类型的值始终可以被调用的特殊属性;这些调用返回 any。
function test(f: Function): void {
const x = f(1, 2, 3) //const x: any
}
这是不安全的,如果函数可以接收任何类型的参数,且不需要返回值,那么建议用()=>void替换Function。
剩余形参和实参
剩余形参
除了使用可选参数或重载来制作可以接受各种固定参数计数的函数之外,我们还可以使用剩余参数定义接受无限数量参数的函数。
剩余参数出现在所有其他参数之后,并使用 … 语法
让我们来分析下面这段代码。其中m就是10,n就是1,2,3,4组成的数组。
返回值:n的所有数乘以m再返回一个数组,最终结果为:[10,20,30,40]
function multiply(m: number, ...n: number[]) {
return n.map((item) => item * m)
}
const a = multiply(10, 1, 2, 3, 4)
剩余实参
相反,我们可以使用扩展语法从可迭代对象(例如数组)中提供可变数量的参数。例如,数组的 push 方法接受任意数量的参数:
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
arr1.push(...arr2);
TypeScript 并不假定数组是不可变的。
下面这段代码就会报错,atan2是一个只接收两个参数的函数,TypeScript认为args是可变的,故TypeScript不允许将一个可变的数组传递给一个只能接收两个参数的函数。
const args = [8, 5]
const angle = Math.atan2(...args)
这种情况使用as const就可以解决了。
const args = [8, 5] as const
const angle = Math.atan2(...args)
参数解构
你可以使用参数解构来方便地将作为参数提供的对象解包到函数体中的一个或多个局部变量中。
和JavaScript中的解构差不多,就是多了类型的定义。
type ABC = { a: number; b: number; c: number }
function sum({ a, b, c }: ABC) {
console.log(a + b + c)
}
sum({ a: 1, b: 2, c: 3 })
函数的可赋值性
返回类型void
具有 void 返回类型 (type voidFunc = () => void) 的上下文函数类型,当实现时,可以返回任何其他值,但会被忽略。
因此,下列写法是有效的。
type voidFunc = () => void
const f1: voidFunc = () => {
return true
}
const f2: voidFunc = () => true
const f3: voidFunc = function () {
return true
}
const v1 = f1();//const v1: void
const v2 = f2();//const v1 = f1()
const v3 = f3();//const v1 = f1()
我们可以发现,尽管TypeScript允许我们在函数类型的返回类型为void的情况下返回其他值,但是我们返回的值是不生效的。
当字面量函数定义具有 void 返回类型时,该函数不得返回任何内容。
function f2(): void {
return true //不能将类型“boolean”分配给类型“void”
}
const f3 = function (): void {
return true //不能将类型“boolean”分配给类型“void”
}
注意,这里的void是我们手动加在函数上的,是函数的返回类型。上面的voidFunc,是我们定义的某种函数的类型。
上面是直接定义整个函数的类型,下面定义的是函数的返回值类型。