TypeScript是一种由微软开发的自由和开源的编程语言、是JavaScript的一个超集,TypeScript 在 JavaScript 的基础上添加了可选的静态类型和基于类的面向对象编程。
TypeScript 与JavaScript两者的特性对比,主要表现为以下几点:
TypeScript是一个应用程序级的JavaScript开发语言。(这也表示TypeScript比较牛逼,可以开发大型应用,或者说更适合开发大型应用)TypeScript是JavaScript的超集,可以编译成纯JavaScript。这个和我们CSS离的Less或者Sass是很像的,我们用更好的代码编写方式来进行编写,最后还是有好生成原生的JavaScript语言。TypeScript跨浏览器、跨操作系统、跨主机、且开源。由于最后他编译成了JavaScript所以只要能运行JS的地方,都可以运行我们写的程序,设置在node.js里。TypeScript始于JavaScript,终于JavaScript。遵循JavaScript的语法和语义,所以对于我们前端从业者来说,学习前来得心应手,并没有太大的难度。TypeScript可以重用JavaScript代码,调用流行的JavaScript库。TypeScript提供了类、模块和接口,更易于构建组件和维护。Undefined
Number
var num:number = 24; num = 8; console.log(num) //打印正确 8 // 如果给num赋一个字符串或者其他类型的值 num = "kobe" //会直接报错Boolean
var flag:boolean = true;flag = false //正确 flag = 123 //错误String
var str:string = "kobe" str = "sunday" //正确 str = 24; //报错Array
第一种:
var arr:number[] = [1,2,3]; // 正确 var arr:string[] = ['1','2','3']; // 正确 var arr:number[] = [1,2,'3']; //报错同理在字符串数组中有其他类型也会报错第二种:
var arr:Array<number> = [1,2,3]; // Array<number>泛型语法 和第一种一样,定义了数组类型之后,元素不可以有其他类型Enum
定义一些带名字的常量,使用枚举可以清晰的表达意图或创建一组有区别的用例,TypeScipt支持数字和基于字符串的枚举。
数字枚举
enum Direction { NORTH, // NORTH = 3, 也可以设置初始值 SOUTH, EAST, WEST, } let dir: Direction = Direction.NORTH; /* 默认情况下,NORTH 的初始值为 0,其余的成员会从 1 开始自动增长。换句话说,Direction.SOUTH 的值为 1,Direction.EAST 的值为 2,Direction.WEST 的值为 3。上面的枚举示例代码经过编译后会生成以下代码:*/ "use strict"; var Direction; (function (Direction) { Direction[(Direction["NORTH"] = 0)] = "NORTH"; Direction[(Direction["SOUTH"] = 1)] = "SOUTH"; Direction[(Direction["EAST"] = 2)] = "EAST"; Direction[(Direction["WEST"] = 3)] = "WEST"; })(Direction || (Direction = {})); var dir = Direction.NORTH;字符串枚举
在 TypeScript 2.4 版本,允许使用字符串枚举。在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。
enum Direction { NORTH = "NORTH", SOUTH = "SOUTH", EAST = "EAST", WEST = "WEST", }异构枚举
是数字和字符串的混合:
enum Enum { A, B, C = "C", D = "D", E = 8, F, }以上代码对于的 ES5 代码如下:
"use strict"; var Enum; (function (Enum) { Enum[Enum["A"] = 0] = "A"; Enum[Enum["B"] = 1] = "B"; Enum["C"] = "C"; Enum["D"] = "D"; Enum[Enum["E"] = 8] = "E"; Enum[Enum["F"] = 9] = "F"; })(Enum || (Enum = {}));通过观察上述生成的 ES5 代码,我们可以发现数字枚举相对字符串枚举多了 “反向映射”:
console.log(Enum.A) //输出:0 console.log(Enum[0]) // 输出:AVoid
某种程度上来说,void 类型像是与 any 类型相反,它表示没有任何类型。当一个函数没有返回值时,你通常会见到其返回值类型是 void:
// 声明函数返回值为void function warnUser(): void { console.log("This is my warning message"); }以上代码编译生成的 ES5 代码如下:
"use strict"; function warnUser() { console.log("This is my warning message"); }需要注意的是,声明一个 void 类型的变量没有什么作用,因为它的值只能为 undefined 或 null:
let unusable: void = undefined;Null 和 Undefined
let u: undefined = undefined; let n: null = null;默认情况下null和 undefined 是所有类型的子类型。 就是说你可以把 null 和 undefined 赋值给 number 类型的变量。然而,如果你指定了--strictNullChecks 标记,null 和undefined 只能赋值给 void 和它们各自的类型。
Any 任意类型
任何类型都可以被归为any类型,所以any类型也称为全局超级类型;
let notSure: any = 666; notSure = "Semlinker"; notSure = false; // typecscript 允许我们对any类型的值执行任何操作,不需要事先执行任何形式的检查 let value: any; value.foo.bar; // OK value.trim(); // OK value(); // OK new value(); // OK在许多场景下,这太宽松了。使用 any 类型,可以很容易地编写类型正确但在运行时有问题的代码。如果我们使用 any 类型,就无法使用 TypeScript 提供的大量的保护机制。为了解决 any 带来的问题,TypeScript 3.0 引入了 unknown 类型。
Unkonw
就像所有类型都可以赋值给 any,所有类型也都可以赋值给 unknown。这使得 unknown 成为 TypeScript 类型系统的另一种顶级类型(另一种是 any)。下面我们来看一下unknown类型的使用示例:
let value: unknown; value = true; // OK value = 42; // OK value = "Hello World"; // OK value = []; // OK value = {}; // OK value = Math.random; // OK value = null; // OK value = undefined; // OK value = new TypeError(); // OK value = Symbol("type"); // OK对 value变量的所有赋值都被认为是类型正确的。但是,当我们尝试将类型为 unknown的值赋值给其他类型的变量时会发生什么?
let value: unknown; let value1: unknown = value; // OK let value2: any = value; // OK let value3: boolean = value; // Error let value4: number = value; // Error let value5: string = value; // Error let value6: object = value; // Error let value7: any[] = value; // Error let value8: Function = value; // Error 复制代码unknown类型只能被赋值给 any 类型和 unknown 类型本身。直观地说,这是有道理的:只有能够保存任意类型值的容器才能保存 unknown类型的值。毕竟我们不知道变量value中存储了什么类型的值。
现在让我们看看当我们尝试对类型为 unknown 的值执行操作时会发生什么。以下是我们在之前any 章节看过的相同操作:
let value: unknown; value.foo.bar; // Error value.trim(); // Error value(); // Error new value(); // Error value[0][1]; // Error 复制代码将 value变量类型设置为 unknown后,这些操作都不再被认为是类型正确的。通过将 any类型改变为unknown类型,我们已将允许所有更改的默认设置,更改为禁止任何更改。
Tuple (元组类型)
元组类型是数组类型的一种,可以在数组中定义多类型元素
var arr:[number,string] = [1,'sunday'] //正确 var arr:[number,string] = [1,2] //错误 let tupleType: [string, boolean]; tupleType = ["Semlinker", true];在元组初始化的时候,如果出现类型不匹配的话,比如:
tupleType = [true, "Semlinker"];此时,TypeScript 编译器会提示以下错误信息:
[0]: Type 'true' is not assignable to type 'string'. [1]: Type 'string' is not assignable to type 'boolean'.很明显是因为类型不匹配导致的。在元组初始化的时候,我们还必须提供每个属性的值,不然也会出现错误,比如:
tupleType = ["Semlinker"];此时,TypeScript 编译器会提示以下错误信息:
Property '1' is missing in type '[string]' but required in type '[string, boolean]'.never
never 类型表示的是那些永不存在的值的类型。 例如,never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型。
// 返回never的函数必须存在无法达到的终点 function error(message: string): never { throw new Error(message); } function infiniteLoop(): never { while (true) {} }在 TypeScript 中,可以利用 never 类型的特性来实现全面性检查,具体示例如下:
type Foo = string | number; function controlFlowAnalysisWithNever(foo: Foo) { if (typeof foo === "string") { // 这里 foo 被收窄为 string 类型 } else if (typeof foo === "number") { // 这里 foo 被收窄为 number 类型 } else { // foo 在这里是 never const check: never = foo; } } 复制代码注意在 else 分支里面,我们把收窄为 never 的 foo 赋值给一个显示声明的 never 变量。如果一切逻辑正确,那么这里应该能够编译通过。但是假如后来有一天你的同事修改了 Foo 的类型:
type Foo = string | number | boolean; 复制代码然而他忘记同时修改 controlFlowAnalysisWithNever 方法中的控制流程,这时候 else 分支的 foo 类型会被收窄为 boolean 类型,导致无法赋值给 never 类型,这时就会产生一个编译错误。通过这个方式,我们可以确保
controlFlowAnalysisWithNever方法总是穷尽了 Foo 的所有可能类型。 通过这个示例,我们可以得出一个结论:使用 never 避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码。
应用场景:
当你比TypeScript 更了解某个值的详细信息,发生在你清楚地知道一个实体具有比它现有类型更确切的类型。
通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。类型断言好比其他语言里的类型转换,但是不进行特殊的数据检查和解构。它没有运行时的影响,只是在编译阶段起作用。
类型断言有两种形式:
尖括号语法
let someValue: any = "this is a string"; let strLength: number = (<string>someValue).length;as语法
let someValue: any = "this is a string"; let strLength: number = (someValue as string).length;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); } }typeof 关键字(与JavaScript 的typeof 同)
/* typeof 类型保护只支持两种形式:typeof v === "typename" 和 typeof v !== typename,"typename" 必须是 "number", "string", "boolean" 或 "symbol"。 但是 TypeScript 并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。*/ function padLeft(value: string, padding: string | number) { if (typeof padding === "number") { return Array(padding + 1).join(" ") + value; } if (typeof padding === "string") { return padding + value; } throw new Error(`Expected string or number, got '${padding}'.`); }instanceof 关键字
interface Padder { getPaddingString(): string; } class SpaceRepeatingPadder implements Padder { constructor(private numSpaces: number) {} getPaddingString() { return Array(this.numSpaces + 1).join(" "); } } class StringPadder implements Padder { constructor(private value: string) {} getPaddingString() { return this.value; } } let padder: Padder = new SpaceRepeatingPadder(6); if (padder instanceof SpaceRepeatingPadder) { // padder的类型收窄为 'SpaceRepeatingPadder' }自定义类型保护的关键字
function isNumber(x: any): x is number { return typeof x === "number"; } function isString(x: any): x is string { return typeof x === "string"; }联合类型
联合类型通常和null和undefined一起使用。
const sayHello = (name: string | undefined) => { /* ... */ }; // name 的类型是string|undefined (将string或undefined的值传给sayHello函数)sayHello("Semlinker");sayHello(undefined);可辨识联合(又称为代数数据类型或标签联合类型)
本质:联合类型和字面量类型的一种类型保护方法
如果一个类型是多个类型的联合类型,且多个类型含有一个公共属性,那么就可以利用这个公共属性,来创建不同的类型保护区块。
1.可辨识
可辨识要求联合类型中的每个元素都含有一个单例类型属性,比如:
enum CarTransmission { Automatic = 200, Manual = 300 } interface Motorcycle { vType: "motorcycle"; // discriminant make: number; // year } interface Car { vType: "car"; // discriminant transmission: CarTransmission } interface Truck { vType: "truck"; // discriminant capacity: number; // in tons } 复制代码在上述代码中,我们分别定义了 Motorcycle、 Car 和 Truck 三个接口,在这些接口中都包含一个 vType 属性,该属性被称为可辨识的属性,而其它的属性只跟特性的接口相关。
2.联合类型
基于前面定义了三个接口,我们可以创建一个 Vehicle 联合类型:
type Vehicle = Motorcycle | Car | Truck; 复制代码现在我们就可以开始使用 Vehicle 联合类型,对于 Vehicle 类型的变量,它可以表示不同类型的车辆。
3.类型守卫
下面我们来定义一个 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'. Property 'capacity' does not exist on type 'Motorcycle'. 复制代码原因是在 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; } }在以上代码中,我们使用 switch 和 case 运算符来实现类型守卫,从而确保在 evaluatePrice 方法中,我们可以安全地访问 vehicle 对象中的所包含的属性,来正确的计算该车辆类型所对应的价格。
类型别名
类型别名会给一个类型起个新的名字,类型别名有时候和接口很像,但是可以作用于原始值。联合类型,元组以及其它任何你需要手写的类型。
type Message = string | string[]; let greet = (message: Message) => { // ... };一些关键字
extends:
T extends U ? X : Y; //上面的类型意思是,若 T 能够赋值给 U,那么类型是 X,否则为 Y。 // 原理是令 T' 和 U' 分别为 T 和 U 的实例,并将所有类型参数替换为 any,如果 T' 能赋值给 U',则将有条件的类型解析成 X,否则为Y。typeof
在 JS 中 typeof 可以判断一个变量的基础数据类型,在 TS 中,它还有一个作用,就是获取一个变量的声明类型,如果不存在,则获取该类型的推论类型。
举两个栗子:
interface Person { name: string; age: number; location?: string; } const jack: Person = { name: 'jack', age: 100 }; type Jack = typeof jack; // -> Person function foo(x: number): Array<number> { return [x]; } type F = typeof foo; // -> (x: number) => number[]Jack 这个类型别名实际上就是 jack 的类型 Person,而 F 的类型就是 TS 自己推导出来的 foo 的类型 (x: number) => number[]。
keyof 可以用来取得一个对象接口的所有 key 值:
interface Person { name: string; age: number; location?: string; } type K1 = keyof Person; // "name" | "age" | "location" type K2 = keyof Person[]; // "length" | "push" | "pop" | "concat" | ... type K3 = keyof { [x: string]: Person }; // string | numberin 可以遍历枚举类型:
type Keys = "a" | "b" type Obj = { [p in Keys]: any } // -> { a: any, b: any } 复制代码上面 in 遍历 Keys,并为每个值赋予 any 类型。
在条件类型语句中, 可以用 infer 声明一个类型变量并且对它进行使用,
我们可以用它获取函数的返回类型, 源码如下:
type ReturnType<T> = T extends ( ...args: any[] ) => infer R ? R : any; 复制代码其实这里的 infer R 就是声明一个变量来承载传入函数签名的返回值类型, 简单说就是用它取到函数返回值的类型方便之后使用。
还有一些内置类型别名,详情看文档
附带链接:https://juejin.im/post/5cf7d87cf265da1ba328b405交叉类型是把多个类型合并为一个类型,它包含了所需要的所有类型的特性。从字面上理解,可能会误认为是把取出几个类型交叉的(即交集)成员。
交叉类型的使用场景:Mixins、不适合用类来定义。
我觉得,交叉类型和Mixins有一点区别:交叉类型只是一个类型声明,用于类型约束;Mixins会给类增加成员,new对象时,对象会包含增加的成员属性。
TypeScript函数与 JavaScript函数的区别
一个函数有输入和输出,要在 TypeScript 中对其进行约束,需要把输入和输出都考虑到,其中函数声明的类型定义较简单:
function sum(x: number, y: number): number { return x + y; }输入多余的(或者少于要求的)参数,是不被允许的
let mySum: (x: number, y: number) => number = function (x: number, y: number): number { return x + y; };在 TypeScript 的类型定义中,=> 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。
用接口定义函数的形状:
interface SearchFunc { (source: string, subString: string): boolean; } let mySearch: SearchFunc; mySearch = function(source: string, subString: string) { return source.search(subString) !== -1; } // 采用函数表达式|接口定义函数的方式时,对等号左侧进行类型限制,可以保证以后对函数名赋值时保证参数个数、参数类型、返回值类型不变。箭头函数
常用语法
myBooks.forEach(() => console.log('reading')); myBooks.forEach(title => console.log(title)); myBooks.forEach((title, idx, arr) => console.log(idx + '-' + title); ); myBooks.forEach((title, idx, arr) => { console.log(idx + '-' + title); });如果你有一个实例方法是箭头函数那么它会保持 this。既然只有一个 this,这种函数不能使用 super 调用(super 只对原型成员有效)。你可以简单地在子类中重载它之前创建一个方法的副本来获得它。
class Adder { constructor(public a: number) {} // This function is now safe to pass around add = (b: string): string => { return this.a + b; } } class ExtendedAdder extends Adder { // Create a copy of parent before creating our own private superAdd = this.add; // Now create our override add = (b: string): string => { return this.superAdd(b); } }参数类型与返回类型
function createUserId(name: string, id: number): string { return name + id; }剩余参数(表示为 ...argumentName 出现在最后一个参数).
function iTakeItAll(first, second, ...allOthers) { console.log(allOthers); } iTakeItAll('foo', 'bar'); // [] iTakeItAll('foo', 'bar', 'bas', 'qux'); // ['bas','qux']函数类型
let IdGenerator: (chars: string, nums: number) => string; function createUserId(name: string, id: number): string { return name + id; } IdGenerator = createUserId;可选参数及默认参数
// 可选参数 function createUserId(name: string, id: number, age?: number): string { return name + id; } // 默认参数 function createUserId( name: string = "Semlinker", id: number, age?: number ): string { return name + id; }在声明函数时,可以通过 ? 号来定义可选参数,比如 age?: number 这种形式。在实际使用时,需要注意的是可选参数要放在普通参数的后面,不然会导致编译错误。
函数重载
函数重载或者方法重载是使用相同名称和不同参数数量或者类型创建多个方法的一种能力;
方法就是为同一个函数提供多个函数类型定义来进行函数重载,编译器会根据这个列表去处理函数的调用。
代码示例:
function add(a: number, b: number): number; function add(a: string, b: string): string; function add(a: string, b: number): string; function add(a: number, b: string): string; function add(a: Combinable, b: Combinable) { if (typeof a === "string" || typeof b === "string") { return a.toString() + b.toString(); } return a + b; }数组结构
let x: number; let y: number; let z: number; let five_array = [0,1,2,3,4]; [x,y,z] = five_array;数组展开运算符
let two_array = [0, 1]; let five_array = [...two_array, 2, 3, 4];数组遍历
let colors: string[] = ["red", "green", "blue"]; for (let i of colors) { console.log(i); }对象解构 ( 和js一样 )
let person = { name: "Semlinker", gender: "Male", }; let { name, gender } = person;对象展开运算符
let person = { name: "Semlinker", gender: "Male", address: "Xiamen", }; // 组装对象 let personWithAge = { ...person, age: 33 }; // 获取除了某些项外的其它项 let { name, ...rest } = person;对象的形状
可选|只读属性
interface Person { readonly name: string; age?: number; }类的属性和方法
class Greeter { // 静态属性 static cname: string = "Greeter"; // 成员属性 greeting: string; // 构造函数 - 执行初始化操作 constructor(message: string) { this.greeting = message; } // 静态方法 static getClassName() { return "Class name is Greeter"; } // 成员方法 greet() { return "Hello, " + this.greeting; } } let greeter = new Greeter("world");访问器
通过getter 和setter 方法来实现数据的封装和有效性校验,防止出现异常数据。
class Greeter { // 静态属性 static cname: string = "Greeter"; // 成员属性 greeting: string; // 构造函数 - 执行初始化操作 constructor(message: string) { this.greeting = message; } // 静态方法 static getClassName() { return "Class name is Greeter"; } // 成员方法 greet() { return "Hello, " + this.greeting; } } let greeter = new Greeter("world");类的继承
ECMAScript 的私有字段
class Greeter { // 静态属性 static cname: string = "Greeter"; // 成员属性 greeting: string; // 构造函数 - 执行初始化操作 constructor(message: string) { this.greeting = message; } // 静态方法 static getClassName() { return "Class name is Greeter"; } // 成员方法 greet() { return "Hello, " + this.greeting; } } let greeter = new Greeter("world");私有字段规则:
私有字段以 # 字符开头,有时我们称之为私有名称;每个私有字段名称都唯一地限定于其包含的类;不能在私有字段上使用 TypeScript 可访问性修饰符(如 public 或 private);私有字段不能在包含的类之外访问,甚至不能被检测到。使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据,这样用户就可以以自己的数据类型来使用组件。
设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:类的实例成员、类的方法、函数参数和函数返回值。
泛型(Generics)是允许同一个函数接受不同类型参数的一种模板。相比于使用 any 类型,使用泛型来创建可复用的组件要更好,因为泛型会保留参数类型。
泛型接口
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; };泛型变量
对刚接触 TypeScript 泛型的小伙伴来说,看到 T 和 E,还有 K 和 V 这些泛型变量时,估计会一脸懵逼。其实这些大写字母并没有什么本质的区别,只不过是一个约定好的规范而已。也就是说使用大写字母 A-Z 定义的类型变量都属于泛型,把 T 换成 A,也是一样的。下面我们介绍一下一些常见泛型变量代表的意思: T(Type):表示一个 TypeScript 类型 K(Key):表示对象中的键类型 V(Value):表示对象中的值类型 E(Element):表示元素类型泛型工具类型
typeofkeyofininferextendsPartial是什么
是一个表达式,被执行之后会返回一个函数,函数的入参分别为target、name、descriptor 对象,用于配置target 对象
分类
类装饰器(Class decorators)属性装饰器(Property decorators)方法装饰器(Method decorators)参数装饰器(Parameter decorators)类装饰器
用来装饰类的,它接收一个参数
function Greeter(target: Function): void { target.prototype.greet = function (): void { console.log("Hello Semlinker!"); }; } @Greeter class Greeting { constructor() { // 内部实现 } } let myGreeting = new Greeting(); myGreeting.greet(); // console output: 'Hello Semlinker!';属性装饰器
declare type PropertyDecorator = (target:Object, propertyKey: string | symbol ) => void;方法装饰器
declare type MethodDecorator = <T>(target:Object, propertyKey: string | symbol, descriptor: TypePropertyDescript<T>) => TypedPropertyDescriptor<T> | void;参数装饰器
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number ) => void学习参考资料:
https://juejin.im/post/5edd8ad8f265da76fc45362c#heading-67