1 、什么是typescript? TypeScript是Microsoft(微软)开发的一种开源编程语言,它充分利用了JavaScript原有的对象模型,并在此基础上进行了扩充,所以TypeScript是JavaScript的一个超集,也支持ECMAScript标准。TypeScript设计目标是开发大型应用,它可以编译成纯JavaScript,编译出来的JavaScript可以运行在任何一种JS运行环境中。
相比JS,TS引入了较为严格的类型系统,通过类型注解实现了静态类型检查,面向对象等特性供开发者使用,增强了代码的可读性和可维护性。
2、静态类型 or 动态类型? 通常,编程语言按照类型安全可以分为:强类型和弱类型
按照类型检查可以分为:静态类型?(Static Typing) 和动态类型 (Dynamic Typing)。
静态类型语言会在编译时进行数据类型检查。也就是说,静态类型语言,在编译前,变量的数据类型就确定了,变量值受限于其类型。因为typescript提供了静态类型系统,所以在编译时会进行类型检查。
例如:
let i : number = 10;
在编译前,变量i的数据类型就确定为number了,所以变量值(这里的10)必须归属于number,否则编译时会报错。如下所示:
由于现在的编辑器都很强大,默认通常都内置并启用了Validate,所以,甚至不用到编译时,在编码阶段就会给出语法错误提示。例如:
3、TypeScript的基础类型 TypeScript中的基础类型包括:null、undefined、boolean、number、string、symbol、数组/元祖、enum、any、unknown、void、never
3.1、null/undefined TypeScript里,undefined和null两者各自有自己的类型分别叫做undefined和null。通常在TypeScript项目的根目录下会存在一个tsconfig.json文件,该文件指定了用来编译TypeScript项目的根文件和编译选项。如果在tsconfig.json文件的"compilerOptions"配置项里设置了 “strictNullChecks”: false,则null和undefined是所有类型(never除外)的子类型,也就是说,可以把null和undefined赋值给任何类型(never除外)。
例如:
const ref: null = null;
let e:undefined = undefined;
var m: null = e;
var n: undefined = ref;
let a:number = e;
let b:number = ref;
let str1:string = ref;
let str2:string = e;
......
上面的代码都可以通过类型检查,但如果修改配置为"strictNullChecks": true,则后面几句都是语法错误,如下:
说明:强烈推荐"strictNullChecks": true设置,否则编程要处处留心‘空’的情况。
3.2、boolean/number/string/symbol 例如:
let isDone: boolean = false;
let num: number;
num = 123;
num = 0b1111011;
num = 0o173;
num = 0x7b;
let name: string = "smith";
let info: string = `Hello, my name is ${name}`;
const s: symbol = Symbol();
const m = 100n;
const v:BigInt = 100n;
const n = BigInt(100);
console.log(m === n)
......
代码解释:
第?1 行,声明了一个 number 类型最大值的变量 biggest,对于 number 类型来说,这个就是最大精度。
第?3-4 行,最大精度就是这个容器已经完全满了,无论往上加多少都会溢出,所以这两个值是相等的。
注意:在TypeScript中,number和BigInt类型虽然都表示数字,但是实际上两者类型是完全不同的,所以不能互相赋值。
3.3、数组/元祖 TypeScript中数组通常用于放同种类型的元素,当然也可以放不同类型的元素。元祖则表示一个已知元素数量和类型的数组,确切地说,是已知数组中每一个位置上的元素的类型,且各元素的类型不必相同。
数组的两种定义方式:
(1)在元素类型后面接上[],表示由此类型元素组成的一个数组
(2)使用数组泛型,Array<元素类型>
例如:
let list: number[] = [1, 2, 3];
let list: Array<number> = [1, 2, 3];
......
元组Tuple
例如:
let arr:[string,number?];
arr = ['smith'];
arr = ['smith',18];
......
3.4、enum enum类型是对JavaScript标准数据类型的一个补充,使用枚举类型可以为一组数值赋予友好的名字。
(1)数字枚举?(双向映射)
例如:
enum Direction {
NORTH,
SOUTH,
EAST,
WEST
}
let dir: Direction = Direction.NORTH;
console.log('方向是:',dir)
默认情况下,从0开始为元素编号,如上例,NORTH的初始值为 0,其余的成员的值会自增长。换句话说,Direction.SOUTH 的值为 1,Direction.EAST 的值为 2,Direction.WEST 的值为 3。当然也可以手动的指定成员的数值。?例如,我们将上面的例子改成从3开始编号:
例如:
enum Direction {
NORTH = 3,
SOUTH,
EAST,
WEST
}
console.log('方向是:',dir)
或者,全部都采用手动赋值:
例如:
enum Direction{ NORTH = 2, SOUTH = 4, EAST = 6, WEST = 8}
let dir: Direction = Direction.WEST;
let dirName: string = Direction[8];
console.log(dir)
console.log(dirName)
(2)字符串枚举
在?TypeScript 2.4 版本,允许我们使用字符串枚举。在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。
例如:
enum Direction {
NORTH = "NORTH",
SOUTH = "SOUTH",
EAST = "EAST",
WEST = "WEST"
}
(3)异构枚举
例如:
enum Enum {
A,
B,
C = "C",
D = "D",
E = 8,
F
}
let v1: Enum = Enum.A;
let v2: Enum = Enum.C;
let v3: Enum = Enum.F;
3.5、any 在typescript中,any是一种“顶级类型”,也就是说,任何值都可以冠以any类型。如果不希望类型检查器对值进行类型检查而是直接让它们通过编译阶段,那么就可以使用any类型:
例如:
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false;
……
any类型在对老代码进行typescript重构时是非常有用的,它允许你在编译时可选择地包含或移除类型检查。当你只知道一部分数据的类型时,any类型也是有用的。
比如,你有一个数组,它包含了不同的类型的数据:
let list: any[] = [1, true, "free"];
list[1] = 100;
3.6、unknown TypeScript 3.0引入的新类型,它是any类型对应的安全类型,也就是说,unknown是一种类型安全的“顶级类型”,任何值都能冠以unknown类型。
例如:
let value: unknown;
value = true;
value = 42;
value = "Hello World";
value = new TypeError();
……
在不想更明确地指定类型或不清楚具体类型时,可使用unknown。通常,直接使用unknown意义不大,但是你可借助“类型守卫”在块级作用域内“收窄”类型,并由此获得准确的类型检查。可以通过不同的方式将unknown 类型缩小为更具体的类型范围,包括 typeof 运算符,instanceof 运算符和自定义类型保护函数等。
例如:
function copy(x:unknown){
if (typeof x === 'object') {
return JSON.parse(JSON.stringify(x))
} else{
return x
}
}
function isNumberArray(value: unknown): value is number[] {
return (
Array.isArray(value) &&
value.every(element => typeof element === "number")
);
}
const unknownValue: unknown = [15, 23, 8, 4, 42, 16];
if (isNumberArray(unknownValue)) {
const max = Math.max(...unknownValue);
console.log(max);
}
unknown和any的主要区别是:unknown是类型安全的,不会破坏类型系统,不能将unknown类型的值直接赋给其它类型(any除外),如:
3.7、void void类型似乎是与any类型相反,它表示没有任何类型。 在typescript中,如果不关注函数的返回时,通常会将其返回值类型设为void
例如:
function warnUser(): void {
console.log("This is my warning message");
}
声明一个void类型的变量没有什么大用,因为你只能为它赋予undefined或null,
例如:
let unusable: void = undefined;
unusable = null;
但如果"strictNullChecks": true,则只能赋予undefined,不能赋予null,如下:
3.8、never never类型是范畴最小的类型,它是一个空集合,表示永远不存在值的类型,也就是说只要有值就不可能是never类型,所以,任何值都不能冠以never类型。never类型的使用场景:
(1)函数永远无法返回值的情况(例如:死循环、总是报错)
例如:
function infiniteLoop(): never{
while (true) {}
}
function error(message: string): never{
throw new Error(message);
}
(2)收窄类型,用于兜底
例如:
function move(direction: "up" | "down") {
switch (direction) {
case "up":
return 1;
case "down":
return -1;
}
return Error("永远不应该到这里");
}
说明:never类型其实是很重要的,它使得typescript的类型系统更完善,很多类型函数中,都会用到never类型。
3.9、object object表示非原始类型,也就是除number、string、boolean、symbol、null或undefined之外的类型。
例如:
let obj: object = { name: "LiLei",age:18};
let fn: object = function (x: number, y: number): number {
return x + y;
};
4、类型断言 类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。它没有运行时的影响,只是在编译阶段起作用。
类型断言有两种形式,例如:
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
5、类型别名 TS 允许我们为类型创建一个新名字,这个名字就是类型的别名,然后可以在多处使用这个别名。
类型别名的语法是:“type 名称 = 类型”,?这里的“类型”可以是字面量类型、基础类型、元组、函数、联合类型和交叉类型、甚至还可以是其它类型别名的组合。
例如:
type Str = string;
type ID = number | string;
type Fruit = 'apple' | 'pear' | 'orange' | 'banana'
type PositionType<T> = { x: T, y: T }
let p1: PositionType<number> = {
x: 3,
y: 5
}
let p2: PositionType<string> = {
x: 'right',
y: 'top'
}
假如我们有一个获取一个人姓名的函数,它接收一个参数,这个参数有可能直接是要获取的姓名,它是一个?string 类型,也有可能是调用一个函数获取的姓名,它是一个函数类型,我们来看一下这个例子:
例如:
function getName(n) {
if (typeof n === 'function') {
return n();
} else {
return n;
}
}
如果要给参数n 进行类型注解,那么它应该是 string | () => string ,即(n:string | () => string),显然这影响代码的可读性,而且string | () => string类型没法复用,所以这时就可以使用类型别名,
例如:
type NameParams = 'string' | () => 'string';
function getName(n: NameParams): string {
}
对于上面的NameParams类型别名 ,可以进一步拆解,
例如:
type Name = string;
type NameResolver = () => string;
type NameParams = Name | NameResolver;
function getName(n: NameParams): Name {
}
这里将NameParams 拆成了两个类型别名:Name 和 NameResolver ,分别处理 string 和 () => string 的情况,然后通过联合操作符(|)赋值给NameParams ;还带来了一个优势,函数的返回值很明确就是Name类型,这样Name类型变化,也能很好的反应这个变化。
使用类型别名时也可以在属性中引用自己,例如:
type Next<T> = {
val: T,
next?: Next<T>
}
let list: Next<string> = {
val: 'first',
next: {
val: 'second',
next: {
val: 'third'
}
}
}
6、面向对象 6.1、类 例如:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter = new Greeter("world");
console.log(greeter.greet())
6.2、继承 例如:
class Animal {
eat(): void {
console.log("动物的吃方法")
}
}
class Dog extends Animal {
public eat(): void {
console.log("小狗的吃方法")
super.eat();
}
}
let dog: Dog = new Dog();
dog.eat();
let dog2: Dog = new Animal();
dog2.eat();
6.3、public/protected/private修饰符 带有private或protected成员的类型,如果其中一个类型里包含一个private成员,那么只有当另外一个类型中也存在这样一个private成员,且它们都是来自同一处声明时,才认为这两个类型是兼容的。
例如:
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
class Dog extends Animal {
constructor() { super("Dog"); }
}
class Employee {
private name: string;
constructor(theName: string) { this.name = theName; }
}
let animal = new Animal("Cat");
let dog = new Dog();
console.log((dog as any).name);
animal = dog;
let employee = new Employee("Bob");
模拟真私有
6.4、readonly修饰符 你可以使用 readonly关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化。
例如:
class Person {
readonly sex: string = '男';
readonly name: string;
constructor(theName: string) {
this.name = theName;
}
}
let p = new Person("Tom");
p.name = "dats";
简写:
6.5、存取器 TypeScript支持通过getters/setters来截取对对象成员的访问,它能帮助你有效的控制对对象成员的访问。
例如:
let passcode = "secret passcode";
class Employee {
private _age: number;
get age(): number {
return this._age;
}
set age(newAge: number) {
if (passcode && passcode == "secret passcode") {
this._age = newAge;
}
else {
console.log("Error: Unauthorized update of employee!");
}
}
}
let employee = new Employee();
employee.age = 30;
对于存取器有下面几点需要注意的: 首先,存取器要求你将编译器设置为输出ECMAScript 5或更高。 不支持降级到ECMAScript 3。 其次,只带有 get不带有 set的存取器自动被推断为 readonly。 这在从代码生成 .d.ts文件时是有帮助的,因为利用这个属性的用户会看到不允许够改变它的值。
6.6、静态属性 用static修饰的属性即为“静态属性”,静态属性是从属于类的。
例如:
class Grid {
static origin = {x: 0, y: 0};
calculateDistanceFromOrigin(point: {x: number; y: number;}) {
let xDist = (point.x - Grid.origin.x);
let yDist = (point.y - Grid.origin.y);
return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
}
constructor (public scale: number) { }
}
let grid1 = new Grid(1.0);
let grid2 = new Grid(5.0);
console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));
6.7、抽象类 用abstract修饰的类即为“抽象类”,抽象类通常做为其它派生类的基类使用,它们一般不会直接被实例化。
例如:
abstract class Animal {
abstract makeSound(): void;
move(): void {
console.log('roaming the earch...');
}
}
抽象类中的抽象方法不包含具体实现并且必须在非抽象的派生类中实现。 抽象方法的语法与接口方法相似,两者都是定义方法签名但不包含实现。
例如:
abstract class Department {
constructor(public name: string) {}
printName(): void {
console.log('Department name: ' + this.name);
}
abstract printMeeting(): void;
}
class AccountingDepartment extends Department {
constructor() {
super('Accounting and Auditing');
}
printMeeting(): void {
console.log('The Accounting Department meets each Monday at 10am.');
}
generateReports(): void {
console.log('Generating accounting reports...');
}
}
let department: Department;
department = new Department();
department = new AccountingDepartment();
department.printName();
department.printMeeting();
department.generateReports();
6.8、重载 例如:
class Demo {
public log(): void;
public log(arg1: string): void;
public log(arg1: number, arg2: string): void;
log(arg1?: string | number, arg2?: string) {
}
}
7、接口 在 TypeScript中,接口的作用是:为你的代码或者第三方代码定义类型,约定好契约关系。简单理解,接口就是用来描述对象或者类的具体结构,约束它们的行为。
7.1、怎么定义接口? 和其它语言类似,?TypeScript中接口也是使用interface关键字来定义。
例如:
interface Point {
x: number;
y: number;
}
7.2、接口的实际使用 (1)可选属性(?)
interface ISumx {
x: number;
y?: number;
}
function sum({x, y}: ISumx): number {
return x;
}
sum({ x: 0 });
sum({ x: 0, y: 1});
(2)只读属性(readonly)
readonly和const的区别:const是在定义变量的时候使用,而readonly则是定义属性的时候使用。
(3)属性检查
传对象必须使用类型断言才能通过类型检查,如:create({ xx: 0, x: 0, y: 1 } as ICheckPoint)
变量声明类型注解,遵循与上面一样的规则:
(4)约定函数
interface IFunc {
sum: (x: number, y: number) => number;
}
const d: IFunc = {
sum(x: number, y: number): number{
return x + y;
}
}
上面在接口中定义函数,会约定sum函数接收两个number类型的参数,返回值为number类型。
(5)接口继承
接口继承使用extends关键字,可以让我们更方便灵活的复用。
interface Animal {
name: string;
eat(): void;
}
interface Flyable {
fly(): void;
}
interface Fish {
swim(): void;
}
interface FlyFish extends Animal, Fish, Flyable{
move: (m: string) => string
}
(6)实现接口
实现接口使用implement关键字
class Myfish implements FlyFish {
name: string
constructor(name: string) {
this.name = name
}
eat(){}
swim(){}
fly(){}
move(name: string) {
return `${name}在移动`
}
}
const fish = new Myfish('飞鱼');
console.log(fish.move(fish.name));
(7)接口继承class
(8)其它
interface MyArray {
[index: number]: any;
}
interface MyObj {
[attr: string]: any;
}
interface People{
(): void;
name: string;
age: number;
sayHi(): void;
}
function getPeople(): People{
let people: People = (() => {}) as People;
people.name = 'James';
people.age = 23;
people.sayHi = () => { console.log('Hi!') }
return people;
}
let p = getPeople();
p.sayHi();
7.3、interface vs?type 相同点:
(1)?都可以描述一个对象或者函数
interface User {
name: string;
age: number;
}
interface SetUser {
(name: string, age: number): void;
}
type User = {
name: string;
age: number
}
type SetUser = (name: string, age: number): void;
(2)都允许拓展
interface和type都可以拓展,只不过语法不一样而已,并且两者并不是互相独立的,也就是说interface可以extends type, type也可以extends interface。
// 接口同时继承interface和type
// 类型同时拓展于interface和type
因为类可以创建出类型,所以能够在允许使用接口的地方使用类。
不同点
(1)type可以声明基本类型别名、联合类型、元祖等类型
type Name = string;
interface Dog {wong()}
interface Cat {miao();}
type Pet = Dog | Cat;
type PetList = [Dog, Pet];
(2)type语句中还可以使用typeof获取实例的类型进行赋值
let div = document.createElement('div');
type B = typeof div;
(3)type其它操作
type Text = string | { text: string };
type NameLookup = Dictionary<string, Person>;
type Callback<T> = (data: T) => void;
type Pair<T> = [T, T];
type Coordinates = Pair<number>;
type Tree<T> = T | { left: Tree<T>, right: Tree<T> };
type T2 = () => void;
const foo: T2 = function(){ return 'aaa';};
const out = foo();
out.length;
(4)interface能够声明合并
interface User {
name: string;
age: number;
}
interface User {
sex: string;
}
总结:interface实现能实现的,type基本也能实现,但官方似乎更钟情于interface,所以,建议能用interface实现,就用interface,否则就用type。
8、高级类型 8.1、各种字面量类型
let a : 10 = 10;
let b : 1 | 2 | 3 | 4 | 5 | 6 = 6;
const str:'foo' = 'foo';
const type:'success' | 'warning'| 'danger' = 'success';
type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
animate(dx: number, dy: number, easing: Easing) {
if (easing === "ease-in") {
}
else if (easing === "ease-out") {
}
else if (easing === "ease-in-out") {
}
else {
}
}
}
8.2、交叉类型(Intersection Types) “T & U”表示交叉类型,其是将多个类型合并为一个类型,感觉称之为“并集类型”更合适。
例如:
interface Button {
type: string
text: string
}
interface Link {
alt?: string
href: string
}
const linkBtn: Button & Link = { type: 'danger', text: '跳转到百度', href: 'http://www.baidu.com' }
console.log(linkBtn);
8.3、联合类型(Union Types) “T | U” 表示联合类型,联合类型的语法规则和逻辑?“或” 的符号一致,表示其类型为连接的多个类型中的任意一个。
例如:
interface Button {
type: 'default' | 'primary' | 'danger'
text: string
}
const btn: Button = {
type: 'primary',
text: '按钮'
}
如果一个值是联合类型,我们只能访问此联合类型的所有类型所共有的成员。
例如:
interface Bird {
fly(): void;
eat(): void;
}
interface Fish {
swim(): void;
eat(): void;
}
var pet1: Bird = {
eat() {
console.log('鸟吃东西');
},
fly() {
console.log('鸟会飞');
}
}
var pet2: Fish = {
eat() {
console.log('鱼吃东西');
},
swim() {
console.log('鱼会游');
}
}
function getSmallPet(): Fish | Bird {
return pet1;
}
let pet3 = getSmallPet();
pet3.eat();
如果一个值的类型是A | B,就能够确定它包含了A和B中共有的成员。 这个例子里, Bird具有一个fly成员,但我们并不能确定Bird | Fish类型的变量就一定有fly方法,因为如果变量在运行时是Fish类型,那么就不具fly(),这时调用fly()就会出错。
8.4、条件类型(U ? X : Y) 条件类型的语法规则和三元表达式一致,经常用于一些类型不确定的情况。
T extends U ? X : Y? ? 即T是U或者U的子类型,则返回X类型,否则返回Y类型。
例如:
type NonNullable<T> = T extends null | undefined ? never : T;
let demo1: NonNullable<number>;
let demo2: NonNullable<string>;
let demo3: NonNullable<undefined | null>;
type Diff<T, U> = T extends U ? never : T;
type Filter<T, U> = T extends U ? T : never;
type T1 = Filter<"a" | "b" | "c" | "d", "a" | "c" | "f">;
type T2 = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">;
8.5、各种内置的类型操作
9、泛型 泛型允许我们在定义函数、接口、type或类的时候先给出类型标记(“类型变量”) 进行类型占位,然后在使用的时候再传递具体的类型给“类型变量”,这样通过“类型变量”就可以实现不同的类型复用同一份代码,所以,泛型是一种创建可复用代码的工具。很多时候我们无法准确定义一个类型,它可以是多种类型,这种情况下我们习惯用 any 来指定它的类型,代表它可以是任意类型。any 虽好用,但是它并不是那么安全的,这时候应该更多考虑泛型。
9.1、泛型函数 例如:
function identity<T>(arg: T): T {
return arg;
}
9.2、泛型接口 例如:
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
9.3、泛型类 例如:
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; };
9.4、泛型约束 例如:
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
loggingIdentity({length: 10, value: 3});
9.5、使用泛型来扩展泛型 例如:
function copyFields<T extends U, U>(target: T, source: U): T {
for (let id in source) {
(<any>target)[id] = (<any>source)[id];
}
return target;
}
let x = { a: 1, b: 2, c: 3, d: 4 };
console.log(x);
let y = copyFields(x, { b: 10, d: 20});
console.log(y);
这个描述表示的是,我们接受两个泛型类型的参数用作函数的参数,而第一个类型要被第二个类型所约束,即第二个类型的对象属性必须存在于第一个类型的对象属性列表中。否则就会报错。我们可以将第一个类型看成是子类而第二个类型看成是父类,但是要求并不如继承那样严格罢了。
9.6、在泛型中使用类类型:Using Class Types in Generics 我们可以讲一个类作为类型传入到泛型声明的函数中。所以我们需要对其做一个约束:我们判断传入的类型是否存在一个new的函数,且这个函数返回一个该类型的对象。
例如:
function create<T>(c: {new(): T; }): T {
return new c();
}
这个代码分为几个部分。接受的参数为c: {new(): T; },表示传入的参数名为c,它有一个名为new()的属性(这个属性恰好就是构造函数),且这个属性的返回值为T,这就决定了传入的c是类类型,即类的类型而不是对象,作为一个参数传入函数中。如果要理解这个模型我们可以借助接口章节的范例来理解。其次,我们定义了返回类型为T的返回值,而在函数体内,我们通过new来新建类型c的对象。
总结:一般来说,在决定是否使用泛型时,应该满足两个标准: (1)当函数、接口或类要处理多种数据类型时 (2)当函数、接口或类在多个位置使用该数据类型时
|