在js中函数是一等对象,我们可以像对象那样使用函数,可以复制给变量,可以作为参数传递,返回值,赋值给对象的原型,赋值属性,读取属性等
function add(a:number,b:number){
return a+b
}
通常我们会显式注解函数的参数(上例中的a,b)。ts能推导出函数体中的类型,但是多数情况下无法推到出参数的类型,只在少数情况下能够根据上下文推导出参数的类型。返回类型能够推导出来,不过也可以显示注解(有利于阅读):
function add(a:number,b:number):number{
return a+b
}
上面使用的具名函数,下面还有几种
// 具名函数
function add(a:number,b:number):number{
return a+b
}
// 函数表达式
let greet = function(name:string):string{
return "hello"+name
}
// 箭头函数表达式
let greett = (name:string):string=> 'hello'+name
// 函数构造方法 不安全
let greettt = new Function("name","return 'hello'+name")
相关术语 形参:声明函数时指定的运行函数所需的数据 实参:调用函数时才给函数的数据
function log(message:string,userId?:string){
let time = new Date().toLocaleTimeString()
console.log(time,message,userId||"Not signed ini");
}
log("PageLoaded")
log("Usersigned in ","dddde4")
function log(message:string,userId="not signed in"){
let time = new Date().toISOString()
console.log(time,message,userId);
}
log("User click ed On a bootn","ddd33")
// 显示注解默认参数的类型,
type Context = {
appId?:string,
userId?:string
}
function log(message:string,context:Context={}){
let time = new Date().toISOString()
console.log(time,message,context.userId);
}
log("egege",{appId:"eeee","userId":"3533g"})
function sumVariadicSafe(start:number,...numbers:number[]):number{
return numbers.reduce((total,n)=>total+n,start)
}
console.log(sumVariadicSafe(1,2,3,4) );
同JavaScript
js中的每个函数都有this变量,而不局限于类中的方法。以不同的方式调用函数,this的值也不同,这极易导致代码脆弱,难以理解。
鉴于此 很多团队禁止在类方法以外使用this。
this之所以这么脆弱,与他的赋值方式有关,一般来说,this的值为调用方法时位于点号左侧的对象。例如
let x = {
a(){
return this
}
}
console.log(x.a()==x);// true
// 但是,倘若在调用a之前重新赋值了,结果将发生变化。
let a = x.a
console.log(a());// undefined
function fancyDate(this:Date){
return `${this.getDate()}/${this.getMonth()}`
}
console.log(fancyDate.call(new Date));
生成器函数,时生成一系列值的便利方式。生成器的使用方法可以精确控制生成什么值。生成器时惰性的,只在使用方要求的时候才计算下一个值,鉴于此,可以利用生成器实现一些其他方式难以实现的操作,例如生成无穷列表
生成器的用法如下:
function* createFibonacciGenerator():IterableIterator {
let a = 0;
let b = 1;
console.log("111");
while (true) {
yield a;
[a, b] = [b, a + b]
}
}
let er = createFibonacciGenerator();// 返回一个可迭代的迭代器,内部代码尚未执行
console.log(er.next());// 内部代码开始执行
迭代器是生成器的相对面:生成器生成一些列值的方式,而迭代器是使用这些值的方式
可迭代对象 有Symbol.iterator属性的对象,而且改属性的值是一个函数,返回一个迭代器
迭代器 定义有next方式的对象,该方法返回一个具有value和done属性的对象(结果对象)
创建生成器(调用createFibonacciGenerator),得到的值既是可迭代对象,可以是迭代器,称为可迭代的迭代器,因为该值既有Symbol.iterator属性,也有next方法。
目前我们学习了如何注解函数的参数和返回值的类型。下面说一下函数自身的完整类型。
再看一下前面定义的sum函数
function sum(a:number,b:number):number{
return a+b;
}
sum的类型是什么呢,由于sum是一个函数,所以他的类型是:Function
多数时候Function并不是我们想要的最终结果。我们知道,object能描述所有对象,类似的,Function也可以表示所有函数,但是并不能表示函数的具体类型。 在Typescript中可以像下面这样表示该函数的类型:
(a:number,b:number)=>number
这是Typescript表示函数类型的句法,也称调用签名(或叫类型签名)
*注意:调用签名(类型签名)的句法与箭头函数十分相似,这是有意为之。如果把函数作为参数,传给另一个函数,或者作为其他函数的返回值,就要使用调用签名(类型签名)句法注解类型 *
函数的类型签名只包含类型层面的代码,即只有类型,没有值。因此,函数的调用签名可以表示参数的类型,this的类型,返回值的类型,剩余参数的类型和可选参数的类型,但是无法表示默认值(因为默认值是值,不是类型)。调用签名没有函数的定义体,无法推导出返回类型,所以必须显示注解。
枚举既生成 类型 也生成 值
命名空间只存在于值层面
// 类型别名声明一个类型签名
type Log = (message:string,userId?:string)=>void
let log:Log = (message,userId="not signed in")=>{
let time = new Date().toISOString()
console.log(time,message,userId);
}
根据上面的代码
由于我们把log的类型声明为Log,所以Typescript能从上下文中推导出message的类型为string。这是Typescript类型推导的一个强大特性,称为 上下文类型推断
使用上下文推导的情形:回调函数。
下面声明一个函数times,他调用n次回调函数f,每次把当前索引传给f:
function times(
f:(index:number)=>void,
n:number
){
for(let i=0;i
调用times时传给times的函数如果是在行内声明的,无需显示注解函数的类型
times(n=>console.log(n),4)
Typescript能从上下文中推导出n时一个数字,因为在times的签名中,我们声明f的参数index是一个数字。Typescript能推导n就是那个参数,那么该参数的类型必然就是number。
*注意:如果f不是在行内声明的,Typescript则无法推导出他的类型 *
function(n){
console.log(n)
}
times(f,4)// 报错 无法推导出 n的类型 作为any
前一节使用的函数类型句法,即type Fn = (…) => …,其实是简写型调用签名。如果愿意,可以使用完整形式。仍以Log为例
type Log = (message:string,userId?:string)=>void
// 完整写法
type Log = {
(message:string,userId?:string):void
}
这两种写法完全等效,只是使用的句法不同。
那么何时使用完整型调用签名,何时使用简写形式呢?
较为复杂的函数用完整调用签名
首先是重载的函数类型的情况
重载函数 有多个调用签名的函数
JavaScript是一门动态语言,可以用多种方式调用一个函数(没有标准的重载)
Typescript支持动态重载函数声明,而且函数的输出类型取决于输入类型,这一切得益于Typescript的类型系统,只有先进的类型系统才有。
我们可以使用重载函数签名设计十分具有表现力的API.
type Reserve = {
(from:Date,to:Date,destination:string):Reservation
}
let reserve:Reserve = (from,to,destination)=>{
...
}
调用reserveAPI传入 开始日期,结束日期,目的地
可以修改一下,让这个API支持单程旅行:
class Reservation {}
type Reserve = {
(from:Date,to:Date,destionation:string):Reservation
(from:Date,destionation:string):Reservation
}
let reserve:Reserve = (from,to,destionation)=>{return new Reservation()} // 报错
上面的报错的原因是Typescript的调用签名机制造成的,如果为函数f声明多个重载的签名,在调用签名看来,f的类型是各签名的并集。但是在实现f时,必须一次实现整个类型组合。实现f时,我们要自己设法声明组合后的调用签名,Typescript无法自动推导。对Reserve示例来说,我们可以像下面这样更新reserve函数:
// 更新写法
let reserve: Reserve = (
from: Date,
toOrDestionation: Date | string,
destionation?: string
) => { return new Reservation }
reserve(new Date, new Date, "33")
reserve(new Date, "tt")
上面我们
- 声明了两个重载的函数签名
- 自己动手组合两个签名(即自行计算Signature1|Signature2的结果),实现声明的签名。注意,组合后的签名对调用reserve的函数时不可见的。
- 在是哦刚方看来,Reserve的签名是:
type Reserve = {
(from:Date,to:Date,destionation:string):Reservation
(from:Date,destionation:string):Reservation
}
注意,类型声明中没有组合后的签名
// 错误
type Reserve = {
(from:Date,to:Date,destionation:string):Reservation
(from:Date,destionation:string):Reservation
(from:Date,toOrDestionation:Date|string,destionation?:string):Reservation
}
由于reserve可以通过两种方式调用,因此实现reserve时要像Typescript证明你检查过了调用方式:
let reserve: Reserve = (
from: Date,
toOrDestionation: Date | string,
destionation?: string
) => {
if(toOrDestionation instanceof Date && destionation!==undefined){
// 单次
return new Reservation
}else if(typeof toOrDestionation === 'string'){
// 往返
return new Reservation
}
return new Reservation
}
例如:DOM API中的createElement用于新建html元素,其参数为表示html标签的字符串,返回值为对应类型的html元素,Typescript内置了每个html元素的类型,例如
type createElement = {
(tag:'a'):HTMLAnchorELement
(tag:'canvas'):HTMLCanvasElement
(tag:'table'):HTMLTableElement
(tag:string):HTMLElement
}
let createElement:CreateElement = (tag:string):HTMLElement => {
//
}
完整的类型签名并不只局限于用来重载调用函数的方式,还可以描述函数的属性,由于JavaScript函数是可调用的对象,因此我们可以为函数赋予属性,例如
type WarnUser = {
(warning:string):void
wasCalled:boolean
}
let warnUser:WarnUser = (warning:string){
if(warnUser.wasCalled){
return
}
warnUser.wasCalled = true
alert(warning)
}
warnUser.wasCalled = false
Typescript足够智能,他能发现,声明warnUser函数时没有给wasCalled赋值,但是随后就赋值了
目前,我们的都在讨论类型的用法和用途,以及使用具体类型的函数,什么是具体类型呢?,目前我们见到的每个类型都是具体类型。
使用具体类型的前提是明确知道需要什么类型,并且想确定传入的确实是那个类型,但是,有时候事先并不知道需要什么类型,不想限制函数只能接受某些个类型
举个例子
function filter(array,f){
let result = []
for(let i=0;i_<3) // [1,2]
从中我们可以提取出filter函数的完整类型签名,类型先用unknown代替:
type Filter = {
(array:unknown,f:unknown)=>unknown[]
}
// 下面我们尝试填入具体的类型,比如number
type Filter = {
(array:number[],f:(item:number)=>boolean):number[]
}
这里,数组元素的类型可以为number,不过filter函数的作用应该更广泛,可以筛选数字数组,字符串数组,对象数组等。我们编写的签名可以处理数字数组,但是不能处理元素为其他类型的数组。下面通过重载,让filter函数也能处理字符串数组:
type Filter = {
(array:number[],f:(item:number)=>boolean):number[]
(array:string[],f:(item:string)=>boolean):string[]
}
// 处理对象数组
type Filter = {
(array:number[],f:(item:number)=>boolean):number[]
(array:string[],f:(item:string)=>boolean):string[]
(array:object[],f:(item:object)=>boolean):object[]
}
咋一看可能没啥问题,可是用着就会遇到问题
let names = [
{firstName:'beth'},
{firstName:'redrun'},
{firstName:'eed'}
]
let result = filter(
names,
_=>_.firName.startsWith('b')
)// error Propery 'firstName' does not exist on typ 'object'
我们告诉Typescript,传给filter的可能是数字数组,字符串数组,或对象数组。这里传入的时对象数组,可是你记得吗,object无法描述对象的结构,因此,尝试访问数组中某个对象的属性时,Typescript抛出错误,毕竟我们没有指明该对象的具体结构
泛型(generic type)参数 在类型层面施加约束的占位类型,也称多态类型参数!
仍以filter的函数为例,使用泛型参数T重写后得到的声明如下
type Filter ={
(array:T[],f:(item:T)=>boolean):T[]
}
这样做的意思是,“filter函数使用一个泛型参数T,可是我们事先不知道具体是什么类型,typescript在调用filter函数时,自动推导T的类型。在推导出T的类型之后,将T出现的每一处替换为推导出的类型。T就像是一个站位类型,类型检查器将根据上下文填充具体的类型。T把Filter的类型参数化了,因此才称为泛型参数。
泛型参数使用奇怪的尖括号<>声明(你可以把尖括号理解为type关键字,只不过声明的泛型)。尖括号的位置限定泛型的作用域(只有少数几个地方可以使用尖括号哦).Typescript将确保当前作用域中相同的泛型参数最终都绑定同一个具体类型。鉴于这个示例中尖括号的位置,Typescript将在调用filter函数时为泛型T绑定具体类型。而为T绑定哪一个具体类型,取决于调用filter函数时传入的参数。在一堆尖括号中可以声明任意个以逗号分隔的泛型参数。
我们知道,每次调用函数时都要重新绑定函数的参数,类似地,每次调用filter都会重新绑定T:
type Filter = {
(array:T[],f:(item:T)=>boolean):T[]
}
let filter:Filter = (array,f)=> {///}
filter(['a','b'],_=>_!== 'b')// 推断为string
filter([1,2,3],_=>_>2)// 推断为number
let names = [
{
firstName:"bet",
firstName:'red',
firstName:'run',
}
]
filter(names=>_=>_.firstName.startWith('b'))
泛型让函数的功能更具一般性,比接受具体类型的函数更强大。泛型可以理解为一种约束。我们知道,把函数的参数注解为n:number,参数n的值就被约束为number类型。同样,泛型T把T所在位置的类型约束为T绑定的类型。
泛型也可以在类型别名,类和接口中使用。本书将大量使用泛型,在介绍相关话题时再详细说明,只要可能就应该使用泛型,这样写出的代码更具一般性,可重复使用,并且简单扼要
声明泛型的位置不仅限定泛型的作用域,还决定Typescript什么时候为泛型绑定具体的类型
type Filter = {
(array:T[],f:(item:T)=>boolean):T[]
}
let filter:Filter = (array,f)=>{} // error generic type require 1 type argument
type OtherFilter = Filter // error
let filter:Filter = (array,f)=> //
一般来说,Typescript在使用泛型时为泛型绑定具体类型:对函数来说,在调用函数时,对类来说,在实例化类时:对类型别名和接口来说,在使用别名和实现结构时。
只要是在Typescript支持声明调用签名的地方,都有办法在签名中加入泛型:
// 完整调用签名 作用域子在单个标签中
type Filter = {
(array:T[],f:(item:T)=>boolean):T[]
}
let filter:Filter = //
// 完整调用签名 作用域覆盖全部标签
type Filter = {
(array:T[],f:(item:T)=>boolean):T[]
}
let filter:Filter = //
// 简写标签
type Filter = (array:T[],f:(item:T)=>boolean)=>T[]
let filter:Filter = //
// 简写标签
type Filter = (arrray:T[],f:(item:T)=>boolean) =>T[]
let filter:Filter = //
// 具名函数调用标签
function filter(array:T[],f:(item:T)=>boolean):T[]{
}
// 使用两个泛型
function map(array:T[],f:(item:T)=>U):U[]{
let result = []
for(let i=0;i
标准库中的filter和map函数
interface Array{
filter(
callbackfb:(value:T,index:number,array:T[])=>any,thisArg?:any
):T[]
map(
callbackfn:(value:T,index:number,array:T[])=>U,thisArg?:any
):U[]
}
多数情况下,Typescript能自动推导出泛型。例如调用前面编写的map函数,经过Typescript推导,T的类型是string,U的类型是boolean
function map(array:T[],f:(item:T)=>U):U[]{
//
}
map(
['a','b'],
_=>_=== 'a'
)
可以显示注解泛型
// 全部都要实现
map(
['a'],
_=>_ === 'a'
)
let promise = new Promise(resolve=>{
resolve(45)
})
promise.then(result=> // number)
type Filter = {
(array:T[],f:(item:T)=>boolean):T[]
}
// 只读数组
type A = Readonly<[number,string]>
上面的例子已经涉及泛型别名。
我们来定义一个MyEvent类型,描述DOM事件,例如click或mousedown:
type MyEvent = {
target:T
type:string
}
注意,在类型别名中,只有这一个地方可以声明泛型,即紧随类型别名的名称之后,赋值运算符(=)之前。
MyEvent的target属性指向触发事件的元素,比如一个,一个
type ButtonEvent = MyEvent
使用MyEvent这样的泛型时,必须显示绑定类型参数,typescript无法自行推导:
let myEvent:MyEvent = {
target:document.querySelector("button"),
type:'click'
}
我们可以使用MyEvent构建其他类型,比如说TimedEvent.绑定TimedEvent中的泛型T时,Typescript同时还会把他绑定给MyEvent:
type TimedEvent = {
event:MyEvent
from:Date
to:Date
}
泛型别名也可以在函数的签名中使用。Typescript为T绑定类型时,还会自动为MyEvent绑定:
function triggerEvent(event:MyEvent):void{
// ...
}
triggerEvent({
target:document.querySelector("button"),
type:'mouseover'
})
下面逐步说明这里涉及的操作
二叉树 一种树结构 由节点构成 一个节点有一个值,最多可以指向两个子节点 节点有两种类型:叶节点(没有子节点)和内节点(至少有一个子节点)
有时候,说“这个是泛型T,那个也是泛型T”还不够,我们还想表达“类型U至少应为T”,即为U设定一个上限。
为什么要这么做?假设我们在实现一个二叉树,把节点分为三类
首先声明各种节点的类型
type TreeNode = {
value:string
}
type LeafNode = TreeNode & {
isLeaf:true
}
type InnerNode = TreeNode & {
children:[TreeNode]|[TreeNode,TreeNode]
}
上面代码的意思是,TreeNode是一个对象,只有一个属性value;LeafNode类型具有TreeNode的所有属性,外加一个isLeaf属性,其值始终为true;InnerNode也具有TreeNode的全部属性,另外还有一个children属性,执行一个或两个子节点。
接下来,编写mapNode函数,映射传入的TreeNode的值,返回一个新的TreeNode,我们希望下面这样使用mapNode函数:
type TreeNode = {
value:string
}
type LeafNode = TreeNode & {
isLeaf:true
}
type InnerNode = TreeNode & {
children:[TreeNode]|[TreeNode,TreeNode]
}
let a:TreeNode = {value:"a"}
let b:LeafNode = {value:'b',isLeaf:true}
let c:InnerNode = {value:'c',children:[b]}
function mapNode(node:T,f:(value:string)=>string):T{
return {
...node,
value:f(node.value)
}
}
let a1 = mapNode(a,_=>_.toUpperCase());
let b1 = mapNode(b,_=>_.toUpperCase())
console.log(a1);
console.log(b1);
为什么要这样声明T呢?
- 如果只输入T(没有extends TreeNode),那么mapNode会抛出编译时错误,因为这样不能从T类型的node中安全读取node.value(试想传入一个数字的情况)
- 如果根本不用T,把mapNode声明为(node:TreeNode,f:(value:string)=>string)=>TreeNode,那么映射节点后将丢失信息:a1,b1和c1都只是TreeNode
声明T extends TreeNode,输入节点的类型(TreeNode,LeafNode或InnerNode)将得到保留,映射后类型也不变。
上面我们只为T施加了一个类型约束,即T至少为TreeNode。那么,如果需要多个类型约束呢? 方法是扩展多个约束的交集(&):
// 有侧边
type HasSides = {numberOfSides:number}
// 侧边有长度
type SidesHaveLength = {sideLength:number}
function logPerimeter<
Shape extends HasSides & SidesHaveLength
>(s:Shape):Shape{
console.log(s.numberOfSides);
return s
}
type Square = HasSides & SidesHaveLength
let square:Square = {numberOfSides:4,sideLength:3};
// 打印周长
logPerimeter(square); //
借助受限的多态还可以模拟变长参数函数(可接受任意个参数的函数)。
下面我们自己实现一版JavaScript内置的call函数(回顾一下,call函数接受一个函数和不定量的参数,这些参数将传给第一个参数指定的函数),以此为例。我们这样定义call函数,unknown类型稍后再替换:
function call(
f:(...args:unknown[])=>unknown,
...args:unknown[]
):unknown{
return f(...args)
}
function fill(length:number,value:string):string[]{
return Array.from({length},()=>value)
}
call(fill,10,'a')
下面来替换unknown,我们想表达的约束是:
因此,需要两个类型参数:T,即参数数组;R,任意类型的返回值。下面把类型填进去:
// 有问题
// function call(
// f:(...args:T)=>R,
// ...args:T
// ):R{
// return f(...args)
// }
// function fill(length:number,value:string):string[]{
// return Array.from({length},()=>value)
// }
// call(fill,10,'a')
// 有问题 T被推断为一个(any|any)
// function call(
// f:(...args:T[])=>R,
// ...args:T[]
// ):R{
// return f(...args)
// }
// function fill(length:number,value:string):string[]{
// return Array.from({length},()=>value)
// }
// call(fill,10,'a')
// 没问题
// function call(
// f:(...args:[T,V])=>R,
// ...args:[T,V]
// ):R{
// return f(...args)
// }
// function fill(length:number,value:string):string[]{
// return Array.from({length},()=>value)
// }
// call(fill,10,'a')
// 没问题 被推断为 元组类型
function call(
f:(...args:T)=>R,// 剩余参数将元组解析为多个参数
...args:T
):R{
return f(...args)
}
function fill(length:number,value:string):string[]{
return Array.from({length},()=>value)
}
call(fill,10,'a')
为什么要这么做呢?
这样,调用call时,Typescript知道返回值具体是什么类型,而且如果传入的参数有误,Typescript将报错。
type A = [string,number];
let a:A = ["a",1];
type B = (string|number)[]
let b:B = ["a",123]
function test(...a:A){
console.log(a);
}
test("1",2)
call(fill,10,'a')
call(fill,10)// 报错
call(fill,10,'a','b');// 报错
Typescript在为剩余参数推到类型时利用了一项改进推导结果的技术,详见6.4.1节。
函数的参数可以指定默认值,类似的,泛型参数也可以指定默认类型。
type MyEvent = {
target:T
type:string
}
// 为了提供便利,默认绑定一个类型
type MyEvent = {
target:T
type:string
}
此外,我们还可以利用前几节所学,为T设置限制,确保T是一个HTML元素:
type MyEvent = {
target:T
type:string
}
注意:与函数可选参数一样,有默认类型的泛型要放在没有默认类型的泛型后面
强大的类型系统自有强大的功能。编写Typescript时候,往往发现自己“受类型的指引”。理所当然,我们称之为类型驱动开发(type-driven development).
类型驱动开发 先草拟类型签名,然后填充值的编程风格
静态类型系统的要义是约束表达式的值可以为什么类型。类型系统的表现力越强,提供的关于表达式中的信息越多。使用表现力强的类型系统注解函数,通过函数的类型签名就能知晓关于函数的多数信息:
function map(array:T[],f:(item:T)=>U):U[]{
//...
}
即使以前没有使用过map也能通过签名直观的看出map的作用。
编写Typescript程序时,先定义函数的类型签名,即“受类型的指引”,然后在具体实现。先在类型层面规划程序可以确保在着手实现之前程序的整体合理性。
目前为止,我们都是反着做的,即受实现的指引,然后再推演类型。现在我们知道在Typescript中如何编写函数和注解函数的类型了,那就可以换种模式,先规划类型,然后再填充细节。