18
引言:最近笔者在学习typescript,同时应用到项目开发中,除了简单的类型约束以外,由于typescript仍有许多让初学者不明确的点(官方文档也没有写清楚),故笔者整理此文章供初学者学习参考。

前言

typescript是javascript的超集,在js的基础上,为你的变量参数等加上类型定义,于是就变成了typescript,在项目中使用typescript会有以下几点好处:

  • 1、预先思考代码逻辑,预设变量类型和传参类型,代码更加规范;
  • 2、类型检查校验,报错提示;
  • 3、调用方法或属性时的代码提示;(结合IDE)
  • 4、方便后期维护以及团队其他成员接手开发,省去写一大堆文档注释的功夫。

语法基础

1、基本类型值

number、string、boolean、object、null、undefined、symbol

2、tuple元组、any、Array<T>、enum枚举类型

tuple元组指的是可以对数组内不同项设置不同的数值类型;
any类型不指定变量具体类型;
Array<T>中T为任意类型,也可为自定义类型;
enum为枚举类型。
注:有时候为了ts不报错,可以通过类型断言绕过检测。

3、interface、class、type

除了以上的基本类型值和ts中定义的特殊类型以外,使用者还可以自定义类型,通过interface定义一个未实现的接口,它可以用来限制变量所具备的数据格式,同理,class作为构造类,也可用来变量类型,type是类型别名,有点类似interface,但其主要用于类型别名和类型合并等。

interface Person{
    name: string
}
class Person{
    name: string
}
type Person{
    name: string
}
const person: Person;

以上三者的约束效果是一致的。那什么时候用哪个呢?关键在于你对于接口和类的理解,当你提炼的只是一个通用接口的时候可以用interface,当你要提供一个可以实例化的class给外部模块使用的时候,你需要用class。type前面已经介绍了,主要用于类型别名或合并。所以只要你理解三者用途的区别,自然会知道什么时候使用哪个了。

4、函数定义

如果是普通函数,可以直接通过interface定义:

interface testfunction{
    (name: string):number
}
const myfunc: testfunction = ()=>{return 1;}

定义函数的传参类型以及返回值类型。
当然你也可以直接在函数上加约束条件,不需要单独提取一个interface来定义函数类型:

const myfunc = (name: string):number{
    return 1;
}

如果是构造函数类的话,就不赘述了,构造函数类不同于普通函数。
当然,ts中允许对函数重载,以及设置可选参数,即在参数key后加个问号。

进阶扩展

1、class进阶

你可以对class中的属性设置为只读属性,即设置readonly。
你可以对class中的方法添加public、private、protected修饰符。
你可以对class实现接口。类可以继承类,类实现接口,接口继承接口。
你可以实现抽象类abstract class,抽象类作为其它派生类的基类使用。 它们一般不会直接被实例化。 不同于接口,抽象类可以包含成员的实现细节。 abstract关键字是用于定义抽象类和在抽象类内部定义抽象方法。

1)、public修饰符表示属性是公开的,可以通过实例去访问该属性。类属性默认都是public属性。
2)、private修饰符表示属性是私有的,只有实例的方法才能访问该属性。
3)、protected修饰符表示属性是保护属性,只有实例的方法和派生类中的实例方法才能访问到。
4)、当然除此之外,还有static修饰符,表示只能通过类本身来调用方法,不能通过实例调用。

interface PersonInterface{
    name: string
}
class Person implements PersonInterface{
    readonly name: string
    constructor({name: string} = {}){
        this.name = name
    }
    public getMyName(){
        this.myselfTest()
        this.lastTest()
        return this.name
    }
    static test(){
        console.log(`test`)
    }
    private myselfTest(){
        console.log(`my test`)
    }
    protected lastTest(){
        console.log(`protected`)
    }
}
Person.test()
const person = new Person()
person.getMyName()
//myselfTest只能在实例方法中调用
//lastTest可以在父类和子类的实例方法中调用
class Runner extends Person{
  public test(){
    this.lastTest()
  }
}

2、泛型

泛型泛型,就是比较空泛的类型,不明确的类型,不同于any,any直接把所有东西都只带成any,不做任何的约束,泛型可以对参数和返回值之间设置关联约束,同时,也可以在设置变量的数据类型的时候,传入某种具体类型,去“具化泛型”。
函数中的使用:

function someFunction<T>(arg: T) : T {
  return arg;
}
someFunction('123')
someFunction<number>(123)

接口 or type 中使用:

interface character<T>{
    type: T
    name: string
}
type character<T> = {
    type: T
    name: string
}
const char: character<number>

通过传入number具化泛型的约束范围,同时,接口和type都可以再进行复用,这就是泛型在这里起到的作用。
注:泛型还可以添加额外的泛型约束,extend某个接口,让ts知道这个泛型T背后可能存在的额外属性。

3、其他语法

ts中存在交叉类型,联合类型,is、keyof,感觉除了联合类型用的比较多以外,其他很少用到,不赘述。
除此之外,还有一个是ts中的声明合并,interface的声明可以合并,namespace的声明也可以合并。由于可以重复声明,之后会自动合并,所以我们也可以借此来对第三方库中的interface或namespace进行扩展。

interface Person{
    name: string
}
interface Person{
    address: string
}
==>
interface Person{
    name: string
    address: string
}

namespace Test{
    const name: string
}
namespace Test{
    function getName(){}
}
==>
namespace Test{
    const name: string
    function getName():void
}

namespace是个神奇的概念,命名空间,你也可以看成这是一个挂载点,是一个实例化的对象{},它上面挂载了一些属性和方法。这会让你更好的理解它,它不是一个虚拟的存在,像interface、class都只是定义了类型,并没有实例化对象,但是命名空间不一样,你可以把它看成一个现实存在的实例对象。那么接下来你会更好地理解下面这个操作:

declare function globalFunc(name: string): void
declare namespace globalFunc{
    const version: string
    function otherFunc(name: string): void
}

注:declare是类型定义文件中的语法,后面会介绍到。
在这里我declare一个全局的方法,同时我declare了一个命名空间和方法同名,其实这时候你就可以认为这个方法的实例对象上挂载了一些属性和方法。毕竟在js中,函数本身也是一个对象。就像这样子:

const globalFunc = ()=>{}
globalFunc.version = '1.0.0'
globalFunc.otherFunc = ()=>{}

这样,你就能很好地理解这种写法了。而且这种写法十分常见。

4、如何书写类型声明文件

1、当你引入一个js文件的时候,ts会去查找对应文件目录下是否存在同名的d.ts文件,无论是相对路径还是绝对路径还是第三方模块的引入,都是如此;
2、如果你自己在书写一个第三方模块,那么直接用ts开发,编译产物中变回自动生成模块文件的d.ts文件;
3、在开发自己的项目或者模块过程中,不可避免你要引入第三方模块,或者对全局的属性进行扩展,这个时候你需要书写自己项目中的全局类型定义文件,你可以将他们统一放置在typings文件夹下,ts会自动查找项目中的d.ts文件,(且不与其他文件存在同名的情况下),例如你可以在项目下创建一个global.d.ts文件,对第三方模块或全局模块做额外定义或扩展;
4、第三方模块都要自己来定义吗?非也,大部分的第三方库都有对应的类型定义文件,如果没有也不要担心,有人帮你写好了类型定义文件,例如,在nodejs中开发的时候,引用原生模块时,你可以通过引入@types/node包来告诉ts原生模块中的属性方法,其他第三方库也是类似的。
5、declare必须是在全局声明文件中使用时,才能有效定义全局变量,如果使用了export import等,将会被判定为第三方模块,而不会当做全局定义文件。
6、当你在扩展第三方模块的时候,在你的声明文件中,使用import * as xx from 'xx'模块,你能获取到里面定义的interface、enum这种"虚拟"类型,但当你在业务代码ts文件中引用时,只能访问到模块实际挂载的属性的方法。
一个项目中的全局声明文件可能是这样的:

//定义全局变量
declare const version: string
//定义全局方法
declare function jQuery(selector: string): any;
//定义全局类
declare class Person{ ... }
//定义第三方模块
declare module 'fs'{
    import * as fs from 'fs'
    function myTestFunc(): void  //扩展方法
    //自定义一个interface扩展fs内部的interface
    interface myTestInterface extends fs.xxinterface{
        name: string
    }
    //定义一个模块上挂载的新属性,类型为自定义扩展后的interface
    const myTestName: myTestInterface
    
}

接下来,一个模块定义文件可能是这样的:(你可以查阅官方文档,里面会有更多的模板实例,这里会展示一个简单的栗子并解释)

/*当你的模块是一个UMD模块且是在外部加载好的时候,添加这一行,告诉ts全局有这个变量
 */
export as namespace myClassLib;
/*模块所暴露的对象
 */
export = MyClass;
/*你的构造函数 */
declare class MyClass {
    constructor(someParam?: string);
    someProperty: string[];
    myMethod(opts: MyClass.MyClassMethodOptions): number;
}
/*你可以把你的类型定义放到Myclass命名空间下统一管理,也可以供给其他人扩展它
 */
declare namespace MyClass {
    export interface MyClassMethodOptions {
        width?: number;
        height?: number;
    }
}

看完了全局声明文件、模块声明文件以及声明文件注意事项,相信你对声明文件的使用书写以及了然于心!

答疑解惑

笔者个人整理了额外的一些疑问,这里整理了一下罗列如下:

1、type 和 interface的区别?

type和interface本身都可以用来定义类型,约束变量的数据结构,但是type和interface的应用场景不同,需要你更好地理解这两者。type可以作为类型别名,也可以再type中使用联合类型,例如type myType = number|string,这是interface无法实现的。而interface不仅可以定义类型,还定义一个待实现的接口,它的用处更广泛,并且在第三方模块的扩展中,一般都是对interface进行再扩展,你无法对type进行再扩展。

2、抽象类和interface的区别?

1)、抽象类要被子类继承,接口要被类实现。
2)、抽象类中可以声明和实现方法,接口只能申明。
3)、接口抽象级别更高
4)、抽象类用来抽象类别,接口用来抽象功能。

3、declare module和declare namespace的区别?

declare module是定义一个模块,module本身和namespace类似,都是一个挂载点,但是module > namespace,一个module的定义文件里可以含有多个namespace,namespace只是一个辅助的挂载点,在以前namespace被称为内部模块,但是后来不这么叫了,以防混淆。

4、声明文件中的reference path 和reference type的区别?

三斜线指令仅可放在包含它的文件的最顶端,只可使用在d.ts文件中。
看一下两者的使用:

/// <reference path="..." /> 用于声明对某个路径文件的依赖。
/// <reference types="..." />用于声明对某个包的依赖。

例如
/// <reference types="node" />
表明这个文件使用了@types/node/index.d.ts里面声明的名字

path用于约束声明文件之间的依赖关系,types直接声明了对某个包的依赖,相信你已经明白了。

最后

感谢你看完了这篇文章,这篇文章包含了入门的基础知识,但是如果你想了解更多有关基础知识,还请移步官网。
其次,我们归纳总结了ts中进阶、难度较高的语法,同时对声明文件的定义进行了梳理,最后我们对常见的疑惑点进行了归纳整理。希望这篇入门教程对你有所帮助~


曾培森
1.1k 声望875 粉丝

学海无涯皮蛋瘦肉粥