TypeScript的学习实践一

778 阅读39分钟

🙌🙌自己整个TypeScript的学习实践笔记。💋💋学习TypeScript最重要的就是在书写代码时培养类型思维

环境搭建与编译执行

  • TypeScript 编写的程序并不能直接通过浏览器运行,我们需要先通过 TypeScript 编译器把TypeScript 代码编译成 JavaScript 代码
  • TypeScript 的编译器是基于 Node.js 的,所以我们需要先安装 Node.js
  • 安装 TypeScript 编译器。通过 NPM 包管理工具安装 TypeScript 编译器
npm i -g typescript

//安装后可以通过在终端中输入命令 tsc 来调用编译器
//查看当前 tsc 编译器版本
tsc -v
  • 编译执行。使用我们安装的 TypeScript 编译器 tsc 对后缀为.ts的文件进行编译
  • 编译 tsconfig.json 配置文件。我们先来一些有用的编译选项命令:
--outDir:指定编译文件输出目录
tsc --outDir ./dist ./src/hello.ts


--target:指定编译的代码版本目标,默认为 ES3
tsc --outDir ./dist --target ES6 ./src/hello.ts

--watch:在监听模式下运行,当文件发生改变的时候自动编译
tsc --outDir ./dist --target ES6 --watch ./src/hello.ts

//使用 --project 或 -p 指定配置文件目录
//会默认加载该目录下的 tsconfig.json 文件
tsc -p ./configs


//指定加载在相应目录下的tsconfig.json文件
tsc -p ./configs/ts.json
  • 如果每次编译都输入这么一大堆的编译选项命令是很繁琐的,所以我们可以编译 tsconfig.json 配置文件,简单的 tsconfig.json 配置如下:(有了单独的配置文件,我们就可以直接在终端中执行 tsc 命令)
{
    "compilerOptions": {
        "outDir": "../dist",
        "target": "es5",
        "watch": true
    },
    "include": ["../src/**/*"]
}

//'include'配置选项的意思是:
//当前使用TS编译要查找的目录或者文件
//因为并不需要让TS编译所有文件
---> ** : 表示所有目录(包括子目录)
---> * :  表示所有文件,
---> *.ts: 也可以指定文件类型例如 

类型系统初识

  • JavaScript是程序运行期间才做数据类型检查的语言。我们可以通过下面的代码感受一下:
function get(str) {
  return str.length;
}

//在IDE中不会出现错误但是一运行在浏览器中马上报错
//我们希望越早出现错误提示越好
//Cannot read property 'length' of undefined
get()
  • 此时我们希望程序在编译阶段(配合IDE、编辑器甚至可以在编码阶段)就发现一些潜在错误,避免程序在生产环境运行了以后再出现错误提示。

什么是类型系统

  • 类型系统包含两个重要组成部分:一个是类型标注(定义、注解),一个是类型检测(检查)
  • 类型标注就是在代码中给数据(变量、函数(参数、返回值))添加类型说明,当一个变量或者函数(参数)等被标注以后就不能存储或传入与标注类型不符合的类型
  • 有了类型标注, TypeScript 编译器就能按照标注对这些数据进行类型合法检测,有了类型标注,各种编辑器、IDE等就能进行智能提示
  • 类型检测就是对数据的类型进行检测。注意这里,重点是类型两字。类型系统检测的是类型,不是具体值(虽然,某些时候也可以检测值),比如某个参数的取值范围(1-100之间),我们不能依靠类型系统来完成这个检测,它应该是我们的业务层具体逻辑,类型系统检测的是它的值类型是否为数字!
  • 在 TypeScript 中,类型标注的基本语法格式为:数据载体:类型

基础的简单的类型标注

  • 基础类型:string、number、boolean
标注语法:

let title:string='lth';
let n:number=1;
let is:boolean=true;

  • 空和未定义类型:Null和Undefined
  • 注意事项:因为在 Null 和 Undefined 这两种类型有且只有一个值,在标注一个变量为 Null 和 Undefined 类型后,那就表示该变量不能修改了
//例子一:
let abc: null;
let bac: undefined;


//不能将类型“1”分配给类型“null”
abc = 1;

//不能将类型“22”分配给类型“undefined”。
bac=22
  • 注意事项:默认情况下 Null 和 Undefined是所有类型的子类型,此时可以把 Null 和 Undefined 赋值给number类型的变量。但是当你在当你在tsconfig.json中配置"strictNullChecks": true的情况下, Null 和 Undefined只能赋值给void和它们各自。
  • 也许在某处你想传入一个 string或null或undefined,你可以使用联合类型string | null | undefined。
let ccc: number;
// 在配置"strictNullChecks": true下
// 不能将类型“undefined”分配给类型“number”。
ccc = undefined;


let cc:number;
cc=null;

---------------------------------

let ccc: number;
// 在配置"strictNullChecks": true下
// 不能将类型“undefined”分配给类型“number”。
ccc = undefined;


let cc:number;
// 在配置"strictNullChecks": true下
// 不能将类型“null”分配给类型“number”。
cc=null;
  • 建议配置"strictNullChecks": true以避免不必要的错误
//例子二:不在tsconfig.json中配置
//"strictNullChecks": true的情况下:


let ccc: number;
ccc = undefined;

-------------------


let abcd:number;
abcd = null;
//ts编译器竟然能让下一行通过检测了
//这不是我们想要的结果
abcd.toFixed(1);

---------------------------------------

//合理利用ts编译可以帮助我们规避许多错误
//让代码规范写的更加严谨
//例如:利用ts的类型提示排除null值的情况
let ele = document.querySelector('div');
if (ele) {
    ele.style.display = 'none';
}
  • 如果一个变量声明了,但是未赋值,那么该变量的值为 undefined ,但是如果它同时也没有标注类型的话,默认类型为 any , any 类型后面有详细说明
let abc:number;
console.log(abc,'undefined','类型为number');

let abcd;
console.log(abcd,'undefined','类型为any');

  • 对象类型
  • 在 JavaScript 中,有许多的内置对象,比如:Object、Array、Date等等,我们可以通过对象的 构造函数 或者 类 来进行标注,这些可以统称为内置对象类型
let obj: Object = {};
let arr12: number[]=[1,2,3,4]
let arr21: Array<number> = [1,2,3];
let d1: Date = new Date();

//Oject有的object不一定有
//object有的Object一定有
//两者还是有区别的
let abcd:object={}
let abcde:Object={}
  • 我们还需要了解什么是包装对象?包装对象其实就是 JavaScript 中的 String 、 Number 、 Boolean ,我们知道 string 类 型String 类型并不一样,在 TypeScript 中也是一样,例子如下:
let a: string;
a = '1';

//“string”是基元,但“String”是包装器对象。
//报错:不能将类型“String”分配给类型“string”。
a = new String('1');


let b: String;
b = new String('1');

//字符串有的字符串对象一定有
//字符串对象有的字符串不一定有
b = '1';
  • 许多时候,我们可能需要自定义结构的对象。这个时候,我们可以使用字面量标注、接口、定义 类 或者 构造函数,这些可以统称为自定义对象类型
  • 下面是字面量标注的例子:
let user: {username:string, age:number} = {
    username: 'lth',
    age: 35
}
//类型“{ username: string; age: number; }”上不存在属性“gender”。
//user.gender;
user.age
  • 下面是接口的例子:
interface Person {
    username: string;
    age: number;
}

let user: Person = {
    username: 'lth',
    age: 35
}
let user1: Person = {
    username: 'lwl',
    age: 30
}

//错误的使用做法:
//接口只能作为类型标注使用,不能作为具体值
//报错:“Person”仅表示类型,但在此处却作为值使用。
// let user2 = Person;
  • 下面是类与构造函数的例子:
//在constructor内给属性添加上public前缀后
//这些属性会自动转换为类(实例对象)身上的属性
class Person {
    
    constructor(public username: string, public age: number) {

    }
}

//第一种写法
let person=new Person('lth', 35);
console.log(person.age);

//第二种写法
let user: Person = new Person('lwl', 35);

-------------------------------------


//上述的例子被ts编译后的代码如下:
class Person {
    constructor(username, age) {
        this.username = username;
        this.age = age;
    }
}
let person = new Person('lth', 35);
console.log(person.age);

let user = new Person('lwl', 35);
  • 使用类与构造函数的缺点是复杂,比如只想约束某个函数接收的参数结构,没有必要去定一个类,使用接口会更加简单,例子如下:
interface AjaxOptions {
    url: string;
    method: string;
}

function ajax(options: AjaxOptions) {}

ajax({
    url: '',
    method: 'get'
});

  • 数组类型:
  • TypeScript 中数组存储的类型必须一致,所以在标注数组类型的时候,同时要标注数组中存储的数据类型
  • 数组是一类具有相同特性的数据的有序集合,是有序的相同类型的数据,ts中数组存储的类型必须一致
  • 数组类型的标注方式:使用泛型标注:,例子如下:
let arr1: Array<number> = [1,2,3];
arr1.push(100);

//报错:类型“string”的参数不能赋给类型“number”的参数。
arr1.push('lth');
  • 数组类型的标注方式:使用简单标注:,例子如下:
let arr2: string[] = ['a', 'b', 'c'];
arr2.push('lth');
//类型“number”的参数不能赋给类型“string”的参数。
// arr2.push(100);



//类型“string”的参数不能赋给类型“number”的参数
let arr3: number[] = [1,2,3];
arr3.push('lth');

  • 元组类型
  • 元组类似数组,但是存储的元素类型不必相同,但是需要注意:使用元组类型在初始化数据时,初始化数据的个数以及对应位置标注类型必须一致,并且越界数据必须是元组标注中的类型之一(标注越界数据可以不用对应顺序--但是得满足其联合类型中的一种类型),例子如下:
let data1: [string, number] = ['lth', 1];

data1.push(110);
//类型“boolean”的参数不能赋给类型“string | number”的参数。
//越界数据必须是元组标注中的类型之一
data1.push(true);


//源具有 3 个元素,但目标仅允许 2 个。  
//注意初始化数据的个数以及对应位置标注类型必须一致
let data2: [string, number] = ['lth', 1,'2'];



//源具有 0 个元素,但目标需要 2 个。
//注意初始化数据的个数以及对应位置标注类型必须一致
let data3: [string, number] = [];


  • 枚举类型
  • 枚举的作用组织收集一组关联数据的方式,通过枚举我们可以给一组有关联意义的数据赋予一些友好的名字
  • 使用枚举类型时的名称命名习惯是推荐使用全大写,名称一旦定义便不可修改。如果value是数字,称为 数字类型枚举,如果value是是字符串,称为 字符串类型枚举。value不可以是是其它值,默认为数字:0。还要注意 key 不能是数字
  • 枚举值(value)可以省略。规则是如果第一个枚举值省略则默认为:0。如果非第一个枚举值省略,则这个枚举值是上一个数字枚举值值加一。如果上一个不是数字枚举值,则这个枚举值必须具有初始化表达式(不能省略后续枚举项必须手动赋值)。 例子如下:
enum HTTP_CODE {
    OK = 200,
    NOT_FOUND = 2,
    //3  上一个数字枚举值加一
    METHOD_NOT_ALLOWED,   
    //枚举成员不能具有数值名。
    //100='1',
    'LTH'='myName'
};

//定义以后无法在赋值 
//无法分配到 "OK" ,因为它是只读属性。
HTTP_CODE['OK'] = 200;


/**
* 编译后的思路解析
* HTTP_CODE['OK'] = 200;  即可以通过ok访问200
* HTTP_CODE['200'] = 'OK'; 也可以通过200访问ok
*/

//如果value是数字则访问可以是双向的
console.log(HTTP_CODE['OK']);
console.log(HTTP_CODE['200']);


//如果value是字符串则访问不是双向的
console.log(HTTP_CODE['LTH']);
console.log(HTTP_CODE['myName']);  //undefined


---------------------------------
//枚举类型的值,也可以是字符串类型

enum URLS  {
    USER_REGISETER = '/user/register',
    USER_LOGIN = '/user/login',
    // 如果前一个枚举值类型为字符串,则后续枚举项必须手动赋值
    INDEX = 1
}
URLS.USER_LOGIN;


  • 无值类型
  • 表示没有任何数据的类型,通常用于标注无返回值函数的返回值类型,函数默认标注类型为: void
function func(): void {
    //没有return 或者 return undefined
    
    
    //当 strictNullChecks 为 true 的情况下
    //只有 undefined 才可以赋值给 void
    return undefined;

    //在 strictNullChecks 为 false 的情况下
    //undefined 和 null 都可以赋值给 void
    //不能将类型“null”分配给类型“void”。
    // return null;
}
let vvv = func();

  • Never类型
  • 当一个函数永远不可能执行 return 的时候,返回的就是 never ,与 void 不同, void 是执行了return,只是没有值never 是不会执行 return ,比如抛出错误,导致函数终止执行
function abac() :never{
    throw new Error('error')
}

  • 任意类型
  • 有的时候,我们并不确定这个值到底是什么类型或者不需要对该值进行类型检测,就可以标注为 any类型
  • 一个变量申明未赋值且未标注类型的情况下,默认为 any 类型,任何类型值都可以赋值给 any 类型,any 类型也可以赋值给任意类型,any 类型有任意属性和方法
  • 标注为 any 类型,也意味着放弃对该值的类型检测,同时放弃 IDE 的智能提示,当指定tsconfig.js中的noImplicitAny配置为true,当函数参数出现隐含的 any 类型时会提示报错。
let aaa;


let a: any;
//任何类型值都可以赋值给 any 类型
a = 1;

let b: number;
//any 类型可以赋值给任意类型
b = a;



//any 类型有任意属性和方法
//放弃 IDE 的智能提示
a.ab;

--------------------------------------


//没有配置noImplicitAny的智能提示为:
//参数 "a" 隐式具有 "any" 类型,但可以从用法中推断出更好的类型


//指定tsconfig.js中的noImplicitAny配置为true后
//函数参数出现隐含的 any 类型时会提示报错。
//提示为:参数“a”隐式具有“any”类型。
function func(a:string) {
    a.indexOf('a', 1);
}


  • 未知类型
  • unknow,3.0 版本中新增,属于安全版的 any,但是与 any 不同的是:unknow 仅能赋值给 unknow、any。unknow 没有任何属性和方法
  • 因为any具有函数参数的隐式问题,可能会在函数参数赋值后调用了不应该调用的方法。
let c: unknown = 'lth';

let d: number = 1;

//没有报错
d.toFixed(1);

//unknow 没有任何属性和方法
//报错:对象的类型为 "unknown"。
c.toFixed(1);

//unknow类型仅能赋值给unknow\any
//报错不能将类型“unknown”分配给类型“number”。
d = c;

  • 函数类型:
  • 在 JavaScript 函数是非常重要的,在 TypeScript 也是如此。同样的,函数也有自己的类型标注格式:函数名称( 参数1: 类型, 参数2: 类型... ): 返回值类型;
function fn1(x: number, y: number): number {
    return x + y;
}

//函数名称( 参数1: 类型, 参数2: 类型... ): 返回值类型;

//方法一:
let v = fn1(1, 2);

//方法二:
let v: number = fn1(1, 2);

接口

  • TypeScript 的核心之一就是对值(数据)所具有的结构进行类型检查,除了一些前面说到基本类型标注,针对对象类型的数据,除了前面提到的一些方式以外,我们还可以通过: Interface(接口),来进行标注。
  • 接口(接口):对复杂的对象类型进行标注的一种方式,或者给其它代码定义一种契约(比如:类)
//我们可以通过这个接口来给一个数据进行类型标注
interface Point { 
	//接口中多个属性之间可以使用 逗号 或者 分号 进行分隔
	x: number; 
    	y: number;
}

//接口是一种 类型 ,不能作为 值 使用

//正确的使用方式
let p1: Point = { x: 100, y: 100 };

//错误的使用方式
let p1 = Point; 
  • 接口也可以定义可选的属性,通过?来进行标注
interface attentions{
    age:number,
    color?:number
}


let ats:attentions={
    //下一行是错误的写法
    // x:100

    //下一行是正确的写法
    //color是可选的属性
    age:100
}
  • 我们还可以在接口中通过 readonly 来标注属性为只读,当我们标注了一个属性为只读,那么该属性除了初始化以外,是不能被再次赋值的
interface attentions{
    readonly age:number,
    color?:number
}


let ats:attentions={
    //下一行是错误的写法
    // x:100

    //下一行是正确的写法
    //color是可选的属性
    age:100
}

//报错:无法分配到 "age" ,因为它是只读属性。
ats['age']=250
  • 有的时候,我们希望给一个接口中添加任意属性,可以通过索引类型来实现
  • 一种是数字类型索引,另一种是字符串类型索引,并且数字索引是字符串索引的子类型因为obj[1]可以转换为obj['1']
  • 注意:索引签名参数类型必须为 string 或 number 之一,但两者可同时出现。当同时存在数字类型索引和字符串类型索引的时候,数字类型的值类型必须是字符串类型的值类型或子类型
  • 例子一:
//给一个接口中添加任意属性
interface attention{
    color?:number;
    // 索引签名参数类型必须是string或者number之一

    [key:string]:number|undefined
}

let b:attention={
    x:200
}

b.z=100;

// 数字索引是字符串索引的子类型
b[0]=300
console.log(b['0']);

//{ '0': 300, x: 200, z: 100 }
console.log(b);
  • 例子二:
interface attention{
    color?:number;
    // 索引签名参数类型必须是string或者number之一

    [key:string]:number|undefined
    
    //数字类型的值类型必须是字符串类型的值类型或子类型
    //这里数字类型的值类型是字符串类型的值类型
    [props:number]:number
}


--------------------------------------


interface attention1{
    color?:number;
    // 索引签名参数类型必须是string或者number之一

    [key:string]:number|undefined
    
    //数字类型的值类型必须是字符串类型的值类型或子类型
    //这里数字类型的值类型不是字符串类型的值类型或子类型
    //报错:数字索引类型“object”不能赋给字符串索引类型“number | undefined”。
    // [props:number]:object
}

--------------------------


interface attention3{


    [key:string]:object
    
    //数字类型的值类型必须是字符串类型的值类型或子类型
    //这里数字类型的值类型是字符串类型子类型
    [props:number]:Date
}


-----------------------------------------

class Person {
    constructor(public username: string) {}
}
class Student extends Person {

}

interface Point {

  	// number也是object的子类型

    [key: string]: Person;
    [key: number]: Student;
}
  • 我们还可以使用接口描述函数,注意,如果使用接口来单独描述一个函数,是没 key 的,函数关注的的是参数和返回值
  • 不使用接口:
//不使用接口
function fn1(x: number, y: number): number {
    return x + y;
}
//不使用接口
function fn2(x: number, y: number): number {
    return x + y;
}

-----------------------------------

  • 使用接口例子一:


interface IFunc {
    //使用接口描述函数关注的是参数和返回值
    (x: number, y: number): number
}

//使用接口
let fn1IN: IFunc = function(a: number, b: number): number {
    return a + b;
}


//使用接口
let fn2IN: IFunc = function(a: number, b: number): number {
    return a + b;
}
  • 使用接口例子二:
interface IFunc {
    //使用接口描述函数关注的是参数和返回值
    (x: number, y: number): number
}

function todo(callback: IFunc) {
    // ....
    let v = callback(1, 2);
    // ....
}

todo( function(a: number, b: number): number {
    return a + b;
})


  • 使用接口例子三:
interface IEventFunc {
    (e: MouseEvent): void
}


function on(el: HTMLElement, evname: string, callback: IEventFunc) {

}

//let div: HTMLDivElement | null
let div = document.querySelector('div');

if (div) {
    on(div, 'click', function(e) {
        //(parameter) e: MouseEvent
        e.clientX
    });
}
  • 我们还可以对接口进行合并:,将多个同名的接口合并成一个接口,如果合并的接口存在同名的(注意是非函数成员)非函数成员,则必须保证他们类型一致,否则编译报错,接口中的同名函数则是采用重载(具体后期函数详解中讲解)
interface Box {
    height: number;
    width: number;
    fn(a: string): string;
}

interface Box {
    scale: number;
    //报错:属性“width”的类型必须为“number”
    //width: string;
    fn(a: number): number;
}

let box: Box = {
    height: 5,
    width: 6, 
    scale: 10,
    fn: function(a:any):any {
        return a;
    },
}

// 接口中的同名函数则是采用重载
// 根据传入不同的参数而返回不同类型的数据
// 它查找重载列表,尝试使用第一个重载定义。 
// 如果匹配的话就使用这个。 
// 因此,在定义重载的时候,一定要把最精确的定义放在最前面。
console.log(box.fn('1')); //'1'
console.log(box.fn(123)); //123

高级类型

  • 联合类型
  • 联合类型也可以称为多选类型,当我们希望标注一个变量为多个类型之一时可以选择联合类型标注,是的关系
function css(ele: Element, attr: string, value: string|number) {
    // ...
}

let box1 = document.querySelector('div');
if (box1) {
    css( box1, 'width', '100px' );
    css( box1, 'opactiy', 1 );
    //下面这一行报错
    // css( box1, 'opactiy', [1,2] );
}

  • 交叉类型
  • 交叉类型也可以称为合并类型,可以把多种类型合并到一起成为一种新的类型,是并且的关系。下面通过对一个对象进行扩展来演示交叉类型的使用:
interface o1 {x: number; y: string}
interface o2 {z: number}

let o: o1 & o2 = Object.assign({}, {x: 1, y: 'zmouse'}, {z: 2});

  • TypeScript 在编译过程中只会转换语法(比如扩展运算符,箭头函数等语法进行转换)。
  • 对于API (例如Promise.resolve();)是不会进行转换的,(也没必要转换,而是引入一些扩展库进行处理的)。
  • 如果我们的代码中使用了 target(此时我们的"target": "es5",) 中没有的 API ,则需要手动进行引入,默认情况下 TypeScript 会根据target 载入核心的类型库。
  • 当target 为 es5 时会载入: ["dom", "es5", "scripthost"]
  • target 为 es6 时会载入: ["dom", "es6", "dom.iterable", "scripthost"]
  • 如果代码中使用了这些默认载入库以外的代码,则可以通过 lib 选项来进行设置,例如我们使用了在"target": "es6",下使用了Promise.resolve();,可以配置"lib": ["es6","dom"]来满足使用需求。更多的lib的配置可以参考官网

  • 字面量类型
  • 有的时候,我们希望标注的不是某个类型,而是一个固定值,就可以使用字面量类型,配合联合类型会更有用
function setPosition(ele: Element, direction: 'left' | 'top' | 'right' | 'bottom') {
    //todo...
}

let box1 = document.querySelector('div');
if (box1) {
    setPosition(box1, 'left');
    //下一行报错:类型“"a"”的参数不能赋给类型“"left" | "top" | "right" | "bottom"”的参数。
    setPosition(box1, 'a');
}

  • 类型别名(type)
  • 有的时候类型标注比较复杂,这个时候我们可以类型标注起一个相对简单的名字
type dir = 'left' | 'top' | 'right' | 'bottom';
function setPosition(ele: Element, direction: dir) {
  //todo...
}
  • 使用类型别名定义函数类型,如果使用 type 来定义函数类型,和接口(interface)有点不太相同
//使用 interface 来定义函数类型
interface IFunc { 
	(a: string): string; 
}


-------------------------
//使用 type 来定义函数类型
type callback = (a: string) => string; 

let fn: callback = function(a) {}; 

// 或者直接 
let fn: (a: string) => string = function(a) {}
  • 类型别名的其他用法:
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
    if (typeof n === 'string') {
        return n;
    }
    else {
        return n();
    }
}

console.log(getName('1'));

console.log(
getName(
    function(){
    	console.log('this is function ');
        return '1';
    })
);
  • interface 与 type的区别
  • interface只能描述 object / class / function 的类型,同名 interface 自动合并,利于扩展
  • type不能重名,能描述所有数据

  • 类型推导
  • 每次都显式标注类型会比较麻烦,TypeScript 提供了一种更加方便的特性:类型推导。TypeScript 编译器会根据当前上下文自动的推导出对应的类型标注,
  • 这个过程发生在:初始化变量、设置函数默认参数值、返回函数值
let a: string = 'lth';
//可以简写为:
// let a = 'lth';

----------------------------

function fn(a = 1) {
    if (true) {
        return 'string';
    } else {
        return 100;
    }
}
//根据函数的返回值推导函数返回值的类型
fn(1)


--------------------------

let str='123';
// 报错:不能将类型“number”分配给类型“string”。
// str=2;


---------------------------------

//函数参数类型、函数返回值
//会根据对应的默认值和返回值进行自动推断

function fn(a = 1) {return a * a}

  • 类型断言
  • 有的时候,我们可能标注一个更加精确的类型(缩小类型标注范围),比如:let img = document.querySelector('#img');;我们可以看到 img 的类型为 Element,而 Element 类型其实只是元素类型的通用类型,如果我们去访问 src 这个属性是有问题的,我们需要把它的类型标注得更为精确:HTMLImageElement类型(才能访问src属性),这个时候,我们就可以使用类型断言,它类似于一种 类型转换:
  • 注意:断言只是一种预判,并不会数据本身产生实际的作用,即:类似转换,但并非真的转换了
//写法一
let img = <HTMLImageElement>document.querySelector('#img');

//写法二
let img = document.querySelector('#img') as HTMLImageElement;


---------------------------------

// 类型断言
// let img: Element | null
let img = document.querySelector('#img');
if (img) {
    //类型“Element”上不存在属性“src”。
    // img.src
    
    //写法一:
    // (<HTMLImageElement>img).src;

    //写法二:
    (img as HTMLImageElement).src;
}

函数详解

  • 函数的标注:一个函数的标注包含参数和返回值
//函数声明的写法
function fn1(a: string): string {
    return '';
}
//函数声明的写法经过ts编译后
function fn1(a) {
    return '';
}

-------------------------------


//函数表达式的写法一
let fn2: (a: string) => string = function(a) {
    return '';
}

//函数表达式的写法一经过ts编译后
var fn2 = function (a) {
    return '';
};


------------------------------------

//函数表达式的写法二
//函数默认标注类型为: void
//下面指定函数的返回类型是string
let fn33: (a:string) => string =function(a:string):string{

    //报错:不能将类型“undefined”分配给类型“string”
    return 
}

-------------------------------------------
//函数表达式的写法三
let fn34: (a:string) => string =function(a:string):string{

    return ''
}


  • 使用 type(类型别名)来定义函数类型
//使用type
type callback = (a: string) => string;


//b和a是可以取名不一样的
let fn3: callback = function(b) {
    return ''
}
  • 使用接口(interface)来定义函数类型:
interface ICallBack {
    (a: string): string;
}

let fn4: ICallBack = function(c) {
    return '';
}

  • 可选参数和默认参数:可选参数是通过参数名后面添加 ? 来标注该参数是可选的;默认参数可以理解有默认值的参数也是可选的,设置了默认值的参数可以根据值自动推导类型
  • 下面是可选参数的例子:
function css(el: HTMLElement, attr: string, val?: any) {
    //todo...
}

let div = document.querySelector('div');

//可选参数 可传入可不传入
if(div){
    css(div, 'width', '100px')
}
if(div){
    css(div, 'width');
}
  • 下面是是联合类型加默认值的例子:
function sort( items: Array<any>, order: 'desc'|'asc' = 'desc' ) {
    // 如果order是undefined则使用默认参数'desc'
    console.log(order,'默认参数');
}

//asc 默认参数
sort([1,2,3], 'asc');
//desc 默认参数
sort([1,2,3]);

//报错:类型“"lth"”的参数不能赋给类型“"desc" | "asc" | undefined”的参数。
//sort([1,2,3],'lth');

------------------------------

//例子二:
function sortItem(items:Array<number>,order='desc'){
	console.log(void 0); //undefine
}

//报错:类型“true”的参数不能赋给类型“string | undefined”的参数。
sortItem([1,2],true)

  • 剩余参数:剩余参数是一个数组,所以标注的时候一定要注意
//给一个接口中添加任意属性
//使用接口描述对象格式
interface IObj {
    [key: string]: any;
}

let b:IObj={
    x:200
}


function merge(target: IObj, ...others:Array<IObj>) {
    
    //{ x: 1 } 'target'
    console.log(target,'target');

    //数组中的每一位元素是对象格式
    //[ { y: 2 }, { z: 3, k: 4  } ] 'others'
    console.log(others,'others');


    return Object.assign(target, ...others);
}


let newObj = merge({x: 1}, {y: 2}, {z: 3,k:4});
//{ x: 1, y: 2, z: 3,k: 4  } 'newObj'
console.log(newObj,'newObj');


----------------------------------------
//使用接口描述函数
interface IFunc {
    //使用接口描述函数关注的是参数和返回值
    (x: number, y: number): number
}
let fn1IN: IFunc = function(a: number, b: number): number {
    return a + b;
}

  • 函数中的 this,无论是 JavaScript 还是 TypeScript ,函数中的 this 都是我们需要关心的,那函数中 this 的类型该如何进行标注呢?
  • 我们需要针对普通函数和箭头函数做不同方式的标注
  • 普通函数:对于普通函数而言, this 是会随着调用环境的变化而变化的,所以默认情况下,普通函数中的 this被标注为 any ,但我们可以在函数的第一个参数位(它不占据实际参数位置)上显式的标注 this 的类型
  • 例子一:
interface T {
    a: number;
    b?:number;
    fn: (x: number) => void
}


let obj1: T = {
    a: 1,
    fn( x: number) {
        //默认this被标注为any
        //any上有任意的属性和方法
        //放弃了类型检测和IDE 的智能提示
        this.fn;

        //{ a: 1, fn: [Function: fn] }
        console.log(this,'this');

        //function(x){...}
        console.log('this.fn',this.fn);
    }
}

obj1.fn(1)
  • 例子二:
let obj3: T = {
    a: 1,
    //们可以在函数的第一个参数位
    //(它不占据实际参数位置)
    //上显式的标注 this 的类型
    
    //即x还是fn的第一个参数
    fn(this: T, x: number) {
        //this指向T
        this
        console.log(this,'this');
        console.log(this===obj3);
    }
}
obj3.fn(1)

  • 例子三:我们还可以使用类型断言来改变this的类型,比如(<HTMLImageElement>img).src;
let obj2: T = {
    a: 1,
    fn( x: number) {
        //默认this被标注为any
        //使用类型断言将this的类型预判是T
        //缺点是每一次使用this都需要类型断言
        (<T>this).fn
    }
}

  • 箭头函数:箭头函数的 this 不能像普通函数那样进行标注,它的 this 标注类型取决于它所在的作用域 this的标注类型
  • 例子一:错误的写法
//箭头函数中的 this 是相对固定
//this 标注类型取决于它所在的作用域 this的标注类型
interface T {
    a: number;
    fn: (x: number) => void;
}


//不能去动态的改变箭头函数中this的指向类型
// 错误的写法:(this:T) => {}

let obj5:T = {
    a: 1,
    fn( x: number) {
        //下一行报错
        //箭头函数不能包含 "this" 参数。
        return (this:T) => {
        	//this:any
            this
            console.log(this,x);
        }
    }
}
  • 例子二:正确的做法:
//箭头函数中的 this 是相对固定
//this 标注类型取决于它所在的作用域 this的标注类型
interface T {
    a: number;
    fn: (x: number) => void;
}


let obj7:T = {
    a: 1,
    fn(this:T,x: number) {
        return () => {
            //this取决于所在的作用域
            //this:T
            this
        }
    }
}

----------------------------------


let obj8:T = {
    a: 1,
    fn(this:window,x: number) {
        return () => {
            //this取决于所在的作用域
            //this:window
            this
        }
    }
}

  • 函数重载:有的时候,同一个函数会接收不同类型的参数返回不同类型的返回值,我们可以使用函数重载来实现,通过下面的例子来体会一下函数重载的必要性
function showOrHide(el: HTMLElement, attr: 'display'|'opacity', value: 'block'|'none'|number) {
    // 
}


let div = document.querySelector('div'); 

if (div) {
    showOrHide( div, 'display', 'none' ); 
    showOrHide( div, 'opacity', 1 ); 


    //如果我们传递给display的是1呢?
    //如果我们传递给opacity的是block呢?
    //下面这一行其实在执行过程中参数是有问题的
    //但是ts编译确是可以通过的
    showOrHide( div, 'display', 1 ); 
}
  • 我们来通过函数重载改造上面的例子:

//函数重载
function showOrHide1(ele: HTMLElement, attr: 'display', value: 'block'|'none'){}; 

function showOrHide1(ele: HTMLElement, attr: 'opacity', value: number){}; 

function showOrHide1(ele: HTMLElement, attr: any, value: any) { 
    //...
    ele.style[attr] = value;
}


let div = document.querySelector('div'); 

if (div) {
    showOrHide1( div, 'display', 'none' ); 
    showOrHide1( div, 'opacity', 1 ); 

	//通过函数重载可以设置不同的参数对应关系
    //会提示报错
    //没有与此调用匹配的重载
    showOrHide1( div, 'display', 5); 
}

  • 重载函数类型只需要定义结构,不需要实体,类似接口
//使用接口定义一个对象格式
interface PlainObject { 
    [key: string]: string|number; 
}

function css(ele: HTMLElement, attr: PlainObject)
function css(ele: HTMLElement, attr: string, value: string|number);
function css(ele: HTMLElement, attr: any, value?: any) {
    if (typeof attr === 'string' && value) {
        ele.style[attr] = value;
    }
    if (typeof attr === 'object') {
        for (let key in attr) {
            ele.style[attr] = attr[key];
        }
    }
}


let div = document.querySelector('div');
if (div) { 
    css(div, 'width', '100px');
    css(div, {
        width: '100px'
    })


    //使用重载后ts会提示报错
    // css(div, 'width');
}

面向对象编程

  • 面向对象编程中一个重要的核心就是: ,当我们使用面向对象的方式进行编程的时候,通常会首先去分析具体要实现的功能,把特性相似的抽象成一个一个的类,然后通过这些类实例化出来的具体对象来完成具体业务需求。
  • 类的基础:TypeScript 与 EMCAScript2015+ 在类方面共有的一些特性有class 关键字、构造函数constructor成员属性定义成员方法this关键字
  • 我们来看class 关键字在ts中的运用例子一
class User{
    constructor(){
        //当类通过new实例化的时候
        //就会执行该构造函数
        console.log('这是构造函数');
    }
}
let user1=new User();
//这是构造函数 User {}
console.log(user1);


  • 我们来看class 关键字和构造函数在ts中的运用例子二:
  • 注意点:在ts中构造函数 constructor 不允许有 return(因为要返回实例对象)
  • 注意点:在ts中不能对构造函数返回值进行类型标注
  • 注意点:如果在实例化(new)一个类的时候无需传入参数,则可以省略 ()即大括号
  • 注意点:默认情况下,构造函数是一个空函数,我们定义的构造函数会覆盖默认构造函数
class User2{
    //类型批注不能出现在构造函数声明中
    constructor():Number{
        //创建类得函数,当类通过new实例化的时候,就会执行该函数
        console.log('这是构造函数2但是做法是错误的');
        // 在ts中构造函数 constructor 不允许有 return
        return 1;
    }
}
let user2=new User2();
console.log(user2);

  • 我们来看定义成员属性和定义成员方法在ts中的运用例子一:
  • 注意点:在类内部,我们可以通过 this 关键字来访问类的成员属性和方法
class User3{
    // 成员属性的定义与运用
    id:number;
    username:string;

    constructor(
        id:number,
        username:string
    ){  
        //this User3 {}
        console.log('this',this);
        this.id=id;
        this.username=username
    }


    //成员方法的定义与运用
    postArticle(title:string,content:string){
        console.log(`发表了一篇文章:${title}`);
        console.log(`通过this关键字调用类成员属性:${this.username}`);
    }
}


let user3=new User3(1,'username');
//User3 { id: 1, username: 'username' }
console.log(user3);
//发表了一篇文章:标题一
// 通过this关键字调用类成员属性:username
user3.postArticle('标题一','标题的内容')

--------------------------------------------------

//被ts编译后的js代码内容为:
class User3 {
    constructor(id, username) {
        //this User3 {}
        console.log('this', this);
        this.id = id;
        this.username = username;
    }
    //成员方法的定义与运用
    postArticle(title, content) {
        console.log(`发表了一篇文章:${title}`);
        console.log(`通过this关键字调用类成员属性:${this.username}`);
    }
}
let user3 = new User3(1, 'username');
//User3 { id: 1, username: 'username' }
console.log(user3);
//发表了一篇文章:标题一
// 通过this关键字调用类成员属性:username
user3.postArticle('标题一', '标题的内容');
  • 我们来看定义成员属性和定义成员方法在ts中的运用例子二:
  • 注意点:public 就是类的默认修饰符,表示该成员可以在任何地方进行读写操作。
  • 注意点:因为在构造函数中对类成员属性进行传参赋值初始化是一个比较常见的场景,所以 ts 提供了一个简化操作:给构造函数参数添加修饰符来直接生成成员属性
class User4{
    //省略了下面
    // id:number;
    // username:string;


    constructor(
        //给构造函数参数添加修饰符来直接生成成员属性
        //给当前类添加同名的成员属性
        //在类的实例化的时候会把传入的参数赋值给对应的成员属性
        public id:number,
        public username:string
    ){  
        //省略了下面
        // this.id=id;
        // this.username=username
    }

    postArticle(title: string, content: string) {
        console.log(`用户id为${this.id}用户名称为${this.username} 发表了一篇文章:${title}`)
}
let user9=new User4(1,'user9');
//用户id为1用户名称为user9 发表了一篇文章:article9
user9.postArticle('article9','content1')


---------------------------------------


//被ts编译后的js代码内容为:
class User4 {

    constructor(id, username) {
        this.id = id;
        this.username = username;

    }
    
    postArticle(title, content) {
        console.log(`用户id为${this.id}用户名称为${this.username} 发表了一篇文章:${title}`);
    }
}
let user9 = new User4(1, 'user9');
//用户id为1用户名称为user9 发表了一篇文章:article9
user9.postArticle('article9', 'content1');

  • 继承:在 ts 中,也是通过 extends 关键字来实现类的继承
//这里的User5是父类
class User5{
    constructor(
        public id:number,
        public username:string
    ){
        console.log(this.id,this.username);
    }

    //成员方法的定义与运用
    postArticle(title:string,content:string){
        console.log(`发表了一篇文章:${title}`);
        console.log(`通过this关键字调用类成员属性:${this.username}`);
    }
}

let user5=new User5(1,'用户5')
console.log(user5.postArticle('article5','content5'));


//这里的VIP是子类继承父类User5
class VIP extends User5{
    //派生类的构造函数必须包含 "super" 调用
    constructor(
        id:number,
        username:string,
        public score:number
    ){
        super(id,username)
        //在子类构造函数中只有在 super(参数) 之后才能访问 this
        // console.log(this.id);
        // console.log('子类构造函数');
    }

    postAttachment(file: string): void {
        console.log(`${this.username} 上传了一个附件: ${file}`)
    }
}

let vip1=new VIP(1,'vip用户',0)
// console.log(vip1);

vip1.postAttachment('file1')
//通过 super 访问父类的的同时,会自动绑定上下文对象为当前子类 this
vip1.postArticle('5','文章5')
  • 注意点:在子类中,我们可以通过 super 来引用父类,如果子类没有重写构造函数,则会在默认的 constructor 中调用 super()
  • 注意点:如果子类有自己的构造函数,则需要在子类构造函数中显示的调用父类构造函数 : super(参数) ,否则会报错
  • 注意点:在子类构造函数中只有在 super(参数) 之后才能访问 this
  • 注意点:在子类中,可以通过 super 来访问父类的成员属性和方法
  • 注意点:通过 super 访问父类的的同时,会自动绑定上下文对象为当前子类 this

  • 注意点:默认情况下,子类成员方法集成自父类,但是子类也可以对它们进行重写和重载
  • 我们先来看重写的例子:
class VIP extends User5{
    //派生类的构造函数必须包含 "super" 调用
    constructor(
        id:number,
        username:string,
        public score:number
    ){
        super(id,username)
        //在子类构造函数中只有在 super(参数) 之后才能访问 this
        // console.log(this.id);
        // console.log('子类构造函数');
        

    }

    postAttachment(file: string): void {
        console.log(`${this.username} 上传了一个附件: ${file}`)
    }


    // 重写  postArticle
    //重新实现了父类的方法:参数的个数和参数的类型和父类一致
    postArticle(title:string,content:string){
        this.score++;
        console.log(`${this.username}发表了一篇文章:${title},分数为${this.score}`);
    }



}
  • 我们再来看重载的例子:
class VIP extends User5{
    //派生类的构造函数必须包含 "super" 调用
    constructor(
        id:number,
        username:string,
        public score:number
    ){
        super(id,username)
        //在子类构造函数中只有在 super(参数) 之后才能访问 this
        // console.log(this.id);
        // console.log('子类构造函数');
        

    }

    postAttachment(file: string): void {
        console.log(`${this.username} 上传了一个附件: ${file}`)
    }


    //重载 参数个数和参数类型不同
    postArticle(title: string, content: string): void;
    postArticle(title: string, content: string, file: string): void;
    postArticle(title: string, content: string, file?: string) {
        //直接去调用了父类的postArticle的方法 假如逻辑一样的情况下
        //在子类中,可以通过 super 来访问父类的成员属性和方法
        super.postArticle(title, content);
        if (file) {
            //多了一个行为
            this.postAttachment(file);
        }
    }

}

  • 修饰符:我们已经接触过了public(公有,默认)类修饰符,其实类修饰符还有其他三种,分别是protected:受保护private:私有readonly只读
  • 总结:添加protected(受保护)后它的访问级别为:自身和子类,不能外部修改;
  • 总结:添加private(私有)后它的访问级别为:自身,外部包括子类不能访问,也不能修改
  • 总结:添加readonly(只读)后可以访问但是一旦确定(赋值)后就不能修改
  • 总结:添加public(公有)后(默认就是public)它的访问级别为:自身、子类、类外
class User6{
    constructor(
        //可以访问但是一旦确定不能修改
        readonly id:number,


        //可以访问但是不能外部修改
        //它的访问级别为:自身和子类
        protected username:string,


        // 外部包括子类不能访问,也不能修改
        //它的访问级别为:自身
        private password:string
    ){

    }


    //方法也可以加上修饰符
    //它的访问级别为:自身和子类
    //不能外部修改
    protected method1(){
        console.log(this.username);
    }

    //默认public
    //它的访问级别为:自身、子类、类外
    setPassword(password: string) {
        if (password.length >= 6) {
            this.password = password;
        }
    }

    //寄存器  对比defineProperty
    set a(password: string){
        if (password.length >= 6) {
            this.password = password;
        }
    }

    //得到的会是被处理后的
    get a():string{
        return '***'
    }
}

class ViP2 extends User6{
    method2(){
        console.log(this.username);
        //属性“password”为私有属性,只能在类“User6”中访问
        // console.log(this.password);
    }
}

let user6=new User6(1,'mt','123');
//无法分配到 "id" ,因为它是只读属性。
user6.id=1


// 只能在类的内部自身以及子类当中访问
// 在类的外部是不能够访问的
//属性“username”受保护,只能在类“User6”及其子类中访问。
user6.username

// 属性“password”为私有属性,只能在类“User6”中访问。ts(2341)
user6.password

//*** user6.a被寄存器拦截了
console.log(user6.a,'user6.a被寄存器拦截了');

  • 寄存器:有的时候,我们需要对类成员 属性 进行更加细腻的控制,就可以使用 寄存器 来完成这个需求,通过寄存器 ,我们可以对类成员属性的访问进行拦截并加以控制,更好的控制成员属性的设置和访问边界,寄存器分为两种:getter和setter
  • getter访问控制器,当访问指定成员属性时调用
  • setter设置控制器,当设置指定成员属性时调用

  • 静态成员:前面我们说到的是成员属性和方法都是实例对象的,但是有的时候,我们需要给类本身添加成员,区分某成员是静态还是实例的:
  • 判断依据一:该成员属性或方法是类型的特征还是实例化对象的特征
  • 判断依据二:如果一个成员方法中没有使用或依赖 this ,那么该方法就是静态的
  • 注意点:类的静态成员是属于类的,所以不能通过实例对象(包括 this)来进行访问,而是直接通过类名访问(不管是类内还是类外)
  • 注意点:静态成员也可以通过访问修饰符进行修饰,静态成员属性一般约定(非规定)全大写
type IAllowFileTypeList='png'|'gif'|'jpg'|'jpeg'|'webp';


//我们如果需要给User7添加一个用户能够上传的所有类型 
//使用静态成员只加在类的身上而不是加在类的实例上
class User7{
    //static 必须在 readonly 之前 
    //静态成员属性一般约定(非规定)全大写
    static readonly ALLOW_FILE_TYPE_LIST:Array<IAllowFileTypeList> =['png','gif','jpg','jpeg','webp'];
    
    constructor(
        public id:number,
        public username:string,
        private _allowFileTypes: Array<IAllowFileTypeList>
    ){

    }   

    info():void{
        // VIP 这种类型的用户允许上传的所有类型有哪一些
        // 类上的属性
        console.log(User7.ALLOW_FILE_TYPE_LIST,'类上的属性');

        // 当前这个 vip 用户允许上传类型有哪一些
        // 实例对象上的属性
        console.log(this._allowFileTypes,'实例对象上的属性');
    }
}

//类上的属性
// User7.ALLOW_FILE_TYPE_LIST

//实例对象上的属性
let user7=new User7(1,'user7',['png'])

// [ 'png', 'gif', 'jpg', 'jpeg', 'webp' ] '类上的属性'
// [ 'png' ] '实例对象上的属性'
console.log(user7.info());


--------------------------------------

//被ts编译后的js代码为:


class User7 {
    constructor(id, username, _allowFileTypes) {
        this.id = id;
        this.username = username;
        this._allowFileTypes = _allowFileTypes;
    }
    info() {

        console.log(User7.ALLOW_FILE_TYPE_LIST, '类上的属性');
        console.log(this._allowFileTypes, '实例对象上的属性');
    }
}



User7.ALLOW_FILE_TYPE_LIST = ['png', 'gif', 'jpg', 'jpeg', 'webp'];


//类上的属性
// User7.ALLOW_FILE_TYPE_LIST


//实例对象上的属性
let user7 = new User7(1, 'user7', ['png']);
// [ 'png', 'gif', 'jpg', 'jpeg', 'webp' ] '类上的属性'
// [ 'png' ] '实例对象上的属性'
console.log(user7.info());

  • 抽象类:有的时候,一个基类(父类)的一些方法无法确定具体的行为,而是由继承的子类去实现,此时可以考虑使用抽象类
//定义父类并规定了和子类所共有的特征
class Component{
    props:any;
    state:any;

    constructor(props:any){
        this.props=props
    }


    render():string {
        console.log(this.props,'Components');
        return ''
    }
}


class MYComponent extends Component{
    constructor(props:any){
        super(props)

        this.state={
            x:1
        }
    }


    render(){
        
        console.log(this.props,'myComponents');
        return 'myComponent'
    }
}


//这里才可以new
let myComponents=new MYComponent({val:1});
//{ val: 1 } 'myComponents' myComponent
console.log(myComponents.render());


//如果子类的render方法未(忘记)定义则会触发父类上的render方法
//{ val: 1 } 'Components'
//可能会让我们误解这是子类的render方法成功执行了的结果
//我们现在的需求是让ts编译器帮我们检测
//如果在子类中我们少写了render方法便向我们发出警告提示
//即警告我们子类的render行为必须存在
  • 使用抽象类来帮助我们规范子类的设计:
//定义父类并规定了和子类所共有的特征
//如果一个类是抽象的,那么就不能使用 new 进行实例化
//即不能new Component
abstract class Component{
    props:any;
    state:any;

    constructor(props:any){
        this.props=props
    }

    //抽象方法只能出现在抽象类中
    //方法“render”不能具有实现,因为它标记为抽象。(不能有{}
    //缺少返回类型批注的“render”隐式具有“any”返回类型。(得有:返回类型
    abstract render():string
}


//非抽象类“MYComponent”不会实现继承自“Component”类的抽象成员“render”。
//如果一个子类继承了一个抽象类,那么该子类就必须实现抽象类中的所有抽象方法,否则该类还得声明为抽象的
class MYComponent extends Component{
    constructor(props:any){
        super(props)

        this.state={
            x:1
        }
    }


    //必须实现抽象类中的所有抽象方法
    render():string {
        return 'myComponent'
    }
}

//这里才可以new
let myComponents=new MYComponent({val:1});
//myComponent
console.log(myComponents.render());
  • 注意点:abstract 修饰的方法不能有方法体(不能有{})
  • 注意点:如果一个类有抽象方法,那么该类也必须为抽象的
  • 注意点:如果一个类是抽象的,那么就不能使用 new 进行实例化(因为抽象类表名该类有未实现的方法,所以不允许实例化)
  • 注意点:如果一个子类继承了一个抽象类,那么该子类就必须实现抽象类中的所有抽象方法,否则该类还得声明为抽象的
  • 初识泛型<>:有时候需要一种方法使返回值的类型与传入参数的类型是相同的,可以使用类型变量,只用于表示类型而不是值
//T帮助我们捕获用户传入的类型
//之后我们就可以使用这个类型。
//我们再次使用了 T 当做返回值类型。
//保证参数类型与返回值类型是相同的
//记住这些类型变量(这里是T)代表的是任意类型
function identity<T>(arg: T): T {
    return arg;
}

let output = identity<string>("myString");
  • 下面是泛型类的运用:
class Gener<T> {
    Value: T;
    add: (x: T, y: T) => T;
}

let myGener = new Gener<number>();
myGener.Value = 0;
myGener.add = function(x, y) { return x + y; };
  • 下面是泛型类的运用二:
abstract class Component<T1, T2> {

    props: T1;
    state: T2;

    constructor(props: T1) {
        this.props = props;
    }
    abstract render(): string;

}

interface IMyComponentProps {
    val: number;
}
interface IMyComponentState {
    x: number;
}

//泛型类型“Component<T1, T2>”需要 2 个类型参数。
class MYComponent extends Component<IMyComponentProps, IMyComponentState>{
    constructor(props:IMyComponentProps){
        super(props)

        this.state={
            x:1
        }
    }
    
    //必须实现抽象类中的所有抽象方法
    render(){
        console.log(this.props.val,'有提示了props下有val');
        console.log(this.state.x,'有提示了state下有x');
        return 'myComponent'
    }
}

//对象文字可以只指定已知属性,并且“x”不在类型
let myComponents=new MYComponent({val:2});

  • 类与接口:通过接口,我们可以为对象定义一种结构和契约。我们还可以把接口与类进行结合,通过接口,让类去强制符合某种契约,从某个方面来说,当一个抽象类中只有抽象的时候,它就与接口没有太大区别了,这个时候,我们更推荐通过接口的方式来定义契约
  • 抽象类编译后还是会产生实体代码,而接口不会
  • TypeScript 只支持单继承,即一个子类只能有一个父类,但是一个类可以实现过个接口
  • 接口不能有实现,抽象类可以
  • 在一个类中使用接口并不是使用 extends 关键字,而是 implements
  • 多个接口使用 , 分隔
  • implements 与 extends 可同时存在
  • 我们先来看下面这个例子:
//这个例子使用了抽象类
//因为这里不仅需要实现render方法
//还需要初始化实现props和state

abstract class Component<T1, T2> {

    props: T1;
    state: T2;

    constructor(props: T1) {
        this.props = props;
    }

    // render(): string {
    //     return '';
    // }

    abstract render(): string;

}
  • 需求:我们想要在类中实现在接口中定义的方法,而且是必须得给我实现在这个接口中定义的方法,如果你在这个类中不实现我在这个接口中定义的方法就给我报错
  • 在一个类中使用接口可以是通过 implements+接口名来定义契约以保证这个类必须实现这个接口中的定义规则。
abstract class Component<T1, T2> {
    props: T1;
    state: T2;
    
    constructor(props: T1) {
        this.props = props;
    }
    
    abstract render(): string;
}


//需求:getInfo是只存在于子组件中的特有方法
//需求:子组件中必须实现这个接口定义的特有方法
//通过implements ILog来给MyComponent这个类定下契约
interface ILog {
    getInfo(): string;
}


interface IMyComponentProps {
    val: number;
}
interface IMyComponentState {
    x: number;
}

//契约规定了你必须得给我实施了
//在该子组件中一定要有返回字符串的getInfo()方法
//如果不实现报错:类型 "MyComponent" 中缺少属性 "getInfo",但类型 "ILog" 中需要该属性。

class MyComponent extends Component<IMyComponentProps, IMyComponentState> implements ILog {

    constructor(props: IMyComponentProps) {
        super(props);

        this.state = {
            x: 1
        }
    }

    render() {
        this.props.val;
        this.state.x;
        return '<myComponent />';
    }

    
    getInfo() {
        return `我用implements ILog定下的必须实现这个接口的契约`;
    }

}

let myComponent = new MyComponent({val: 1});

function log( target:any ) {
    return target.getInfo();
}

//我用implements ILog定下的必须实现这个接口的契约
console.log(log( myComponent ));
  • 接口也可以继承:
interface ILog { getInfo(): string; }

interface IStorage extends ILog { 
	save(data: string): void; 
}

  • 类与对象类型:当我们在 TypeScript 定义一个类的时候,其实同时定义了两个不同的类型,类类型(构造函数类型)和对象类型
  • 对象类型好理解,就是我们的 new 出来的实例类型
  • 类类型其实本质上还是一个函数,当然我们也称为构造函数,这个类或者构造函数本身也是有类型的,那么这个类型就是类的类型
class Person {
    // 属于类的
    static type = '人';

    // 属于实例(对象)的
    // 将来通过let person=new Person('lth',20,'男')
    // person.name person.age person.gender 来调用
    name: string;
    age: number;
    gender: string;

    // 类的构造函数也是属于类的  类类型  构造函数类型
    constructor( name: string, age: number, gender: '男'|'女' = '男' ) {
        this.name = name;
        this.age = age;
        this.gender = gender;
    }


    public eat(): void {
        // ...
    }

}

// let p1 = new Person('lth', 20, '男');
// p1.eat();  //实例对象有eat方法 name、age、gender属性
// Person.type;  //类(构造函数)的属性