TypeScript学习心得

什么是TypeScript

  • TypeScript 是添加了类型系统的 JavaScript,适用于任何规模的项目

  • TypeScript 是一门静态、弱类型的语言

  • TypeScript 是完全兼容 JavaScript 的,它不会修改 JavaScript 运行时的特性

  • TypeScript 可以编译为 JavaScript,然后运行在浏览器、Node.js 等任何能运行 JavaScript 的环境中

  • TypeScript 拥有很多编译选项,类型检查的严格程度由你决定

  • TypeScript 可以和 JavaScript 共存,这意味着 JavaScript 项目能够渐进式的迁移到 TypeScript

  • TypeScript 增强了编辑器(IDE)的功能,提供了代码补全、接口提示、跳转到定义、代码重构等能力

  • TypeScript 拥有活跃的社区,大多数常用的第三方库都提供了类型声明

  • TypeScript 与标准同步发展,符合最新的 ECMAScript 标准(stage 3)

TypeScript的类型安全

类型安全,即借助类型避免程序做无效的事情(在不同的静态类型语言中,无效可以指运行时程序崩溃,也可以指未崩溃,但做的事情毫无意义)

举几个无效例子:

  • 一个数字乘以一个列表

  • 调用接受一组对象的函数却传入一串字符串

  • 在对象上调用不存在的方法

这些问题在编程时时常发生。编程语言在发现你做无效的事情时会尝试判断你的真正意图,以JavaScript为例:

3 + []        //结果为“3”

const obj = {}
obj.test      //结果为undefined

function x(a) {
    return a/2
}
a('y')        //结果为NaN
复制代码

显而易见,遇到无效操作时JavaScript并未报错,而是自行推断结果,这会给后期的更新和维护带来巨大的麻烦

而TypeScript的作用就是在输入代码的过程中,文本编辑器会报出错误信息。下面展示一下上面例子的报错信息:

3 + []        //运算符“+”不能应用于类型“number”和“never[]”

const obj = {}
obj.test      //类型“{}”上不存在属性“test”

function x(a) {
    return a/2
}
a('y')        //'y'不是数字类型
复制代码

类型介绍

any

any是类型的教父。在TypeScript中,编译时一切都要有类型,但除非迫不得已,千万不要用any,如果你和TS无法确定类型,默认为any。这是兜底类型,尽量避免使用。

any类型的值就像常规的js一样,类型检查器完全不能发挥作用。

举个例子

let a:any = 666        //any
let b:any = 'yyds'     //any
let c:any = a + b      //any
复制代码

正常情况下第三个语句会报错,但实际上没有,因为我们告诉ts相加的两个值都是any类型。如果要使用any,一定要显式注解。

unknown

少数情况下,如果你确实无法预知一个值的类型,不要使用any,应该使用unknown。与any类似,unknown也表示任何值,但是TS会要求你再做检查,细化类型。

unknown类型的值可以比较(使用 ==、===、||、&&和?)、可以否定(!)、可以使用js的typeofinstanceof运算符细化,以下为unknown类型使用示例:

let a:unknown = 30      //unknown
let b = a === 123       //boolean
let c = a + 10          //对象的类型为 "unknown"
if(typeof a === 'number') {
    let d = a + 10      //number
}
复制代码

总结:

1.TS不会把任何值推导为unknown类型,必须显示注解(上述a为例)

2.unknown类型的值可以比较(b)

3.执行操作时不能假定unknown类型的值为某种特定类型(c),必须先向TS证明一个值确实是某种类型(d)

boolean

boolean类型有两个值:true和false。该类型的值可以比较(使用 ==、===、||、&&和?)、可以否定(!),以下为boolean类型使用示例:

let a = true            //boolean
var b = false           //boolean
const c = true          //true
let d:boolean = true    //boolean
let e:true = true       //true
let f:true = false      //不能将类型“false”分配给类型“true”
复制代码

以上示例表明我们可以通过多种方法告知TS一个值的类型为boolean:

1.可以让TS推到出值得类型为boolean(a和b)

2.可以让TS推导出值为某个具体得布尔值(c)

3.可以明确告知TS值的类型为boolean(d)

4.可以明确告知TS值为某个具体的布尔值(e和f)

e不是普通的boolean类型,而是只为true的boolean类型。把类型设为某个值,就限制了e和f在所有布尔值中只能取指定的那个值。这个特性称为类型字面量(仅表示一个值的类型)

number

number包括所有数字:整数、浮点数、正数、负数、Infinity、NaN等。当然,数字也可以进行算数运算:加(+)、减(-)、乘(*)、除(/)、求模(%)、比较(<、>)等。以下为number类型使用示例:

let a = 123                 //number
var b = Infinity*0.10       //number
const c = 666               //666
let d = a < b               //boolean
let e:number = 100          //number
let f:3.1415926 = 3.1415926 //3.1415926
let g:3.14 = 666            //不能将类型‘666’分配给类型‘3.14’
复制代码

与boolean类型相似,把值声明为number类型也有四种方法:

1.可以让TS推导出值的类型为number(a和b)

2.使用const,使TS推导出值为某个具体的数字(c)

3.可以明确告知TS值的类型为number(e)

4.可以明确告知TS值为某个具体数字(f和g)

bigint

bigint是JS和TS新引入的类型,在处理较大的整数时,不必再担心舍入误差。number类型表示的最大整数为2^53^,而bigint能表示的数比这大的多。bigint类型包含所有BigInt数,支持加减乘除和比较。以下为bigint类型使用示例:

let a = 12345n               //bigint
const b = 56789n             //56789n
var c = a + b                //bigint
let d = a < 123              //boolean
let e = 66.6n                //bigint 文本必须是整数
let f:bigint = 100n          //bigint
let g:100n = 100n            //100n
let h:bigint = 100           //不能将类型‘100’分配给类型‘bigint’
复制代码

声明bigint类型与boolean及number类似,就不一一赘述了。

string

string包含所有字符串,以及可以对字符串执行的操作,例如拼接字符串(+)、切片(.slice)等。以下为string类型使用示例:

let a = 'hello mwq'            //string
var b = 'mwqYYDS'              //string
const c = '?'                  //'?'
let d = a + '' + b + c         //string
let e:string = 'zoom'          //string
let f:'mwq' = 'mwq'            //'mwq'
let g:'mwq' = 'qwm'            //不能将类型'qwm'分配给类型'mwq'
复制代码

声明string类型与boolean及number类似,这里也就不一一赘述了。

symbol

symbol经常用于代替对象和映射的字符串键,确保使用正确的已知键,以防键被意外设置。

let a = Symbol('a')          //symbol
let b:symbol = Symbol('b')   //symbol
var c = a === b              //boolean
let b = a + 'x'              //“+”运算符不能应用于类型 "symbol"
复制代码

Symbol(‘a’)使用指定的名称新建一个符号,这个符号是唯一的,不与其他任何符号相等(使用==或===比较)

同样,符号也可以显示声明为unique symbol类型:

const e = Symbol('e')                      //typeof e
const f:unique symbol = Symbol('f')        //typeof f
let g:unique symbol = Symbol('f')          //类型为 "unique symbol" 的变量必须为 "const"
let h = e === e                            //boolean
复制代码

数组

TypeScript像JavaScript一样可以操作数组元素。 有两种方式可以定义数组。 第一种,可以在元素类型后面接上 [],表示由此类型元素组成的一个数组:

let arr: number[] = [1, 2, 3]             //number[]
复制代码

第二种方式是使用数组泛型,Array<元素类型>:

let list: Array<number> = [1, 2, 3]       //number[]
复制代码
const a = [2,'a']                         // (string|number)[]
let b = []                                //any[]
b.push(1)                                 //number[]
b.push('red')                             //(string|number)[]
let c:number[] = []                       //number[]
c.push(1)                                 //number[]
c.push('red')                             //类型“string”的参数不能赋给类型“number”的参数
复制代码

由上a可看出,使用const声明数组不会导致TS推导出范围更窄的类型。

思考一下b为什么不会报错?

接着再看一个例子,你就会明白

const buildArray = () => {
    let a =[]                       //any[]
    a.push(1)                       //number[]
    a.push('a')                     //(string|number)[]
    return a 
}

let myArray = buildArray()          //(string|number)[]
myArray.push(true)                  //类型“boolean”的参数不能赋给类型“string | number”的参数
复制代码

初始化空数组时,TS不知道数组中元素的类型,因此推导出的类型为any。向数组中添加元素后,TS开始拼凑数组的类型。当数组离开定义时所在的作用域后,TS将最终确定一个类型,不在扩张。

元组

元组是Array的子类型,是定义数组的一种特殊方式,长度固定,各索引位上的值具有固定的已知类型。声明元组时必须显示注解类型,因为创建元组使用的句法与数组相同(都是用方括号[]),而TS遇到方括号,推导出来的是数组的类型。

let a: [string, number]
a = ['hello', 10]                   // OK
a = [10, 'hello']                   // Error
复制代码

元组也支持可选元素:

let b:[number, number?][] = [
    [2333],
    [23, 33]
]
复制代码

元组也支持剩余元素,即为元组定义最小长度:

let name:[string, ...string[]] = ['mwq', 'qwm', 'wqm', 'mqw']      //字符串列表,至少有一个元素
let list:[number, boolean, ...string[]] = [1, false, 'a', 'b']     //元素类型不同的列表
复制代码

声明只读数组

TS原生支持只读数组类型,用于创建不可变的数组。若想创建只读数组,要显示注解类型;若想更改只读数组,使用非变形方法,例如.concat及.slice,不能使用可变型方法,例如.push及.splice。

声明只读数组和元组方法如下:

type A = readonly string[]           //readonly string[]
type B = ReadonlyArray<string>       //readonly string[]
type C = Readonly<string[]>          //readonly string[]

type D = readonly [number, string]   //readonly [number, string]
type E = Readonly<[number, string]>  //readonly [number, string]
复制代码

枚举

枚举类型是对JavaScript标准数据类型的一个补充。 像C#等其它语言一样,使用枚举类型可以为一组数值赋予友好的名字。

枚举分为两种:字符串到字符串之间的映射和字符串到数字之间的映射。如下所示:

enum Language {
    English,
    Spanish,
    Russian
}
复制代码

TS可以自动为枚举中的各个成员推导对应的数字,当然你也可以自己手动设置。如下所示:

enum Language {
    English = 2,
    Spanish = 5,
    Russian = 8
}
复制代码

按约定,枚举名称为大写的单数形式。枚举中的键也为大写

枚举中的值使用点号或方括号表示法访问

let myFirstLanguage = Language.Russian
let mySecondLanguage = Language['English']
复制代码

一个枚举可以分成几次声明,TS将自动将其合并。注意,如果分开声明枚举,TS只能推导出其中一部分的值,因此最好为枚举的每个成员显示赋值

enum Language {
    English = 0,
    Spanish = 1,
}

enum Language {
    Russian = 2
}
复制代码

成员的值可以经计算得出,而且不必为所有成员都赋值(TS会自动推导出其值):

enum Language {
    English = 2,
    Spanish = 200+20,
    Russian            //221
}
复制代码

枚举的值也可以为字符串,甚至混用字符串和数字:

enum Color {
    Red = '#c10000',
    Blue = '#007ac1',
    Pink = 0xc10050,     //十六进制字面量
    White = 255          //十进制字面量
}

let red = Color.Red      //'#c10000'
let white = Color[255]   //White
复制代码

由上可以看出,TS即允许同过值访问枚举,也允许通过键访问枚举

void

某种程度上来说,void类型像是与any类型相反,它表示没有任何类型。 当一个函数没有显示返回任何值(例如console.log)时,你通常会见到其返回值类型是 void:

function warnUser(): void {
    console.log("This is my warning message");
}
复制代码

声明一个void类型的变量没有什么大用,因为你只能为它赋予undefined和null:

let unusable: void = undefined
复制代码

null和undefined

TypeScript里,undefined和null两者各自有自己的类型分别叫做undefined和null。 和 void相似,它们的本身的类型用处不是很大:

let a: undefined = undefined;
let b: null = null
复制代码

默认情况下null和undefined是所有类型的子类型。 就是说你可以把 null和undefined赋值给number类型的变量。

never

never类型表示的是那些永不存在的值的类型。 例如, never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型。

const a:never = () => {
    throw TypeError('I always error')
}

const b:never = () => {
    while(true) {
        doSomething()
    }
}
复制代码

如果说unknown是其他每个类型的父类型,那么never就是其他每个类型的子类型。我们可以把never理解为“兜底类型”。这意味着我们可以把never类型赋值给其他任意类型。

对象

在TS中使用类型描述对象有好几种方式,下面我将一一介绍。

对象字面量句法

let a:{b:number} = {
    b:2333
}                          //{b:number}


const a:{b:number} = {
    b:2333
}                          //{b:number}
复制代码

值得一提的是,使用const声明对象并不会缩窄推导的类型

小试牛刀

看一下哪些类型可以赋值给a?

let a:{
    b:number,
    c?:string,
    [key:number]:boolean
}

a = {b: 1}
a = {b: 1, c: undefined}
a = {b: 1, c: 'd'}
a = {b: 1, 10: true}
a = {b: 1, 10: true, 20: false}
复制代码

索引签名

[key:T] :U 句法称为索引签名,我们通过这种方式告诉TS指定的对象可能有更多的键。句法的意思是,“在这个对象中,类型为T的键对应的值为U类型。”

注意:

1.键的类型(T)必须可以赋值给number或string

2.索引签名中键的名称可以是任何词,不一定非得用key

object

object表示非原始类型,也就是除number,string,boolean,symbol,null或undefined之外的类型。

使用object类型,就可以更好的表示像Object.create这样的API。例如:

interface ObjectConstructor {
  create(o: object | null): any;
}

const proto = {};

Object.create(proto);     // OK
Object.create(null);      // OK
Object.create(undefined); // Error
Object.create(1337);      // Error
Object.create(true);      // Error
Object.create("oops");    // Error
复制代码

空对象类型{}

除null和undefined之外的任何类型都可以赋值给空对象类型,使用起来比较复杂,应尽量避免使用

let danger:{}
danger = {}                
danger = {x:1}
danger = []
danger = 2
复制代码

Object

与{}作用基本一样,最好也避免使用,这里就不再做介绍

类型别名

我们可以使用变量声明(let、const和var)为值声明别名,同样地,我们也可以为类型声明别名,例子如下:

type Message = string | string[];

let greet = (message:Message) => {
    //...
};
复制代码

类型别名有助于减少重复输入复杂的类型,还能更清楚地表明变量的作用(具有描述性的类型名称)。

TypeScript断言

类型断言

有时候你会遇到这样的情况,你会比 TypeScript 更了解某个值的详细信息。通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。

通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。类型断言好比其他语言里的类型转换,但是不进行特殊的数据检查和解构。它没有运行时的影响,只是在编译阶段起作用。

类型断言有两种形式:

1.“尖括号”语法

let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
复制代码

2.as语法

let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
复制代码

非空断言

在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符 ! 可以用于断言操作对象是非 null 和非 undefined 类型。具体而言,x! 将从 x 值域中排除 null 和 undefined 。例子如下:

1.忽略null和undefine类型

function myFunc(maybeString: string | undefined | null) {
  const onlyString: string = maybeString; // Error
  const ignoreUndefinedAndNull: string = maybeString!; // Ok
}
复制代码

2.调用函数时忽略undefine类型

type NumGenerator = () => number;

function myFunc(numGenerator: NumGenerator | undefined) {
  const num1 = numGenerator(); // Error
  const num2 = numGenerator!(); //OK
}
复制代码

确定赋值断言

在 TypeScript 2.7 版本中引入了确定赋值断言,即允许在实例属性和变量声明后面放置一个 ! 号,从而告诉 TypeScript 该属性会被明确地赋值。为了更好地理解它的作用,我们来看个具体的例子:

let x: number;
initialize();
// Variable 'x' is used before being assigned.(2454) 
//无法重新声明块范围变量“x”
console.log(2 * x); // Error

function initialize() {
  x = 10;
}
复制代码

很明显该异常信息是说变量 x 在赋值前被使用了,要解决该问题,我们可以使用确定赋值断言:

let x!: number;
initialize();
console.log(2 * x); // Ok

function initialize() {
  x = 10;
}
复制代码

通过 let x!: number; 确定赋值断言,TypeScript 编译器就会知道该属性会被明确地赋值。

联合类型

联合类型

联合类型表示的值可能是多种不同类型当中的某一个。比如,A | B 联合类型的某个值就可能是 A 类型,也可能是 B 类型。

const sayHello = (name:string | null) => {
    //...
};

sayHello('mwq');
sayHello(null);
复制代码

此外,对于联合类型来说,可能还会遇到以下用法:

let num: 1 | 2 = 1;
type Cap = "butt" | "round" | "square";
复制代码

可辨识联合类型

这种类型的本质是结合联合类型和字面量类型的一种类型保护方法。如果一个类型是多个类型的联合类型,且多个类型含有一个公共属性,那么就可以利用这个公共属性,来创建不同的类型保护区块。它包含3个要点:可辨识、联合类型和类型守卫

  1. 可辨识

可辨识要求联合类型中的每个元素都含有一个单例类型属性:

enum CarTransmission {
  Automatic = 200,
  Manual = 300
}

interface Motorcycle {
  vType: "motorcycle"; 
  make: number; // year
}

interface Car {
  vType: "car"; 
  transmission: CarTransmission
}

interface Truck {
  vType: "truck"; 
  capacity: number; // tons
}
复制代码

在上述代码中,我们分别定义了MotorcycleCarTruck三个接口,在这些接口中都包含一个vType属性,该属性被称为可辨识的属性,而其它的属性只跟特性的接口相关。

  1. 联合类型

基于前面定义了三个接口,我们可以创建一个Vehicle联合类型:

type Vehicle = Motorcycle | Car | Truck;
复制代码

现在我们就可以开始使用Vehicle联合类型,对于Vehicle类型的变量,它可以表示不同类型的车辆。

  1. 类型守卫

下面我们来定义一个evaluatePrice方法,该方法用于根据车辆的类型、容量和评估因子来计算价格,具体实现如下:

const EVALUATION_FACTOR = Math.PI; 

function evaluatePrice(vehicle: Vehicle) {
  return vehicle.capacity * EVALUATION_FACTOR;
}

const myTruck: Truck = { vType: "truck", capacity: 9.5 };
evaluatePrice(myTruck);
复制代码

对于以上代码,TypeScript 编译器将会提示以下错误信息:

Property 'capacity' does not exist on type 'Vehicle'.      //类型“Vehicle”上不存在属性“capacity”。
Property 'capacity' does not exist on type 'Motorcycle'.   //类型“Motorcycle”上不存在属性“capacity”
复制代码

原因是在Motorcycle接口中,并不存在capacity属性,而对于Car接口来说,它也不存在capacity属性。那么,现在我们应该如何解决以上问题呢?这时,我们可以使用类型守卫。下面我们来重构一下前面定义的evaluatePrice方法,重构后的代码如下:

function evaluatePrice(vehicle: Vehicle) {
  switch(vehicle.vType) {
    case "car":
      return vehicle.transmission * EVALUATION_FACTOR;
    case "truck":
      return vehicle.capacity * EVALUATION_FACTOR;
    case "motorcycle":
      return vehicle.make * EVALUATION_FACTOR;
  }
}
复制代码

在以上代码中,我们使用switchcase运算符来实现类型守卫,从而确保在evaluatePrice方法中,我们可以安全地访问vehicle对象中的所包含的属性,来正确的计算该车辆类型所对应的价格。

交叉类型

在 TypeScript 中交叉类型是将多个类型合并为一个类型。通过 & 运算符可以将现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。

type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };

let point: Point = {
  x: 1,
  y: 1
}
复制代码

同名基础类型属性的合并

如果在合并多个类型的过程中,刚好出现某些类型存在相同的成员,但对应的类型又不一致,例如下述例子,会发生什么情况呢?

interface X {
  c: string;
  d: string;
}

interface Y {
  c: number;
  e: string
}

type XY = X & Y;
复制代码

从上面可以看出,接口X和Y都有一个相同的属性c,但它俩类型不一样,此时c的类型会不会是 stringnumber 呢?我们一起来看一下:

let p:XY = {
      c:6,        //不能将类型“number”分配给类型“never”
      d:'d',
      e:'e'
    }

let q:XY = {
      c:'c',     //不能将类型“string”分配给类型“never”
      d:'d',
      e:'e'
    }
复制代码

哦吼,属性c的类型竟然变成了 never,我们来分析以下为什么。交叉后属性c的类型为 string & number,即属性c的类型既可以是 string 又可以是 number,很明显这种类型不存在,因此交叉后c的类型为 never

同名非基础类型属性的合并

上述示例中,刚好接口 X 和接口 Y 中内部成员c的类型都是基本数据类型,那么如果是非基本数据类型的话,又会是什么情形。我们来看个具体的例子:

interface D { d: boolean; }
interface E { e: string; }
interface F { f: number; }

interface A { x: D; }
interface B { x: E; }
interface C { x: F; }

type ABC = A & B & C;

let abc: ABC = {
  x: {
    d: true,
    e: 'semlinker',
    f: 666
  }
};

console.log('abc:', abc);
复制代码

我们看一下打印结果:

logabc.png

由此可知,在混入多个类型时,若存在相同的成员,且成员类型为非基本数据类型,那么是可以成功合并的。

类型守卫

类型保护是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内。目前主要有四种的方式来实现类型保护:

类型判断:typeof

function test(input: string | number) {
  if (typeof input === 'string') {
    // 这里 input 的类型「收紧」为 string
  } else {
    // 这里 input 的类型「收紧」为 number
  }
}
复制代码

typeof类型保护只支持两种形式:typeof v === "typename"typeof v !== typename"typename" 必须是 "number""string""boolean""symbol"

实例判断:instanceof

class Foo {}
class Keep {}

function test(input: Foo | Keep) {
  if (input instanceof Foo) {
    // 这里 input 的类型「收紧」为 Foo
  } else {
    // 这里 input 的类型「收紧」为 Keep
  }
}
复制代码

属性判断:in

interface Admin {
  name: string;
  privileges: string[];
}

interface Employee {
  name: string;
  startDate: Date;
}

type UnknownEmployee = Employee | Admin;

function printEmployeeInformation(emp: UnknownEmployee) {
  console.log("Name: " + emp.name);
  if ("privileges" in emp) {
    console.log("Privileges: " + emp.privileges);
  }
  if ("startDate" in emp) {
    console.log("Start Date: " + emp.startDate);
  }
}
复制代码

自定义守卫

自定义守卫的语法结构为: {形参} is {类型} ,由于自定义守卫的本质是一种「类型断言」,因而在自定义守卫函数中,你可以通过任何逻辑来实现对类型的判断。

function isNumber(x: any): x is number {
  return typeof x === "number";
}

function isString(x: any): x is string {
  return typeof x === "string";
}
复制代码

TypeScript函数

声明和调用函数

我们先根据下图来看一下TS与JS函数的区别:

tsVSjs.png

在JS中对函数可执行的操作有很多:可以作为参数传给其他函数;可以作为函数的返回值;可以赋值给对象和原型;可以赋予属性;可以读取属性等。而TS通过丰富的类型系统延续了这一传统。

通常,我们会显示注解函数的参数。TS能推导出函数体中的类型,但多数情况下无法推导出参数的类型。返回类型能推导出来,不过也可以显示注解:

function add(a: number, b: number): number {
    return a + b
}
复制代码

上述例子声明函数使用的是具名函数句法,不过JS和TS至少还支持五种声明函数的方式:

//具名函数
function greet(name: string) {
    return 'hello' + name
}

//函数表达式
let greet2 = function(name: string) {
    return 'hello' + name
}

//箭头函数表达式
let greet3 = (name: string) => {
    return 'hello' + name
}

//箭头函数表达式简写形式
let greet4 = (name: string) => 
    return 'hello' + name

//函数构造方法
let greet5 = new Function('name', 'return "hello" + name')
复制代码

除了函数构造方法,其他几种句法在TS中都可以放心使用,都能确保类型安全。

为什么函数构造方法不安全呢?在编辑器中输入该例子你会发现其类型为Function(如下图)。这是一种可调用的对象(即可以在后面加上()),而且具有Function.prototype的所有原型方法。但是这里没有体现参数和返回值的类型,因此可以使用任何参数调用函数。

在TS中调用函数时,直接传入实参即可,TS将检查实参是否与函数形参的类型兼容。

add(1, 2)       //3
greet('mwq')    //'hello mwq'
复制代码

函数类型

ts中也有函数类型,用于描述一个函数,这种句法也称调用签名或类型签名:

type FnType = (x: number, y: number) => number
复制代码

完整的函数写法:

let myAdd: (x: number, y: number) => number = function(x: number, y: number): number {
  return x + y
}

// 使用 FnType 类型
let myAdd: FnType = function(x: number, y: number): number {
  return x + y
}

// ts 自动推导参数类型
let myAdd: FnType = function(x, y) {
  return x + y
}
复制代码

可选和默认参数

可选参数

与对象和元组一样,可以使用?把参数标记为可选。声明函数的参数时,必选的放在可选的前面:

function log(message: string, userId?: string) {
    let time = new Date().toLocaleTimeString()
    console.log(time, message, userId || 'Not signed in')
}

log('eat')              //下午2:38:19 eat Not signed in
log('game', 'mwq')      //下午2:38:19 game mwq
复制代码

默认参数

与JS一样,可以为可选参数提供默认值。这样在调用时无需传入参数的值。

function log(message: string, userId = 'Not signed in') {
    let time = new Date().toLocaleTimeString()
    console.log(time, message, userId)
}

log('eat')              //下午2:38:19 eat Not signed in
log('game', 'mwq')      //下午2:38:19 game mwq
复制代码

剩余参数

必要参数,默认参数和可选参数有个共同点:它们表示某一个参数。 有时,你想同时操作多个参数,或者你并不知道会有多少参数传递进来。 在JavaScript里,你可以使用arguments来访问所有传入的参数。在TypeScript里,你可以把所有参数收集到一个变量里:

function buildName(firstName: string, ...restOfName: string[]) {
  return firstName + " " + restOfName.join(" ");
}

let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
复制代码

剩余参数会被当做个数不限的可选参数。 可以一个都没有,同样也可以有任意个。

注解this的类型

现在有一个格式化日期的实用函数,如下所示:

function fancyDate(this:Date) {
    console.log(`${this.getDate()}/${this.getMonth()}/${this.getFullYear()}`);
}

fancyDate.call(new Date)       //1/8/2021
复制代码

如果函数使用this,需要在函数的第一个参数中声明this的类型。如果未声明,编辑器将会报错:

function fancyDate(this:Date) {
    console.log(`${this.getDate()}/${this.getMonth()}/${this.getFullYear()}`);
    //"this" 隐式具有类型 "any",因为它没有类型注释
}
复制代码

函数重载

在多数编程语言中,声明函数时一旦指定了特定的参数和返回类型,就只能使用相应的参数调用函数,而且返回值的类型始终如一。而JS是一门动态语言,势必需要多种方式调用一个函数的方法。不仅如此,有时输出的类型取决于输入的参数类型。

TS为了保证类型安全,支持了动态重载函数(有多个调用签名的函数)声明。如下示例:

// 重载签名(函数类型定义)
function toString(x: string): string;
function toString(x: number): string;

// 实现签名(函数体具体实现)
function toString(x: string | number) {
  return String(x)
}

let a = toString('hello') // ok
let b = toString(2) // ok
let c = toString(true) // error
复制代码

函数实现签名并不是重载的一部分,当至少具有一个函数重载的签名时,只有重载是可见的。最后一个实现签名对签名的形状并没有贡献,因此,要获得所需的行为,你需要添加额外的重载:

function toString(x: string): string;

function toString(x: number): string {
  return String(x)
}

toString(2) // error
复制代码

接口

接口简介

TypeScript的核心原则之一是对值所具有的结构进行类型检查。 它有时被称做“鸭式辨型法”或“结构性子类型化”。 在TypeScript里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。与类型别名相似,接口是一种命名类型的方式,这样就无需在行内定义。

接口与类型别名的区别

  1. 两者都可以用来描述对象或函数的类型,但是语法不同
type mwq = {
    name:string,
    age:number,
    alive:boolean
}
复制代码

此类型别名可以重写为下述接口:

interface mwq {
    name:string,
    age:number,
    alive:boolean
}
复制代码
  1. 两者都可以扩展,但是语法又有所不同。此外,请注意接口和类型别名不是互斥的。接口可以扩展类型别名,反之亦然

interface extends interface

interface PartialPointX { x: number }
interface Point extends PartialPointX { y: number }
复制代码

type alias extends type alias

type PartialPointX = { x: number }
type Point = PartialPointX & { y: number }
复制代码

interface extends type alias

type PartialPointX = { x: number }
interface Point extends PartialPointX { y: number }
复制代码

type alias extends interface

interface PartialPointX { x: number }
type Point = PartialPointX & { y: number }
复制代码
  1. 类型别名更通用,右边可以是任何类型,包括类型表达式(类型,外加&或|等类型运算符);而在接口声明中,右边必须为结构。下述例子便不可用接口重写:
type A = number
type B = A | string
复制代码
  1. 扩展接口时,TS将检查扩展的接口是否可赋值给被扩展的接口。例如:
interface A {
    good(x:number):string
    bad(x:number):string
}

interface B extends A {
    good(x:number):string
    bad(x:string):string    //接口“B”错误扩展接口“A”。
                            //属性“bad”的类型不兼容。
                            //不能将类型“(x: string) => string”分配给类型“(x: number) => string”。
                            //参数“x”和“x” 的类型不兼容。
                            //不能将类型“number”分配给类型“string”
}                              
复制代码
  1. 同一作用域中的多个同名接口将自动合并;同一作用域中的多个同名类型别名将导致编译错误。
interface Point { x: number; }
interface Point { y: number; }

const point: Point = { x: 1, y: 2 };
复制代码

对象类型

TypeScript 中的接口是一个非常灵活的概念,也常用于对对象进行描述:

interface Person {
  name: string;
  age: number;
}

let mwq: Person = {
  name: "mwq",
  age: 23,
};
复制代码

只读与可选属性

只读属性

一些对象属性只能在对象刚刚创建的时候修改其值。 你可以在属性名前用 readonly 来指定只读属性:

interface Point {
    readonly x: number;
    readonly y: number;
}

let p1: Point = { x: 10, 
                  y: 20 
                };

p1.x = 5; // error!
复制代码

设置只读属性时,什么时候用const,什么时候用readonly?

可选属性

接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。

interface Person {
    name:string;
    age?:number;
}
复制代码

如下示例:

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

let greeter = new Greeter("world");
复制代码

我们声明一个 Greeter类。这个类有3个成员:一个叫做 greeting 的属性,一个构造函数和一个 greet 方法。

你会注意到,我们在引用任何一个类成员的时候都用了 this。 它表示我们访问的是类的成员。

最后一行,我们使用 new 构造了 Greeter 类的一个实例。 它会调用之前定义的构造函数,创建一个 Greeter 类型的新对象,并执行构造函数初始化它。

继承

在TypeScript里,我们可以使用常用的面向对象模式。 基于类的程序设计中一种最基本的模式是允许使用继承来扩展现有的类。

class Person {
  public love: string;
  constructor(love: string) {
    this.love = love;
  }
  public sayLove() {
    console.log(`my love is ${this.love}`)
  }
}

class SuperPerson extends Person {
  public name: string;
  constructor(love: string, name: string) {
    super(love);
    this.name = name;
  }
  public sayName(){
    console.log(`my name is ${this.name}`)
  }
}

let me = new SuperPerson('HTML', 'mwq');
me.sayLove()
me.sayName()
复制代码

在构造函数里访问 this 的属性之前,我们 一定要调用 super()。 这个是TypeScript强制执行的一条重要规则。

公共、私有与受保护的修饰符

public

修饰的属性或方法是共有的,在任何地方都能访问,在TypeScript里,成员都默认为 public

class Animal {
    public name: string;
    public constructor(theName: string) { this.name = theName; }
    public move(distanceInMeters: number) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}
复制代码

private

当成员被标记成 private 时,它就不能在声明它的类的外部访问,只有在本类才能被访问。

class Animal {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

new Animal("Cat").name; // 错误: 'name' 是私有的.
复制代码

protected

修饰的属性或方法是受保护的在 本类子类 中能够访问。

class Person {
    protected name: string;
    constructor(name: string) { this.name = name; }
}

class Employee extends Person {
    private department: string;

    constructor(name: string, department: string) {
        super(name)
        this.department = department;
    }

    public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}

let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // 错误

复制代码

静态属性 static

使用 static 修饰的属性是通过类去访问,是每个实例共有的,同样 static 可以修饰方法,用 static 修饰的方法称为类方法,可以使用类直接调用

class Parent {
    static species: string = 'human'
    public name: string
    private money: number
    constructor(name: string) {
        this.name = name 
    }
    eat() {
        console.log(`${this.name}在吃饭`)
    }
}
console.log(Parent.species)     //'human'
复制代码

readonly修饰符

我们给属性添加上 readonly 就能保证该属性只读,不能修改,如果存在 static 修饰符,写在其后

class Parent {
    static readonly species: string = 'human'
    public name: string
    private money: number
    constructor(name: string) {
        this.name = name 
    }
    eat() {
        console.log(`${this.name}在吃饭`)
    }
}
Parent.species = 'dog'  //无法分配到 "species" ,因为它是只读属性
复制代码

抽象类 abstract

使用 abstract 关键字声明的类,我们称之为抽象类。抽象类不能被实例化,因为它里面包含一个或多个抽象方法。所谓的抽象方法,是指不包含具体实现的方法

abstract class Person {
  constructor(public name: string){}

  abstract say(words: string) :void;
}

const lolo = new Person();    //无法创建抽象类的实例
复制代码

抽象类不能被直接实例化,我们只能实例化实现了所有抽象方法的子类。具体如下所示:

abstract class Person {
  constructor(public name: string){}

  // 抽象方法
  abstract say(words: string) :void;
}

class Developer extends Person {
  constructor(name: string) {
    super(name);
  }
  
  say(words: string): void {
    console.log(`${this.name} says ${words}`);
  }
}

const lolo = new Developer("mwq");
lolo.say("I love ts!");     // mwq says I love ts!
复制代码

类方法重载

在前面的章节,我们已经介绍了函数重载。对于类的方法来说,它也支持重载。如下示例:

class ProductService {
    getProducts(): void;
    getProducts(id: number): void;
    getProducts(id?: number) {
      if(typeof id === 'number') {
          console.log(`获取id为 ${id} 的产品信息`);
      } else {
          console.log(`获取所有的产品信息`);
      }  
    }
}

const productService = new ProductService();
productService.getProducts(666); // 获取id为 666 的产品信息
productService.getProducts(); // 获取所有的产品信息
复制代码

泛型

泛型就像一个占位符一个变量,在使用的时候我们可以将定义好的类型像参数一样传入,原封不动的输出

泛型语法

先看下面例子:

function getValue(params: number):number {
    return params
}
复制代码

上述例子中我想返回一个值,这里我类型写死是number,但在实际中就未必是number了,我们可以通过泛型来解决这个问题,看下面的示例:

function getValue<T>(params: T): T {
    return params
}
复制代码

其中T相当于一个占位符,在方法(变量、接口等)后面添加<T>

如果我们需要传入多个参数,看如下示例:

function getValue<T,U>(params: T, message:U): T {
    console.log(message)
    return params
}

getValue<number, string>(1,'1')
复制代码

泛型接口

interface GenericIdentityFn<T> {
  (arg: T): T;
}
复制代码

泛型类

class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
  return x + y;
};
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享