当前位置: 首页 > news >正文

Typescript第四章 函数(声明和调用,注解参数类型,多态,类型别名,泛型)

第四章 函数

  • ts中声明和调用函数的不同方式
  • 签名重载
  • 多态函数
  • 多态类型声明

4.1 声明和调用函数

在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")

相关术语 形参:声明函数时指定的运行函数所需的数据 实参:调用函数时才给函数的数据

4.1.1 可选和默认的参数

<!-- 可选参数 -->
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"})

4.1.2 剩余参数

function sumVariadicSafe(start:number,...numbers:number[]):number{return numbers.reduce((total,n)=>total+n,start)
}
console.log(sumVariadicSafe(1,2,3,4) );

4.1.3 call,apply,bind

同JavaScript

4.1.4 注解this的类型

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

4.1.4 生成器函数

生成器函数,时生成一系列值的便利方式。生成器的使用方法可以精确控制生成什么值。生成器时惰性的,只在使用方要求的时候才计算下一个值,鉴于此,可以利用生成器实现一些其他方式难以实现的操作,例如生成无穷列表

生成器的用法如下:

​
function* createFibonacciGenerator():IterableIterator<number> {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());// 内部代码开始执行

4.1.6 迭代器

迭代器是生成器的相对面:生成器生成一些列值的方式,而迭代器是使用这些值的方式

可迭代对象 有Symbol.iterator属性的对象,而且改属性的值是一个函数,返回一个迭代器

迭代器 定义有next方式的对象,该方法返回一个具有value和done属性的对象(结果对象)

创建生成器(调用createFibonacciGenerator),得到的值既是可迭代对象,可以是迭代器,称为可迭代的迭代器,因为该值既有Symbol.iterator属性,也有next方法。

4.1.7 调用签名

目前我们学习了如何注解函数的参数和返回值的类型。下面说一下函数自身的完整类型。

再看一下前面定义的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
  • 不必再次注解参数的类型,因为在定义Log类型的时候,已经注解了message的类型为string.这里不用再次注解,Typescript,能从Log中推导出来。
  • 为userId设置一个默认值。userId的类型可以从Log的签名中获取,但是默认值却不得而知,因为Log是类型,不包含值。
  • 无需再次注解返回类型,因为在Log类型中已经声明为void

4.1.8 上下文类型推断

由于我们把log的类型声明为Log,所以Typescript能从上下文中推导出message的类型为string。这是Typescript类型推导的一个强大特性,称为 上下文类型推断

使用上下文推导的情形:回调函数。

下面声明一个函数times,他调用n次回调函数f,每次把当前索引传给f:

function times(f:(index:number)=>void,n:number
){for(let i=0;i<n;i++){f(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

4.1.9 函数类型重载

前一节使用的函数类型句法,即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
}
<!-- 注解实现的参数时候,要把该参数在createElement重载签名中的所有类型组合在一起,得到'a'|'canvas'|'table'|string 由于前三个字符串字面量类型是string的子类型,所以可以吧最终结果简化为string-->
let createElement:CreateElement = (tag:string):HTMLElement => {//
}

完整的类型签名并不只局限于用来重载调用函数的方式,还可以描述函数的属性,由于JavaScript函数是可调用的对象,因此我们可以为函数赋予属性,例如

type WarnUser = {(warning:string):voidwasCalled:boolean
}
let warnUser:WarnUser = (warning:string){if(warnUser.wasCalled){return}warnUser.wasCalled = truealert(warning)
}
warnUser.wasCalled = false

Typescript足够智能,他能发现,声明warnUser函数时没有给wasCalled赋值,但是随后就赋值了

4.2多态

目前,我们的都在讨论类型的用法和用途,以及使用具体类型的函数,什么是具体类型呢?,目前我们见到的每个类型都是具体类型。

  • boolean
  • string
  • Date[]
  • {a:number}|{b:string}
  • (numbers:number[])=>number

使用具体类型的前提是明确知道需要什么类型,并且想确定传入的确实是那个类型,但是,有时候事先并不知道需要什么类型,不想限制函数只能接受某些个类型

举个例子

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
}
filter([1,2,3],_=>_<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 ={<T>(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 = {<T>(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绑定的类型。

泛型也可以在类型别名,类和接口中使用。本书将大量使用泛型,在介绍相关话题时再详细说明,只要可能就应该使用泛型,这样写出的代码更具一般性,可重复使用,并且简单扼要

4.2.1 什么时候绑定泛型

声明泛型的位置不仅限定泛型的作用域,还决定Typescript什么时候为泛型绑定具体的类型

type Filter<T> = {(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<number> = (array,f)=> //
​

一般来说,Typescript在使用泛型时为泛型绑定具体类型:对函数来说,在调用函数时,对类来说,在实例化类时:对类型别名和接口来说,在使用别名和实现结构时。

4.2.2 可以在什么地方声明泛型

只要是在Typescript支持声明调用签名的地方,都有办法在签名中加入泛型:

// 完整调用签名 作用域子在单个标签中
type Filter = {<T>(array:T[],f:(item:T)=>boolean):T[]
}
let filter:Filter = //
​
// 完整调用签名 作用域覆盖全部标签
type Filter<T> = {(array:T[],f:(item:T)=>boolean):T[]
}
let filter:Filter<number> = //
​
// 简写标签
type Filter = <T>(array:T[],f:(item:T)=>boolean)=>T[]
let filter:Filter = //
​
// 简写标签
type Filter<T> = (arrray:T[],f:(item:T)=>boolean) =>T[]
let filter:Filter<string> = //
​
// 具名函数调用标签
function filter<T>(array:T[],f:(item:T)=>boolean):T[]{
}
// 使用两个泛型
function map<T,U>(array:T[],f:(item:T)=>U):U[]{let result = []for(let i=0;i<arrar.length;i++){result[i] = f(array[i])}return result
}
​
标准库中的filter和map函数
interface Array<T>{filter(callbackfb:(value:T,index:number,array:T[])=>any,thisArg?:any):T[]map<U>(callbackfn:(value:T,index:number,array:T[])=>U,thisArg?:any):U[]
}

4.2.3 泛型推导

多数情况下,Typescript能自动推导出泛型。例如调用前面编写的map函数,经过Typescript推导,T的类型是string,U的类型是boolean

​
function map<T,U>(array:T[],f:(item:T)=>U):U[]{//
}
map(['a','b'],_=>_=== 'a'
)

可以显示注解泛型

// 全部都要实现
map<string,boolean>(['a'],_=>_ === 'a'
)
​
let promise  = new Promise<number>(resolve=>{resolve(45)
})
promise.then(result=> // number)
​

4.2.4 泛型别名

type Filter = {<T>(array:T[],f:(item:T)=>boolean):T[]
}
// 只读数组
type A = Readonly<[number,string]>

上面的例子已经涉及泛型别名。

我们来定义一个MyEvent类型,描述DOM事件,例如click或mousedown:

type MyEvent<T> = {target:Ttype:string
}

注意,在类型别名中,只有这一个地方可以声明泛型,即紧随类型别名的名称之后,赋值运算符(=)之前。

MyEvent的target属性指向触发事件的元素,比如一个,一个

等,例如,可以像下面这样描述一个按钮事件:

type ButtonEvent = MyEvent<HTMLButtonElement>

使用MyEvent这样的泛型时,必须显示绑定类型参数,typescript无法自行推导:

let myEvent:MyEvent<HTMLButtonElement|null> = {target:document.querySelector("button"),type:'click'
}

我们可以使用MyEvent构建其他类型,比如说TimedEvent.绑定TimedEvent中的泛型T时,Typescript同时还会把他绑定给MyEvent:

type TimedEvent<T> = {event:MyEvent<T>from:Dateto:Date
}

泛型别名也可以在函数的签名中使用。Typescript为T绑定类型时,还会自动为MyEvent绑定:

function triggerEvent<T>(event:MyEvent<T>):void{// ...
}
triggerEvent({target:document.querySelector("button"),type:'mouseover'
})

下面逐步说明这里涉及的操作

  1. 调用triggerEvent时传入一个对象
  2. 根据函数的签名,Typescript认定传入的参数类型必为MyEvent。Typescript还发现定义MyEvent时声明的类型为{target:T,type:string}。
  3. Typescript发现传给该对象target字段的值为document.querySelect(“button”)。这意味着,T必定为HTMLButtonElement类型,即HTMLButtonElement|null。
  4. Typescript检查全部代码,把T出现的每一处替换HTMLButtonElement|null。
  5. Typescript确认所有类型都满足可赋值性,确保代码的类型是安全的。

4.2.5 受限的多态

二叉树 一种树结构 由节点构成 一个节点有一个值,最多可以指向两个子节点 节点有两种类型:叶节点(没有子节点)和内节点(至少有一个子节点)

有时候,说“这个是泛型T,那个也是泛型T”还不够,我们还想表达“类型U至少应为T”,即为U设定一个上限。

为什么要这么做?假设我们在实现一个二叉树,把节点分为三类

  • 常规的TreeNode
  • LeafNode,没有子节点的TreeNode
  • InnerNode,即有子节点的TreeNode

首先声明各种节点的类型

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<T extends TreeNode>(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);
  1. mapNode函数定义了一个泛型参数T。T的上限为TreeNode,即T可以是TreeNode,也可以是TreeNode的子类型
  2. mapNode接受两个参数,第一个是类型为T的node。由于在1中指明了node extends TreeNode,如果传入TreeNode之外的类型,例如空对象{},null或TreeNode数组,立即就能看到一条红色波浪线。node要么是TreeNode类型,要么是TreeNode的子类型。
  3. mapNode的返回值类型为T。注意,T要么是TreeNode的类型,要么是TreeNode的子类型。

为什么要这样声明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); //
​
  • logPerimeter函数只接受一个参数,类型为Shape
  • Shape是一个泛型,同时扩展HasSides和SidesHaveLength类型。也就是说,Shape至少要有一定长度的边
  • logPerimeter返回的类型与输入类型一样。

使用受限的多态模拟变长参数

借助受限的多态还可以模拟变长参数函数(可接受任意个参数的函数)。

下面我们自己实现一版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,我们想表达的约束是:

  • f函数接受一系列T类型的参数,返回某种类型R。我们事先不知道f有多少个参数。
  • call的参数为f,以及f接受的那些T类型的参数。同样,我们事先也不知道具体有多少个参数。
  • call返回的类型与f一样,也是R

因此,需要两个类型参数:T,即参数数组;R,任意类型的返回值。下面把类型填进去:

// 有问题
// function call<T,R>(
//     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<T,R>(
//     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<T,V,R>(
//     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<T extends unknown[],R>(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')

为什么要这么做呢?

  1. call是一个变长参数函数(提示一下,变长参数函数是可接受任意个参数的函数),有两个类型参数:T和R。T是unknown[]的子类型,即T是任意类型的数组或元组。
  2. call的第一个参数是函数f。f也就是变长参数函数,参数的类型与args一样,args是什么类型,f的参数就是什么类型
  3. 除了函数f之外,call还接受数量不定的额外参数…args。args是剩余参数,数量不定。args的类型是T,T必须为某一种数组类型(假如忘记指定T扩展数组类型,Typescript将报错),因此Typescript将根据具体传入的args把T推导为一种元组类型。
  4. call返回类型为R的值(R受到f返回类型的限制)

这样,调用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节。

4.2.6 泛型默认参数

函数的参数可以指定默认值,类似的,泛型参数也可以指定默认类型。

type MyEvent<T> = {target:Ttype:string
}
// 为了提供便利,默认绑定一个类型
type MyEvent<T = HTMLElement> = {target:Ttype:string
}

此外,我们还可以利用前几节所学,为T设置限制,确保T是一个HTML元素:

type MyEvent<T extends HTMLElement = HTMLElement> = {target:Ttype:string
}

注意:与函数可选参数一样,有默认类型的泛型要放在没有默认类型的泛型后面

4.3 类型驱动开发

强大的类型系统自有强大的功能。编写Typescript时候,往往发现自己“受类型的指引”。理所当然,我们称之为类型驱动开发(type-driven development).

类型驱动开发 先草拟类型签名,然后填充值的编程风格

静态类型系统的要义是约束表达式的值可以为什么类型。类型系统的表现力越强,提供的关于表达式中的信息越多。使用表现力强的类型系统注解函数,通过函数的类型签名就能知晓关于函数的多数信息:

function map<T,U>(array:T[],f:(item:T)=>U):U[]{//...
}

即使以前没有使用过map也能通过签名直观的看出map的作用。

编写Typescript程序时,先定义函数的类型签名,即“受类型的指引”,然后在具体实现。先在类型层面规划程序可以确保在着手实现之前程序的整体合理性。

目前为止,我们都是反着做的,即受实现的指引,然后再推演类型。现在我们知道在Typescript中如何编写函数和注解函数的类型了,那就可以换种模式,先规划类型,然后再填充细节。

相关文章:

Typescript第四章 函数(声明和调用,注解参数类型,多态,类型别名,泛型)

第四章 函数 ts中声明和调用函数的不同方式签名重载多态函数多态类型声明 4.1 声明和调用函数 在js中函数是一等对象&#xff0c;我们可以像对象那样使用函数&#xff0c;可以复制给变量&#xff0c;可以作为参数传递&#xff0c;返回值&#xff0c;赋值给对象的原型&#x…...

大数据-Spark批处理实用广播Broadcast构建一个全局缓存Cache

1、broadcast广播 在Spark中&#xff0c;broadcast是一种优化技术&#xff0c;它可以将一个只读变量缓存到每个节点上&#xff0c;以便在执行任务时使用。这样可以避免在每个任务中重复传输数据。 2、构建缓存 import org.apache.spark.sql.SparkSession import org.apache.s…...

Android Service的生命周期,两种启动方法,有什么区别

Android Service的生命周期&#xff0c;两种启动方法&#xff0c;有什么区别 Android Service是一种后台组件&#xff0c;用于在后台执行长时间运行的任务&#xff0c;而无需与用户界面进行交互。Service具有自己的生命周期&#xff0c;其主要包含以下几个状态&#xff1a;创建…...

测试开源C#人脸识别模块ViewFaceCore(5:质量检测和眼睛状态检测)

ViewFaceCore模块中的FaceQuality支持预测人脸质量&#xff0c;最初以为是预测人体体重&#xff0c;实际测试过程中才发现是评估人脸图片质量&#xff0c;主要调用Detect函数执行图片质量检测操作&#xff0c;其函数原型如下所示&#xff1a; //// 摘要:// 人脸质量评估///…...

Go语言网络库net/http

Go语言网络库net/http Http 协议(Hyper Text Transfer Protocol&#xff0c;超文本传输协议)是一个简单的请求-响应协议&#xff0c;它通常运行在 TCP 之 上。超文本传输协议是互联网上应用最为广泛的一种网络传输协议&#xff0c;所有的WWW文件都必须遵守这个标准。 Http 协…...

Acwing.91 最短Hamilton路径(动态规划)

题目 给定一张n个点的带权无向图&#xff0c;点从0~n-1标号&#xff0c;求起点0到终点n-1的最短Hamilton路径。Hamilton路径的定义是从0到n-1不重不漏地经过每个点恰好一次。 输入格式 第—行输入整数n。 接下来n行每行n个整数&#xff0c;其中第i行第j个整数表示点i到j的距…...

[hfut] [important] v4l2 vedio使用总结/opevx/ffpeg/v4l2/opencv/cuda

(158条消息) linux驱动camera//test ok_感知算法工程师的博客-CSDN博客 (158条消息) linux V4L2子系统——v4l2架构&#xff08;1&#xff09;之整体架构_感知算法工程师的博客-CSDN博客 (158条消息) linux V4L2子系统——v4l2的结构体&#xff08;2&#xff09;之video_devi…...

2023年深圳杯数学建模A题影响城市居民身体健康的因素分析

2023年深圳杯数学建模 A题 影响城市居民身体健康的因素分析 原题再现&#xff1a; 以心脑血管疾病、糖尿病、恶性肿瘤以及慢性阻塞性肺病为代表的慢性非传染性疾病&#xff08;以下简称慢性病&#xff09;已经成为影响我国居民身体健康的重要问题。随着人们生活方式的改变&am…...

指令调度(Instruction Scheduling)

指令调度&#xff08;Instruction Scheduling&#xff09; 指令调度的约束基本机器模型基本块调度全局调度 指令调度是为了提高指令级并行&#xff08;ILP&#xff09;&#xff0c;对于超长指令字&#xff08;VLIW, Very Long Instruction Word&#xff09;和多发射系统&#x…...

【运维】Linux 跨服务器复制文件文件夹

【运维】Linux 跨服务器复制文件文件夹 如果是云服务 建议用内网ip scp是secure copy的简写&#xff0c;用于在Linux下进行远程拷贝文件的命令&#xff0c;和它类似的命令有cp&#xff0c;不过cp只是在本机进行拷贝不能跨服务器&#xff0c;而且scp传输是加密的。可能会稍微影…...

k8s apiserver如何支持http访问?

原本是可以通过设置api-server的--insecure-port来实现&#xff0c;但是这个参数已经被废弃了&#xff0c;更好的方法则是使用proxy来实现&#xff1a; 在集群任意一个节点上起一个proxy服务&#xff0c;并设置允许所有host访问&#xff1a; kubectl proxy --address0.0.0.0 …...

Safetensors,高效安全易用的深度学习新工具

大家好&#xff0c;本文将介绍一种为深度学习应用提供速度、效率、跨平台兼容性、用户友好性和安全性的新工具。 Safetensors简介 Hugging Face开发了一种名为Safetensors的新序列化格式&#xff0c;旨在简化和精简大型复杂张量的存储和加载。张量是深度学习中使用的主要数据…...

Unity 工具之 NuGetForUnity 包管理器,方便在 Unity 中的进行包管理的简单使用

Unity 工具之 NuGetForUnity 包管理器&#xff0c;方便在 Unity 中的进行包管理的简单使用 目录 Unity 工具之 NuGetForUnity 包管理器&#xff0c;方便在 Unity 中的进行包管理的简单使用 一、简单介绍 二、NuGetForUnity 的下载导入 Unity 三、NuGetForUnity 在 Unity 的…...

运算放大器(二):恒流源

一、实现原理 恒流源的输出电流能够在一定范围内保持稳定&#xff0c;不会随负载的变化而变化。 通过运放&#xff0c;将输入的电压信号转换成满足一定关系的电流信号&#xff0c;转换后的电流相当一个输出可调的简易恒流源。 二、电路结构 常用的恒流源电路如…...

企业选择租用CRM还是一次性买断CRM?分别有哪些优势?

CRM是企业管理客户关系&#xff0c;提升销售业绩&#xff0c;实现业务增长的重要工具。市场上的CRM系统销售方式主要有两种——租用型和买断型。那么&#xff0c;租用CRM好还是一次性买断CRM好&#xff1f;本文将从以下几个方面进行分析&#xff1a; 1、什么是租用型CRM和买断…...

VBA_MF系列技术资料1-133

MF系列VBA技术资料 为了让广大学员在实际VBA编程中有切实可行的思路及有效的提高自己的编程技巧&#xff0c;我参考大量的资料&#xff0c;并结合自己的经验总结了这份MF系列VBA技术综合资料&#xff0c;而且开放源码&#xff08;MF04除外&#xff09;&#xff0c;其中MF01-04属…...

Android 项目架构

🔥 什么是架构 🔥 在维基百科里是这样定义的: 软件架构是一个系统的轮廓 . 软件架构描述的对象是直接构成系统的抽象组件. 各个组件之间的连接则明确和相对细致地描述组件之间的通讯 . 在实现阶段, 这些抽象组件被细化为实际组件 , 比如具体某个类或者对象 . 面试的过程中…...

【Linux】进程通信 — 管道

文章目录 &#x1f4d6; 前言1. 通信背景1.1 进程通信的目的&#xff1a;1.2 管道的引入&#xff1a; 2. 匿名管道2.1 匿名管道的原理&#xff1a;2.2 匿名管道的创建&#xff1a;2.3 父子进程通信&#xff1a;2.3.1 read()阻塞等待 2.4 父进程给子进程派发任务&#xff1a;2.5…...

ROS 2 — 托管(生命周期)节点简介

一、说明 这篇文章是关于理解ROS 2中托管&#xff08;生命周期&#xff09;节点的概念。我们描述了概念性的想法以及我们为什么需要它。所以让我们开始吧&#xff01; 二、托管式节点 — 什么和为什么&#xff1f; 为了理解托管式节点&#xff0c;让我们从一个简单的问题陈述开…...

vue2企业级项目(六)

vue2企业级项目&#xff08;六&#xff09; 自定义指令 创建src/directive/index.js const directives require.context("./modules", true, /\.js$/);export default {install: (Vue) > {directives.keys().forEach((key) > {let directive directives(key…...

k8s从入门到放弃之Ingress七层负载

k8s从入门到放弃之Ingress七层负载 在Kubernetes&#xff08;简称K8s&#xff09;中&#xff0c;Ingress是一个API对象&#xff0c;它允许你定义如何从集群外部访问集群内部的服务。Ingress可以提供负载均衡、SSL终结和基于名称的虚拟主机等功能。通过Ingress&#xff0c;你可…...

ssc377d修改flash分区大小

1、flash的分区默认分配16M、 / # df -h Filesystem Size Used Available Use% Mounted on /dev/root 1.9M 1.9M 0 100% / /dev/mtdblock4 3.0M...

STM32F4基本定时器使用和原理详解

STM32F4基本定时器使用和原理详解 前言如何确定定时器挂载在哪条时钟线上配置及使用方法参数配置PrescalerCounter ModeCounter Periodauto-reload preloadTrigger Event Selection 中断配置生成的代码及使用方法初始化代码基本定时器触发DCA或者ADC的代码讲解中断代码定时启动…...

【磁盘】每天掌握一个Linux命令 - iostat

目录 【磁盘】每天掌握一个Linux命令 - iostat工具概述安装方式核心功能基础用法进阶操作实战案例面试题场景生产场景 注意事项 【磁盘】每天掌握一个Linux命令 - iostat 工具概述 iostat&#xff08;I/O Statistics&#xff09;是Linux系统下用于监视系统输入输出设备和CPU使…...

【配置 YOLOX 用于按目录分类的图片数据集】

现在的图标点选越来越多&#xff0c;如何一步解决&#xff0c;采用 YOLOX 目标检测模式则可以轻松解决 要在 YOLOX 中使用按目录分类的图片数据集&#xff08;每个目录代表一个类别&#xff0c;目录下是该类别的所有图片&#xff09;&#xff0c;你需要进行以下配置步骤&#x…...

全志A40i android7.1 调试信息打印串口由uart0改为uart3

一&#xff0c;概述 1. 目的 将调试信息打印串口由uart0改为uart3。 2. 版本信息 Uboot版本&#xff1a;2014.07&#xff1b; Kernel版本&#xff1a;Linux-3.10&#xff1b; 二&#xff0c;Uboot 1. sys_config.fex改动 使能uart3(TX:PH00 RX:PH01)&#xff0c;并让boo…...

Android第十三次面试总结(四大 组件基础)

Activity生命周期和四大启动模式详解 一、Activity 生命周期 Activity 的生命周期由一系列回调方法组成&#xff0c;用于管理其创建、可见性、焦点和销毁过程。以下是核心方法及其调用时机&#xff1a; ​onCreate()​​ ​调用时机​&#xff1a;Activity 首次创建时调用。​…...

网站指纹识别

网站指纹识别 网站的最基本组成&#xff1a;服务器&#xff08;操作系统&#xff09;、中间件&#xff08;web容器&#xff09;、脚本语言、数据厍 为什么要了解这些&#xff1f;举个例子&#xff1a;发现了一个文件读取漏洞&#xff0c;我们需要读/etc/passwd&#xff0c;如…...

力扣热题100 k个一组反转链表题解

题目: 代码: func reverseKGroup(head *ListNode, k int) *ListNode {cur : headfor i : 0; i < k; i {if cur nil {return head}cur cur.Next}newHead : reverse(head, cur)head.Next reverseKGroup(cur, k)return newHead }func reverse(start, end *ListNode) *ListN…...

Docker、Wsl 打包迁移环境

电脑需要开启wsl2 可以使用wsl -v 查看当前的版本 wsl -v WSL 版本&#xff1a; 2.2.4.0 内核版本&#xff1a; 5.15.153.1-2 WSLg 版本&#xff1a; 1.0.61 MSRDC 版本&#xff1a; 1.2.5326 Direct3D 版本&#xff1a; 1.611.1-81528511 DXCore 版本&#xff1a; 10.0.2609…...