TypeScript 入门教程之函数
- 声明和调用函数的不同方式
- 签名重载
- 多态函数
- 多态类型别名
一、声明和调用函数的不同方式
1.1 声明方式
// 具名函数
function getName(name: string) {
return `hello ${name}`
}
// 函数表达式
let getName2 = function(name: string) {
return `hello ${name}`
}
// 箭头函数表达式
let getName3 = (name: string) => {
return `hello ${name}`
}
// 箭头函数表达式简写形式
let getName4 = (name: string) => `hello ${name}`
// 函数构造方法
let getName5 = new Function('name', 'return `hello ${name}`')
实参:调用函数时传给函数的数据
形参:声明函数时指定的运行函数所需的数据
1.2 函数可选参数和默认参数
// 可选参数
function log(code: number, message?: string) {
console.log(`${code} ${message || '未知'}`)
}
// 参数默认值
function log2(code: number, message = '未知') {
console.log(`${code} ${message}`)
}
// 显示注解默认参数类型
type Context = {
message?: string,
dateTime?: Date
}
function log3(code: string, context: Context = {}) {
console.log(`${code} ${context.message}`)
}
1.3 剩余参数
// 剩余参数
function sum(data: number[]) {
return data.reduce((total, n) => total + n, 0)
}
function sum2():number {
return Array.from(arguments).reduce((total, n) => total + n, 0)
}
这里 arguments 经 typescript 推导 n 和 total 的类型都是 any ,不安全,而且也会报错
sum2(1, 2, 3, 4, 5) // TS2554: Expected 0 arguments, but got 5.
因为声明 sum2 函数时没有指定参数,typescript 推导该函数不接受任何参数,所以会报 TypeError
这里要确保可变参数函数的类型安全,可以使用剩余参数(rest parameter)
// 剩余参数
// numbers arguments 都可以,说明其实 arguments 在 typescript 也是可用的,只是类型检查不过,更安全。
function sum2(...numbers: number[]):number {
return Array.from(numbers).reduce((total, n) => total + n, 0)
}
1.4 call、apply 和 bind
函数调用,除了使用圆括号()之外。还支持其它几种方式
sum([1, 2, 3, 4, 5])
sum.apply(null, [1, 2, 3, 4, 5])
sum.call(null, [1, 2, 3, 4, 5])
sum.bind(null, [1, 2, 3, 4, 5])()
apply 为函数内部的 this 绑定一个值,然后展开第二个参数,作为参数传给要调用的函数。call 的用法类型,不过是按顺序应用参数,不做展开。
bind() 差不多,也为函数的 this 和 参数绑定值。不过,bind 并不调用函数,而是返回一个新函数,让你通过()、.call 或 .apply 调用,而且可以再传入参数,绑定到尚未绑定值的参数上
tsc 为了安全使用 .call 、.apply 和 .bind,要在 tsconfig.json 中启用 strictBindCallApply 选项(启用 strict 模式后自动启用该选项)
1.5 注解 this 的类型
javascript 中的每个函数都有自己 this 变量,而不局限于类中的方法。以不同的方式调用函数,this 的值也不同,这极易导致代码脆弱,难以理解。
很多团队禁止在类方法以外使用 this 。如果你也想这么做,启用 TSLint 的 no-invalid-this 规则
在 typescript 中,如果函数使用 this ,请在函数第一个参数中声明 this 的类型,这样每次调用函数时, typescript 将确保 this 的确是你预期的类型。 this 不是常规的参数,而是保留字,是函数签名的一部分:
function fancyDate(this: Date) {
return `${this.getDate()}/${this.getMonth()}/${this.getFullYear()}`
}
fancyDate() // TS2684: The ‘this’ context of type ‘void’ is not assignable to method’s ‘this’ of type ‘Date’.
这样就避免了 javascript this 丢失引起的错误, typescript 参数类型断言在编码阶段就发现了。
如果想强制显示注解函数中 this 的类型,在 tsconfig.json 中启用 noImplicitThis 设置。strict 模式包括 noImplicitThis,如果已经启用该模式,就不要设置 noImplicitThis了
注意,noImplicitThis 不强制要求为类或对象的函数注解 this。
二、签名重载
2.1 调用签名
function sum(a: number, b: number): number {
return a + b
}
在 typescirpt 中可以像下面这样表示该函数的类型:
(a: number, b: number): number
这是 typescript 表示函数类型的句法,也称调用签名。注意,调用签名的句法与箭头函数十分相似,这是有意为之的。如果把函数做为参数传给另一个函数,或者作为其他函数的返回值,就要使用这样的句法注解类型。
讨论静态类型语言时,人们常使用“类型层面”和“值层面”两个术语,所以我们有必要弄清具体含义。
“类型层面代码”指只有类型和类型运算符的代码。而其他的都是“值层面代码”。一个简单的判断标准是,如果是有效的 javascript 代码,就是值层面代码;如果是有效的 typescript 代码,但不是有效的 javascript 代码,那就是类型层面代码。
2.2 函数类型重载
// 简写型调用签名
type Log = (code: number, message?: string) => void
// 完整型调用签名
type Log = {
(code: number, message?: stirng): void
}
重载函数,有多个调用签名的函数
多数编程语言中,声明函数时一旦指定了特定的参数和返回类型,就只能使用相应的参数调用函数,而且返回值的类型始终如一。但是 javascript 的情况却非如此,因为他是一门动态语言,势必需要以多种方式调用一个函数的方法。不仅如此,而且有时输出的类型取决于输入的参数类型。
typescript 也支持动态重载函数声明,而且函数的输出类型取决于输入类型,这一切都得益于 typescript 静态类型系统。你可能以为这个语言特性是理所当然的,但其实只有先进的类型系统才有。
type Reserve = {
(from: Date, to: Date, destination: string): void
(from: Date, destination: string): void
}
let reserve: Reserve = (from: Date, to: Date | string, destination?: string) => {
if (to instanceof Date && destination !== undefined) {
} else if (typeof to === 'string') {
}
}
type CreateElement = {
(tag: 'a'): HTMLAnchorElement,
(tag: 'canvas'): HTMLCanvasElement,
(tag: 'table'): HTMLTableElement,
(tag: string): HTMLElement
}
三、多态
下面我们来实现一个迭代数组,筛选符合条件的元素,在 javascript 中,可以这样实现
function filter(array, f) {
let result = []
for (let i = 0; i < array.length; i++) {
let item = array[i]
if (f(item)) {
result.push(item)
}
}
return result
}
const a1 = filter(['1', '2', '3', '4'], _ => _ < 3)
typescript 完整类型签名如何实现
type Filter = {
(array: number[], f: (item: number) => boolean): number[]
(array: string[], f: (item: number) => boolean): string[]
(array: object[], f: (item: number) => boolean): object[]
}
let filter: Filter = (array, f) => {
let result = []
for (let i = 0; i < array.length; i++) {
let item = array[i]
if (f(item)) {
result.push(item)
}
}
return result
}
乍一看这个实现没有什么问题,可用做迭代对象数组就会报错了
let names = [
{
name: 'react.js'
},
{
name: 'angular.js'
},
{
name: 'vue.js'
}
]
let result = filter(names, _ => _.name.startsWidth('b'))
TS2339: Property ‘name’ does not exist on type ‘number’.
这里我们类型别名里告诉 typescript ,传给 filter 的可能是数字数组、字符串数组或对象数组。这里传入的是对象数组,object 无法描述对象的结构。因此尝试访问数组中某个对象的属性时, typescript 抛出错误,毕竟我们没有指明该对象的具体结构。
泛型参数:在类型层面施加约束的点位类型,也称多态类型参数
type Filter = {
<T>(array: T[], f:(item: T) => boolean): T[]
}
typescript 推导出 T 的具体类型后,将把 T 出现的每一处替换为推导出的类型。T 就像是一个占位类型,类型检查器将根据上下文填充具体的类型。T 把 Filter 的类型参数化了,因此才称其为泛型参数。
泛型参数使用奇怪的尖括号 <> 声明。尖括号的位置限定泛型的作用域, typescript 将确保当前作用域中相同的泛型参数最终都绑定同一个具体类型。鉴于这个示例中尖括号的位置, typescript 将在调用 filter 函数时为泛型 T 绑定具体类型。
T 就是一个类型名称,如果愿意,可以使用任何其他名称,例如A、B、C,按照惯例,人们经常使用单个大写字母,从 T 开始,依次使用 U、V、W等。
四、多态类型别名
type MyEvent<T> = {
target: T,
type: string
}
类型别名中只有这一个地方可以声明泛型,即紧随类型别名的名称之后,赋值运算符(=)之前
MyEvent 的 target 属性指向触发事件的元素,比如一个 <button />
type ButtonEvent = MyEvent<HTMLButtonElement>
let myEvent: MyEvent<HTMLButtonElement | null> = {
target: document.querySelector('#myButton'),
type: 'click'
}