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 {
    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

调用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
}

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赋值,但是随后就赋值了

4.2多态

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

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

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

举个例子

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绑定的类型。

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

4.2.1 什么时候绑定泛型

声明泛型的位置不仅限定泛型的作用域,还决定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在使用泛型时为泛型绑定具体类型:对函数来说,在调用函数时,对类来说,在实例化类时:对类型别名和接口来说,在使用别名和实现结构时。

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

只要是在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[]
}

4.2.3 泛型推导

多数情况下,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)
​

4.2.4 泛型别名

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'
})

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

  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(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(
//     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')

为什么要这么做呢?

  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 = {
    target:T
    type:string
}
// 为了提供便利,默认绑定一个类型
type MyEvent = {
    target:T
    type:string
}

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

type MyEvent = {
    target:T
    type:string
}

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

4.3 类型驱动开发

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

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

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

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

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

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

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

你可能感兴趣的:(Typescript学习指南,typescript,git,javascript,前端,ecmascript)