快速转 TypeScript 指南

From:https://segmentfault.com/a/1190000040582994

官网:https://www.typescriptlang.org/

Github:https://github.com/microsoft/TypeScript

中文文档 比 英文官网文档旧, 

TypeScript 教程:https://www.runoob.com/typescript/ts-tutorial.html
TypeScript 入门教程:http://ts.xcatliu.com/
TypeScript 超详细入门教程(上):https://blog.csdn.net/Aria_Miazzy/article/details/105641241
TypeScript 超详细教程:https://www.jianshu.com/p/c0ca03cffa62
5分钟上手TypeScript:https://www.tslang.cn/docs/handbook/typescript-in-5-minutes.html

深入理解 TypeScript:https://jkchao.github.io/typescript-book-chinese/

TypeScript is JavaScript with syntax for types. TypeScript is a strongly typed programming language that builds on JavaScript, giving you better tooling at any scale.

types 和 @types

types 和 @typeshttps://zhuanlan.zhihu.com/p/194196536

解决TypeScript报错TS2304: cannot find name ' require':npm install @types/node --save-dev

1、为什么要用 TypeScript

因为 旧 JS 是弱类型语言,一个变量先后可以保存不同类型的数据,这样极不可靠。而且旧 JS 是解释执行语言,一边解释一边执行,即 JavaScript 错误是在运行中才抛出的,导致一 些低级错误无法提前检查和预警。此外旧 JS 对对象要求不够严格,开发人员想怎么写就 怎么写,不便于大项目协作。

这个时候为了彻底解决以上问题,就诞生了 TypeScript。TypeScript 错误直接是在编辑器里告知我们的,这极大的提升了开发效率,也不用花大量的时间去写单测,同时也避免了大量的时间排查Bug。

什么是 TypeScript ?

TypeScript 是一种由微软开发的自由和开源的编程语言,它是 JavaScript 的一个超集,扩展了 JavaScript 的语法。

TypeScript 建立在 JavaScript 之上。首先,我们编写 TypeScript 代码。然后,我们使用 TypeScript 编译器将 TypeScript 代码编译为纯 JavaScript 代码。

拥有纯 JavaScript 代码后,我们可以将其部署到 JavaScript 运行的任何环境中。

TypeScript 文件的扩展是 .ts ,而不是 JavaScript 文件的 .js 扩展名。

TypeScript 使用 JavaScript 语法,并添加了额外的语法来支持类型。

如果我们的 JavaScript 程序没有任何语法错误,那么,它也是一个 TypeScript 程序。这意味着所有的 JavaScript 程序都是 TypeScript 程序。

语法特性

  • 类 Classes
  • 接口 Interfaces
  • 模块 Modules 
  • 类型注解 Type annotations
  • 编译时类型检查 Compile time type checking 
  • Arrow 函数 (类似 C# 的 Lambda 表达式)

JavaScript 与 TypeScript 的区别

TypeScript 是 JavaScript 的超集,扩展了 JavaScript 的语法,因此现有的 JavaScript 代码可与 TypeScript 一起工作无需任何修改,TypeScript 通过类型注解提供编译时的静态类型检查。

TypeScript 可处理已有的 JavaScript 代码,并只对其中的 TypeScript 代码进行编译。

2、TypeScript 优缺点

优点

  • 一般我们在前后端联调时,都要去看接口文档上的字段类型,而TypeScript会自动帮我们识别当前的类型。节省了我们去看文档或者network时间。这叫做类型推导(待会我们会讲到)
  • 友好地在编辑器里提示错误,避免代码在运行时类型隐式转换踩坑。

缺点

  • 有一定的学习成本,TypeScript中有几种类型概念,interface接口class类enum枚举generics泛型等这些需要我们花时间学习。
  • 可能和一些插件库结合的不是很完美

3、TypeScript 和 JavaScript 的运行流程

JavaScript 运行流程

依赖 NodeJs 环境和浏览器环境

  • JavaScript代码转换为JavaScript-AST
  • AST代码转换为字节码
  • 运算时计算字节码

TypeScript 运行流程

以下操作均为 TSC 操作。前三步执行完后,后面的继续同上操作。

  • TypeScript代码编译为 TypeScript-AST
  • 检查AST代码上类型检查
  • 类型检查后,编译为JavaScript代码
  • JavaScript代码转换为JavaScript-AST
  • AST代码转换为字节码
  • 运算时计算字节码

4、TypeScript 和 JavaScript 区别

只有搞懂了二者的区别,我们才可以更好的理解TypeScript

类型系统特性JavaScriptTypeScript
类型是如何绑定?动态静态
是否存在类型隐式转换?
何时检查类型?运行时编译时
何时报告错误运行时编译时

类型绑定

  • JavaScript:JavaScript动态绑定类型,只有运行程序才能知道类型,在程序运行之前JavaScript对类型一无所知
  • TypeScript:TypeScript是在程序运行前(也就是编译时)就会知道当前是什么类型。当然如果该变量没有定义类型,那么TypeScript会自动类型推导出来。

类型转换

  • JavaScript:比如在JavaScript1 + true这样一个代码片段,JavaScript存在隐式转换,这时true会变成number类型number(true)和1相加。
  • TypeScript:TypeScript中,1+true这样的代码会在TypeScript中报错,提示number类型不能和boolean类型进行运算。

何时检查类型

  • JavaScript:JavaScript中只有在程序运行时才能检查类型。类型也会存在隐式转换,很坑。
  • TypeScript:TypeScript中,在编译时就会检查类型,如果和预期的类型不符合直接会在编辑器里报错、爆红

何时报告错误

  • JavaScript:JavaScript只有在程序执行时才能抛出异常,JavaScript存在隐式转换,等我们程序执行时才能真正的知道代码类型是否是预期的类型,代码是不是有效。
  • TypeScript:TypeScript中,当你在编辑器写代码时,如有错误则会直接抛出异常,极大得提高了效率,也是方便。

5、TypeScript 的两种模式

显式注解类型 (类型批注)

let name: string = "前端娱乐圈";
let age: number = 38;
let hobby: string[] = ["write code", "玩游戏"]

显式注解类型就是,声明变量时定义上类型(官方话语就是声明时带上注解),让我们一看就明白,哦~,这个name是一个string类型。

TypeScript 通过类型批注提供静态类型以在编译时启动类型检查。这是可选的,而且可以被忽略而使用 JavaScript 常规的动态类型。

function Add(left: number, right: number): number {
    return left + right;
}

对于基本类型的批注是 number, bool和string。而弱或动态类型的结构则是any类型。

类型批注可以被导出到一个单独的声明文件以让使用类型的已被编译为JavaScript的TypeScript脚本的类型信息可用。批注可以为一个现有的JavaScript库声明,就像已经为Node.js和jQuery所做的那样。

当类型没有给出时,TypeScript编译器利用类型推断以推断类型。如果由于缺乏声明,没有类型可以被推断出,那么它就会默认为是动态的any类型。

示例:在 type.ts 文件中创建一个简单的 area() 函数:

function area(shape: string, width: number, height: number) {
    var area = width * height;
    return "I'm a " + shape + " with an area of " + area + " cm squared.";

document.body.innerHTML = area("rectangle", 30, 15);

创建一个 index.html 文件:

<!DOCTYPE html> 
<html> 
<head> 
<meta charset="utf-8"> 
<title>Learning TypeScript</title>
</head> 
<body> 
    <script src="hello.js"></script>
</body> 
</html>

修改 index.html 的 js 文件为 type.js 然后编译 TypeScript 文件: tsc type.ts

浏览器刷新 index.html 文件,输出结果如下:

推导类型

let name = "前端娱乐圈"; // 是一个string类型
let age = 38;           // 是一个number类型
let hobby = ["write code", "玩游戏"] // 是一个string数组类型

推导类型就是去掉显示注解,系统自动会识别当前值是一个什么类型的。

6、安装 TypeScript && 运行

安装 typescript

我们可以通过以下两种方式来安装 TypeScript:

通过 npm 按安装的步骤:

1、安装 npm ( 安装完 nodejs 后,npm 可以直接使用 )

$ curl http://npmjs.org/install.sh | sh
$  npm --version

2、安装 TypeScript npm 包:

$ npm install -g typescript

安装完成后我们就可以使用 TypeScript 编译器,名称叫 tsc,可将编译结果生成 js 文件。

要编译 TypeScript 文件,可使用如下命令:

tsc filename.ts

一旦编译成功,就会在相同目录下生成一个同名 js 文件,你也可以通过命令参数来修改默认的输出名称。默认情况下编译器以 ECMAScript 3(ES3)为目标但 ES5 也是受支持的一个选项。TypeScript 增加了对为即将到来的 ECMAScript 6 标准所建议的特性的支持。

我们知道了运行 tsc 命令就可以编译生成一个文件,有的小伙伴觉得这样太麻烦了,每次运行只是编译出来一个文件还不是运行,还得用node index.js才可以运行。不急我们接着往下看

ts-node 插件

我们来看一下这个插件 ts-node,这个插件可以直接运行.ts文件,并且也不会编译出来.js文件。

npm i ts-node  // 运行 ts-node index.ts

通过 MSI 文件安装时的界面:

TypeScript 的 Hello World

创建 hello.ts 文件, *.ts 是 TypeScript 文件的后缀,向 hello.ts 文件添加如下代码:

alert('hello world in TypeScript!');

接下来,我们打开命令行,使用 tsc 命令编译 hello.ts 文件:

$ tsc hello.ts

在相同目录下就会生成一个 hello.js 文件,然后打开 index.html 输出结果如下:

7、TypeScript 基础知识

箭头函数表达式( lambda表达式 )

lambda 表达式 ()=>{something} 或 ()=>something 相当于 js 中的函数,它的好处是可以自动将函数中的 this 附加到上下文中。示例:

var shape = {
    name: "rectangle",
    popup: function() {
 
        console.log('This inside popup(): ' + this.name);
 
        setTimeout(function() {
            console.log('This inside setTimeout(): ' + this.name);
            console.log("I'm a " + this.name + "!");
        }, 3000);
 
    }
}; 
shape.popup();

实例中的 this.name 是一个空值:

接下来我们使用 TypeScript 的箭头函数。把 function() 替换为 () =>:

var shape = {
    name: "rectangle",
    popup: function() {
 
        console.log('This inside popup(): ' + this.name);
 
        setTimeout( () => {
            console.log('This inside setTimeout(): ' + this.name);
            console.log("I'm a " + this.name + "!");
        }, 3000);
 
    }
}; 
shape.popup();

输出结果如下:

在以上实例编译后端 js 文件中,我们可以看到一行 var _this = this;_this 在 setTimeout() 的回调函数引用了 name 属性。

7.1 基础静态类型

在 TypeScript 中基础类型跟我们 JavScript 中基础类型是一样的。只是有各别是 Ts 里面新出的。

1. number

const count: number = 18;  // 显示注解一个 number 类型
const count1 = 18;  // 不显示注解,ts 会自动推导出来类型

2. string

const str: string = "前端娱乐圈"; // 显示注解一个string类型
const str1 = "蛙人"; // 不显示注解,ts会自动推导出来类型

3. boolean

const status: string = false; // 显示注解一个string类型
const status1 = true; // 不显示注解,ts会自动推导出来类型

4. null

const value: null = null;

// 这一点null类型可以赋值undefined跟在 js中是一样的,null == undefined
const value: null = undefined; 

5. undefined

const value: undefined = undefined;

// 这一点null类型可以赋值undefined跟在 js中是一样的,null == undefined
const value: undefined = null; 

6. void

字面意思是 "无效" ,早些项目里面会有 <a href="javascript: void(0)"> 这是控制 a 标签的跳转默认行为。你不管怎么执行 void 方法它都是返回 undefined。在 TypeScript 中 void 类型是什么呢。它也是代表无效的,一般只用在函数上,告诉别人这个函数没有返回值。

function fn(): void {} // 正确

function testFn(): void {
    return 1; // 报错,不接受返回值存在
}

function fn1(): void { return undefined} // 显示返回undefined类型,也是可以的

function fn2(): void { return null} // 显示返回null类型也可以,因为 null == undefined

7. never

never "一个永远不会有值的类型" 或者 也可以说 "一个永远也执行不完的类型",代表用于不会有值,undefined、null也算做是值。一般这个类型就不会用到,也不用。大家知道这个类型就行。

const test: never = null; // 错误
const test1: never = undefined // 错误

function Person(): never { // 正确,因为死循环了,一直执行不完
    while(true) {}
}

function Person(): never { // 正确,因为递归,永远没有出口
    Person()
}

function Person(): never { // 正确 代码报错了,执行不下去
    throw new Error()
}

8. any

any 这个类型代表 任何的任意的。希望大家在项目中,不要大片定义any类型。虽然它真的好使,那这样我们写TypeScript就没有任何意义了。

let value: any = ""; // 正确
value = null // 正确
value = {} // 正确
value = undefined // 正确

9. unknown

unknown 类型是我们 TypeScript 中第二个any类型,也是接受任意的类型的值。它的英文翻译过来就是未知的,我们来看一下栗子

let value: unknown = ""     
value = 1;
value = "fdsfs"
value = null
value = {}

unknown 和 any 区别

那现在肯定有小伙伴疑惑,诶,那它 unknown 相当于是any类型,那二者的区别是什么。我们来看一下

let valueAny: any = "";
let valueUnknown: unknown = "";

valueAny = "蛙人";
valueUnknown = "前端娱乐圈"

let status: null = false;
status = valueAny; // 正确
status = valueUnknown // 报错,不能将unknown类型分配给null类型

我们来看一下上面的,为什么any类型就能被赋值成功,而unknown类型不行呢,从它俩的意义来上看,还是有点区别的,any任何的,任意的、unknown未知的。所以你给unknown类型赋值任何类型都没关系,因为它本来就是未知类型嘛。但是你如果把它的unknown类型去被赋值一个null类型,这时人家null这边不干了,我不接受unknown类型。

说白了一句话:

  • 别人不接受 unknown 类型
  • 而 unknown 类型接受别人

7.2 对象静态类型

说起对象类型,我们肯定都能想到对象包含{}数组函数

1. object && {}

其实这俩意思一样,{}object表示非原始类型,也就是除numberstringbooleansymbolnullundefined之外的类型。

const list: object = {}     // 空对象
const list1: object = null; // null对象
const list: object = []     // 数组对象
const list: {} = {}
list.name = 1 // 报错 不可更改里面的字段,但是可以读取
list.toString()

2. 数组

const list: [] = []; // 定义一个数组类型
const list1: number[] = [1,2] // 定义一个数组,里面值必须是number
const list2: object[] = [null, {}, []] // 定义一个数组里面必须是对象类型的
const list3: Array<number> = [1,2,3] // 泛型定义数组必须是number类型,泛型我们待会讲到

3. 类

// 类
class ClassPerson = {
    name: "前端娱乐圈"
}

const person: ClassPerson = new Person();
person.xxx = 123; // 这行代码报错,因为当前类中不存在该xxx属性

4. 函数

// 函数。定义一个变量必须是函数类型的,返回值必须是string类型
const fn: () => string = () => "前端娱乐圈" 

7.3 函数类型注解

这里说一下函数显示注解和函数参数不会类型推导问题。

1. 函数返回类型为 number

function fn(a, b): number {
    return a + b;
}
fn(1, 2)

2. 函数 void

显示注解为void类型,函数没有返回值。

function fn(): void {
    console.log(1)
}

3. 函数不会自动类型推导

可以看到下面的函数类型,不会自动类型推导,我们实参虽然传入的1和2,但是形参方面是可以接受任意类型值的,所以系统也识别不出来你传递的什么,所以这里得需要我们显示定义注解类型。

function testFnQ(a, b) {
    return a + b
}
testFnQ(1,2)

我们来改造一下。 

function testFnQ(a:number, b:number) {
    return a + b
}
testFnQ(1,2)

我们再来看一下参数对象显示注解类型,也是在:号后面赋值每个字段类型即可。 

function testFnQ(obj : {num: number}) {
    return obj.num
}
testFnQ({num: 18})

7.4 元组 Tuple

元组用于表示一个已知数组的数量和类型的数组,定义数组中每一个值的类型,一般不经常使用。

const arr: [string, number] = ["前端娱乐圈", 1]
const arr: [string, string] = ["前端娱乐圈", 1] // 报错

7.5 枚举 Enum

Enum 枚举类型,可以设置默认值,如果不设置则为索引。

enum color {
    RED,
    BLUE = "blue",
    GREEN = "green"
}

// color["RED"] 0
// color["BLUE"] blue

像上面的 color 中 RED 没有设置值,那么它的值则为 0,如果 BLUE 也不设置的话那么它的值则是1,它们这里是递增。如果设置值则是返回设置的值

注意这里还有一个问题,直接来上代码

通过上面学习我们知道了enum可以递增值,也可以设置默认值。但是有一点得注意一下,enum没有json对象那样灵活,enum不能在任意字段上设置默认值。

比如下面栗子,RED没有设置值,然后BLUE设置了默认值,但是GREEN又没有设置,这时这个GREEN会报错。因为你第二个BLUE设置完默认值,第三又不设置,这时代码都不知道该咋递增了,所以报错。还有一种方案就是你给BLUE可以设置一个数字值,这时第三个GREEN不设置也会跟着递增,因为都是number类型。

// 报错
enum color {
    RED,
    BLUE = "blue",
    GREEN
}

// good
enum color {
    RED,       // 0
    BLUE = 4,  // 4
    GREEN      // 5
}

比如enum枚举类型还可以反差,通过valuekey值。像我们json对象就是不支持这种写法的。

enum color {
    RED,       // 0
    BLUE = 4,  // 4
    GREEN      // 5
}

console.log(color[4]) // BLUE
console.log(color[0]) // RED

7.6 接口 Interface

接口interface是什么,接口interface就是方便我们定义一处代码,多处复用。接口里面也存在一些修饰符。下面我们来认识一下它们吧。

1. 接口怎么复用

比如在讲到这之前,我们不知道接口这东西,可能需要给对象定义一个类型的话,你可能会这样做。

const testObj: { name: string, age: number } = { name: "前端娱乐圈", age: 18 }
const testObj1: { name: string, age: number } = { name: "蛙人", age: 18 }

我们用接口来改造一下。

interface Types {
    name: string, 
    age: number
}

const testObj: Types = { name: "前端娱乐圈", age: 18 }
const testObj1: Types = { name: "蛙人", age: 18 }

可以看到使用interface关键字定义一个接口,然后赋值给这两个变量,实现复用。

2. readonly 修饰符

readonly类型,只可读状态,不可更改。

interface Types {
    readonly name: string, 
    readonly age: number
}

const testObj: Types = { name: "前端娱乐圈", age: 18 }
const testObj1: Types = { name: "蛙人", age: 18 }
testObj.name = "张三" // 无法更改name属性,因为它是只读属性
testObj1.name = "李四" // 无法更改name属性,因为它是只读属性

3. ?可选修饰符

可选修饰符以?定义,为什么需要可选修饰符呢,因为如果我们不写可选修饰符,那interface里面的属性都是必填的。

interface Types {
    readonly name: string, 
    readonly age: number,
    sex?: string
}

const testObj: Types = { name: "前端娱乐圈", age: 18}

4. extends继承

我们的interface也是可以继承的,跟ES6Class类一样,使用extends关键字。

interface Types {
    readonly name: string, 
    readonly age: number,
    sex?: string
}

interface ChildrenType extends Types { // 这ChildrenType接口就已经继承了父级Types接口
    hobby: []
}
    
const testObj: ChildrenType = { name: "前端娱乐圈", age: 18, hobby: ["code", "羽毛球"] }

5. propName扩展

interface里面这个功能就很强大,它可以写入不在interface里面的属性。

interface Types {
    readonly name: string, 
    readonly age: number,
    sex?: string,
}

const testObj: Types = { name: "前端娱乐圈", age: 19, hobby: [] } 

上面这个testObj这行代码会爆红,因为hobby属性不存在interface接口中,那么我们不存在的接口中的,还不让人家写了?。这时候可以使用自定义就是上面的propName

interface Types {
    readonly name: string, 
    readonly age: number,
    sex?: string,
    [propName: string]: any // propName字段必须是 string类型 or number类型。 值是any类型,也就是任意的
}

const testObj: Types = { name: "前端娱乐圈", age: 19, hobby: [] } 

在运行上面代码,就可以看到不爆红了~

示例:创建一个 interface.ts 文件,修改 index.html 的 js 文件为 interface.js

interface.js 文件代码如下:

interface Shape {
    name: string;
    width: number;
    height: number;
    color?: string;
}
 
function area(shape : Shape) {
    var area = shape.width * shape.height;
    return "I'm " + shape.name + " with area " + area + " cm squared";
}
 
console.log( area( {name: "rectangle", width: 30, height: 15} ) );
console.log( area( {name: "square", width: 30, height: 30, color: "blue"} ) );

接口可以作为一个类型批注。编译以上代码 tsc interface.ts 不会出现错误,但是如果你在以上代码后面添加缺失 name 参数的语句则在编译时会报错:

console.log( area( {width: 30, height: 15} ) );

重新编译,错误信息如下:

$ tsc hello.ts 
hello.ts(15,20): error TS2345: Argument of type '{ width: number; height: number; }' is not assignable to parameter of type 'Shape'.
  Property 'name' is missing in type '{ width: number; height: number; }'.

浏览器访问,输出结果如下:

7.7 Type

我们再来看一下Type,这个是声明类型别名使的,别名类型只能定义是:基础静态类型对象静态类型元组联合类型

注意:type 别名不可以定义 interface
type Types = string;

type TypeUnite = string | number

const name: typeUnite = "前端娱乐圈"
const age: typeUnite = 18

那么 type 类型别名和 interface 接口有什么区别呢

1. type不支持 interface 声明

type Types = number
type Types = string // 报错, 类型别名type不允许出现重复名字

interface Types1 {
    name: string
}

interface Types1 {
    age: number
}

// interface接口可以出现重复类型名称,如果重复出现则是,
// 合并起来也就是变成 { name:string, age: number }

第一个Types类型别名type不允许出现重复名字,interface接口可以出现重复类型名称,如果重复出现则是,合并起来也就是变 { name:string, age: number }

再来看一下interface另一种情况

interface Types1 {
    name: string
}

interface Types1 {
    name: number
}

可以看到上面两个同名称的interface接口,里面的属性也是同名称,但是类型不一样。这第二个的Types1就会爆红,提示:后续声明的接口,必须跟前面声明的同名属性类型必须保持一致,把后续声明的name它类型换成string即可。

2. type支持表达式 interface不支持

const count: number = 123
type testType = typeof count

const count: number = 123

interface testType {
    [name: typeof count]: any // 报错
}

可以看到上面type支持表达式,而interface不支持

3. type 支持类型映射,interface不支持

type keys = "name" | "age"  
type KeysObj = {
    [propName in keys]: string
}

const PersonObj: KeysObj = { // 正常运行
    name: "蛙人",
    age: "18"
} 

interface testType {
    [propName in keys]: string // 报错
}

7.8 联合类型

联合类型|表示,说白了就是满足其中的一个类型就可以。

const statusTest: string | number = "前端娱乐圈"

const flag: boolean | number = true

再来看一下栗子。我们用函数参数使用联合类型看看会发生什么

function testStatusFn(params: number | string) {
    console.log(params.toFixed()) // 报错
}

testStatusFn(1)

上面我们说过了,函数参数类型不能类型自动推导,更何况现在用上联合类型,系统更懵逼了,不能识别当前实参的类型。所以访问当前类型上的方法报错。

接下来带大家看一些 类型保护,听着挺高级,其实这些大家都见过。

1. typeof

function testStatusFn(params: number | string) {
    console.log(params.toFixed()) // 报错
}
testStatusFn(1)

改造后

// 正常
function testStatusFn(params: string | number) {
    if (typeof params == "string") {
        console.log(params.split)
    }

    if (typeof params == "number") {
        console.log(params.toFixed)
    }
}

testStatusFn(1)

2. in

// 报错
interface frontEnd {
    name: string
}

interface backEnd {
    age: string
}

function testStatusFn(params: frontEnd | backEnd) {
    console.log(params.name)
}

testStatusFn({name: "蛙人"})

改造后

// 正常
function testStatusFn(params: frontEnd | backEnd) {
    if ("name" in params) {
        console.log(params.name)
    }

    if ("age" in params) {
        console.log(params.age)
    }
}

testStatusFn({name: "蛙人"})

3. as 断言

// 报错
interface frontEnd {
    name: string
}

interface backEnd {
    age: string
}

function testStatusFn(params: frontEnd | backEnd) {
    console.log(params.name)
}

testStatusFn({name: "蛙人"})

改造后

// 正常
function testStatusFn(params: frontEnd | backEnd) {
    if ("name" in params) {
        const res = (params as frontEnd).name
        console.log(res)
    }
    
    
    if ("age" in params) {
        const res = (params as backEnd).age
        console.log(res)
    }
}

testStatusFn({age: 118})

7.9 交叉类型

交叉类型就是跟联合类型相反,它用&表示,交叉类型就是两个类型必须存在。这里还用上面的联合类型的栗子来看下。

interface frontEnd {
    name: string
}

interface backEnd {
    age: number
}

function testStatusFn(params: frontEnd & backEnd) {}

testStatusFn({age: 118, name: "前端娱乐圈"})

这里我们可以看到实参必须传入两个接口(interface)全部的属性值才可以。联合类型是传入其中类型就可以。

注意:我们的接口interface出现同名属性

interface frontEnd {
    name: string
}

interface backEnd {
    name: number
}

function testStatusFn(params: frontEnd & backEnd) {
    console.log(params)
}

testStatusFn({name: "前端"})

上面我们两个接口类型中都出现了同名属性,但是类型不一样,这时类型就会变为never

<img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/51e337707b6641e590bbb0139fabacdb~tplv-k3u1fbpfcp-watermark.image" width="90%">

7.10 泛型

泛型是TypeScript中最难理解的了,这里我尽量用通俗易懂的方式讲明白。

function test(a: string | number, b: string | number) {
    console.log(a, b)
}
test(1, "前端娱乐圈")

比如上面栗子,函数参数注解类型定义stringnumber,调用函数实参传入也没什么问题,但是有个需求,就是实参我们必须传入同样的类型(传入两个number类型)。虽然上面这种联合类型也可以实现,但是如果我们要在加一个boolean类型,那么联合类型还得在追加一个boolean,那这样代码太冗余了。

这时就需要用到泛型了,泛型是专门针对不确定的类型使用,并且灵活。泛型的使用大部分都是使用<T>,当然也可以随便使用,如:<Test><Custom>都可以。

function test<T>(a: T, b: T) {
    console.log(a, b)
}

// 调用后面跟着尖括号这就是泛型的类型,这时报错,
// 因为在调用的使用类型是number,所以只能传入相同类型的
test<number>(1, "前端娱乐圈") 

test<boolean>(true, false) 

test<string>("前端娱乐圈", "蛙人")

上面这使用泛型就解决了我们刚才说的传入同一个类型参数问题,但是泛型也可以使用不同的参数,可以把调用类型定义为<any>

function test<T>(a: T, b: T) {
    console.log(a, b)
}

test<any>(1, "前端娱乐圈")

但是上面这种又有一种问题,它可以传入对象,但是如果我们只希望传入number类型和string类型。那么我们泛型也给我们提供了约束类型。泛型使用extends进行了类型约束,只能选择stringnumber类型。

function test<T extends number | string, Y extends number | string>(a: T, b: Y) {
    console.log(a, b)
}

test<number, string>(18, "前端娱乐圈")
test<string, number>("前端娱乐圈", 18)

这时,传入泛型时使用逗号分隔,来定义每一个类型希望是什么。记住,只有我们不确定的类型,可以使用泛型。

7.11 模块

TypeScript也支持importexport这里大多数小伙伴都知道,这里都不多讲啦。

// 导入
import xxx, { xxx } from "./xxx"

// 导出
export default {}
export const name = "前端娱乐圈"

如有不明白的小伙伴,可以看我以前文章 聊聊什么是CommonJs和Es Module及它们的区别: https://juejin.cn/post/6938581764432461854

7.12 Class类

以下这三个修饰符是在 TypeScript类中才能使用,在 JavaScript类中是不支持的。

1. public

public的公共属性,就是不管在的内部还是外部,都可以访问该属性方法。默认定义的属性方法都是public

class Person {
    name = "前端娱乐圈";
    public age = 18;
}
const res = new Person();
console.log(res.name, res.age) // 前端娱乐圈 18

上面可以看到打印结果都能显示出来,name属性没有定义public公共属性,所以里面定义的属性方法默认都是public定义。

2. private

private的私有属性,只有在当前里面才能访问,当前就是{}里面区域内。在{}外面是不能访问private定义的属性方法

class Person {
    private name = "前端娱乐圈";
    private age = 18;
}
const res = new Person();
console.log(res.name, res.age) // 这俩行会爆红,当前属性为私有属性,只能在类内部访问

class Scholl extends Person {
    getData() {
        return this.username + "," + this.age
    }
}
const temp = new Scholl()

 // 爆红~,虽然继承了Person类,但是private定义是只能在当前类访问,子类也不能访问。
console.log(temp.getData())

3. protected

protected的保护属性,只有在当前类子类可以访问。也就是说用protected属性定义的子类也可以访问。

class Person {
    protected username = "前端娱乐圈";
    protected age = 18;
}
const res = new Person();
console.log(res.name, res.age) // 这俩行会爆红,当前属性为私有属性,只能在类内部访问

class Scholl extends Person {
    getData() {
        return this.username + "," + this.age
    }
}
const temp = new Scholl()
console.log(temp.getData()) // 前端娱乐圈,18。可以正常访问父类的属性

4. implements

implements关键字只能在class中使用,顾名思义,实现一个新的类,从父级或者从接口实现所有的属性和方法,如果在PersonAll类里面不写进去接口里面已有的属性和方法则会报错。

interface frontEnd {
    name: string,
    fn: () => void
}

class PersonAll implements frontEnd {
    name: "前端娱乐圈";
    
    fn() {
        
    }
}

5. 抽象类

抽象类使用abstract关键字定义。abstract抽象方法不能实例化,如果,抽象类里面方法是抽象的,那么本身的类也必须是抽象的,抽象方法不能写函数体。父类里面有抽象方法,那么子类也必须要重新该方法。

// 抽象类
abstract class Boss {
    name = "秦";
    call() {} // 抽象方法不能写函数体
}

class A extends Boss {
    call() {
        console.log(this.name);
        console.log("A")
    }
}

class B extends Boss {
    call() {
         console.log("B")
    }
}

new A().call()

该抽象类使用场景,比如A需求或者B需求正好需要一个公共属性,然后本身还有一些自己的逻辑,就可以使用抽象类,抽象类只能在TypeScript中使用。

示例:接下来我们创建一个类文件 class.ts,代码如下:

class Shape {
 
    area: number;
    color: string;
 
    constructor ( name: string, width: number, height: number ) {
        this.area = width * height;
        this.color = "pink";
    };
 
    shoutout() {
        return "I'm " + this.color + " " + this.name +  " with an area of " + this.area + " cm squared.";
    }
}
 
var square = new Shape("square", 30, 30);
 
console.log( square.shoutout() );
console.log( 'Area of Shape: ' + square.area );
console.log( 'Name of Shape: ' + square.name );
console.log( 'Color of Shape: ' + square.color );
console.log( 'Width of Shape: ' + square.width );
console.log( 'Height of Shape: ' + square.height );

以上 Shape 类中有两个属性 area 和 color,一个构造器 (constructor()), 一个方法是 shoutout() 。

构造器中参数(name, width 和 height) 的作用域是局部变量,所以编译以上文件,在浏览器输出错误结果如下所示:

class.ts(12,42): The property 'name' does not exist on value of type 'Shape'
class.ts(20,40): The property 'name' does not exist on value of type 'Shape'
class.ts(22,41): The property 'width' does not exist on value of type 'Shape'
class.ts(23,42): The property 'height' does not exist on value of type 'Shape'

接下来,我们添加 public 和 private 访问修饰符。Public 成员可以在任何地方访问, private 成员只允许在类中访问。然后修改以上代码,将 color 声明为 private,构造函数的参数 name 声明为 public:

...
private color: string;
...
constructor ( public name: string, width: number, height: number ) {
...

由于 color 成员变量设置了 private,所以会出现以下信息:class.ts(24,41): The property 'color' does not exist on value of type 'Shape'

继承:继承使用关键字 extends

接下来我们在 class.ts 文件末尾添加以下代码,如下所示:

class Shape3D extends Shape {
 
    volume: number;
 
    constructor ( public name: string, width: number, height: number, length: number ) {
        super( name, width, height );
        this.volume = length * this.area;
    };
 
    shoutout() {
        return "I'm " + this.name +  " with a volume of " + this.volume + " cm cube.";
    }
 
    superShout() {
        return super.shoutout();
    }
}
 
var cube = new Shape3D("cube", 30, 30, 30);
console.log( cube.shoutout() );
console.log( cube.superShout() );

派生类 Shape3D 说明:

  • Shape3D 继承了 Shape 类, 也继承了 Shape 类的 color 属性。
  • 构造函数中,super 方法调用了基类 Shape 的构造函数 Shape,传递了参数 name, width, 和 height 值。 继承允许我们复用 Shape 类的代码,所以我们可以通过继承 area 属性来计算 this.volume。
  • Shape3D 的 shoutout() 方法重写基类的实现。superShout() 方法通过使用 super 关键字直接返回了基类的 shoutout() 方法。
  • 其他的代码我们可以通过自己的需求来完成自己想要的功能。

7.13 命名空间 namespace

我们学到现在可以看到,不知道小伙伴们发现没有,项目中文件是不是不能有重复的变量(不管你是不是一样的文件还是其它文件),否则就直接爆红了。命名空间一个最明确的目的就是解决重名问题。

命名空间使用namespace关键字来定义,示例:index.ts

namespace SomeNameSpaceName { 
    const q = {}

    export interface obj {
        name: string
    }
}

上面这样,就定义好了一个命名空间,可以看到变量 q 没有写 export 关键字,这证明它是内部的变量,就算别的 .ts 文件引用它,它也不会暴露出去。而 interface 这个 obj 接口是可以被全局访问的。

我们在别的页面访问当前命名空间

1. reference引入

/// <reference path="./index.ts" />
namespace SomeNameSpaceName { 
    export class person implements obj {
        name: "前端娱乐圈"
    }
}

2. import

export interface valueData {
     name: string
}
import { valueData } from "./xxx.ts"

这时使用命名空间之后完全可以解决不同文件重名爆红问题。

7.14 tsConfig.json

这个tsconfig文件,是我们编译ts文件,如何将ts文件编译成我们的js文件。tsc -init这个命令会生成该文件出来哈。执行完该命令,我们可以看到根目录下会生成一个tsconfig.json文件,里面有一堆属性。

那么我们怎么将ts文件编译成js文件呢,直接执行tsc命令可以将根目录下所有的.ts文件全部编译成.js文件输出到项目下。

更多配置文档,请参考 https://www.tslang.cn/docs/handbook/compiler-options.html

{
    // include: ["*.ts"] // 执行目录下所有的ts文件转换成js文件
    // include: ["index.ts"] // 只将项目下index.ts文件转换为js文件
    // files: ["index.ts"] // 跟include一样,只执行当前数组值里面的文件,当前files必须写相对路径
    // exclude: ["index.ts"] // exclude就是除了index.ts不执行,其它都执行
    
    compilerOptions: {
        removeComments: true, // 去掉编译完js文件的注释
        outDir: "./build", // 最终输出的js文件目录
        rootDir: "./src", // ts入口文件查找
    }
}

8、实用类型

最后来说一下实用类型,TypeScript 标准库自带了一些实用类型。这些实用类都是方便接口Interface使用。这里只列举几个常用的,更多实用类型 https://www.typescriptlang.org/docs/handbook/utility-types.html

1. Exclude

从一个类型中排除另一个类型,只能是联合类型,从TypesTest类型中排除UtilityLast类型。

适用于:并集类型

interface UtilityFirst {
    name: string
}

interface UtilityLast {
    age: number
}

type TypesTest = UtilityFirst | UtilityLast;

const ObjJson: Exclude<TypesTest, UtilityLast> = {
    name: "前端娱乐圈"
}

2. Extract

Extract正好跟上面那个相反,这是选择某一个可赋值的联合类型,从TypesTest类型中只选择UtilityLast类型。

适用于:并集类型

interface UtilityFirst {
    name: string
}

interface UtilityLast {
    age: number
}

type TypesTest = UtilityFirst | UtilityLast;

const ObjJson: Extract<TypesTest, UtilityLast> = {
    age: 1
}

3. Readonly

把数组或对象的所有属性值转换为只读的。这里只演示一下对象栗子,数组同样的写法。

适用于:对象、数组

interface UtilityFirst {
    name: string
}

const ObjJson: Readonly<UtilityFirst> = {
    name: "前端娱乐圈"
}
ObjJson.name = "蛙人" // 报错 只读状态

4. Partial

把对象的所有属性设置为可选的。我们知道interface只要不设置?修饰符,那么对象都是必选的。这个实用类可以将属性全部转换为可选的。

适用于:对象

interface UtilityFirst {
    name: string
}

const ObjJson: Partial<UtilityFirst> = {
    
}

5. Pick

Pick选择对象类型中的部分key值,提取出来。第一个参数目标值,第二个参数联合key

适用于:对象

interface UtilityFirst {
    name: string,
    age: number,
    hobby: []
}

const ObjJson: Pick<UtilityFirst, "name" | "age"> = {
    name: "前端娱乐圈",
    age: 18
}

6. Omit

Omit选择对象类型中的部分key值,过滤掉。第一个参数目标值,第二个参数联合key

适用于:对象

interface UtilityFirst {
    name: string,
    age: number,
    hobby: string[]
}

const ObjJson: Omit<UtilityFirst, "name" | "age"> = {
    hobby: ["code", "羽毛球"]
}

7. Required

Required把对象所有可选属性转换成必选属性。

适用于:对象

interface UtilityFirst {
    name?: string,
    age?: number,
    hobby?: string[]
}

const ObjJson: Required<UtilityFirst> = {
    name: "蛙人",
    age: 18,
    hobby: ["code"]
}

8. Record

创建一个对象结果集,第一个参数则是key值,第二个参数则是value值。规定我们只能创建这里面字段值。

适用于:对象

type IndexList = 0 | 1 | 2

const ObjJson: Record<IndexList, "前端娱乐圈"> = {
    0: "前端娱乐圈",
    1: "前端娱乐圈",
    2: "前端娱乐圈"
}

参考资料

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值