够用的TypeScript(1)|能用 Javascript 写的东西,终将会用 TypeScript 书写~

1,543 阅读12分钟

前言

「本文已参与低调务实优秀中国好青年前端社群的写作活动」。

能用 Javascript 写的东西,终将会用 TypeScript 书写

本文带你学习/复习一下 ts 的基础知识~

本文思维导图

TypeScript的意义

  • 避免一些类型或者一些不是我们预期希望的代码结果错误

JavaScript中 很多错误是运行时才会抛出来的,而 TypeScript可以做到出现错误时在编辑器里就能获知,提高了开发效率

以下JavaScriptTypescript 都由 js ts 代替

TypeScript优缺点

优点

  • js 开发中,进行前后端联调时,需要去查看接口文档上的字段类型,而 ts 会自动识别(类型推断),节省时间
  • 编辑器中提示错误,避免代码在运行时隐式转换踩坑

缺点

  • 学习成本
    • 类型概念、interface接口、class类、enum枚举、generic泛型
  • 与一些其他的库的融洽性

js & ts 对比

运行流程

js 运行流程

  1. 将 js 代码转为 js-AST
  2. 将 AST 转换为字节码
  3. 运行时计算字节码

ts 运行流程

  1. 将 ts 编译为 ts-AST
  2. 进行 AST 代码上类型检查
  3. 检查完后转换为 js

剩下三步就是 js 的运行流程

  1. 将 js 代码转为 js-AST
  2. 将 AST 转换为字节码
  3. 运行时计算字节码

其他区别

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

类型绑定

  • js 是动态绑定语言,只有运行程序时才能知道类型,在程序运行之前 js 完全不知道
  • ts 在编译时就已经知道什么类型,如果没有定义类型,也会自动推导类型

类型转换

  • js 会有隐式转换
  • ts 不会
    • :隐式转换——比如各种奇葩的 1+ "1"之类的

何时检查类型

  • js 运行时
  • ts 编译时

何时报告错误

  • js 执行后
  • ts 写代码时

主要使用模式

显式注解类型

let name: string = "zzz";
let age: number = 18;
let hobby: string[] = ["code", "gogogo"]

也就是声明变量时带上类型注解

推导类型

let name = "zzz";//string
let age= 18;//number
let hobby= ["code", "gogogo"]//string数组

它会自动推导是什么类型,当你在 vscode 中将鼠标放上去就能看见

开玩

安装

npm i -g typescript全局安装

执行

tsc index.ts
执行之后,就会编译生成一个index.js,也就是上文提到的ts转换为js的那一步。

ts-node

每次都自己手动编译的话——麻烦,这里介绍一个插件ts-node,直接运行.ts 文件,并且不会编译出来一个新的 js 文件使得目录看起来非常冗余

实际上,用像 cra 等脚手架的话,这些完全不需要自己操作,具体使用只需要知道 ts 的语法就行了

安装
npm i ts-node
执行
ts-node index.ts

基础知识

基础静态类型

前面几个比较简单就只举一个例子

  • boolean
const status: boolean  = false; // 显示注解一个boolean g类型
const status1 = true; // 不显示注解,ts会自动推导出来类型
  • number
  • string
  • null
  • undefined
  • void (表示无效的意思

一般只用在函数上,表示该函数没有返回值

function fn(): void {} // 正确
function testFn(): void {
    return 1; // 报错,不接受返回值存在
}
function fn1(): void { return undefined} // 显示返回undefined类型,也是可以的
function fn2(): void { return null} // 显示返回null类型也可以,因为 null == undefined
  • never (表示永远不会有值或永远执行不完的类型

甚至 null、undefined也不能返回

const test: never = null; // 错误
const test1: never = undefined // 错误
function a(): never { // 正确,因为死循环了,一直执行不完
    while(true) {}
}
function b(): never { // 正确,因为递归,永远没有出口
    b()
}
function c(): never { // 正确 代码报错了,执行不下去
    throw new Error()
}
  • any (表示任意

全都用这个的话——就是和 js 定义类型一样。

  • unknown (表示未知,和any很像

区别在于:

  • 未知,代表你可以容纳任何类型——因为你也不知道你能装什么,索性都装了。但是,别人就不一样了,别人知道自己要装什么,你是未知类型,不是我要装的—— 别人赋值给你,行!你赋值给别人,不行!
  • 而any,就是橡皮泥,啥形状都行。

对象静态类型

  • object 也就是 {}
const list: object = {} // 空对象
const list1: object = null; // null对象
const list: object = [] // 数组对象
const list: {} = {}
list.name = 1 // 报错 
list.toString()

对象类型常与后面说到的接口一起使用

  • 数组 && [ ]
const list: [] = []; // 定义一个数组类型
const list1: number[] = [1,2] // 定义一个数组,里面值必须是number
const list2: object[] = [null, {}, []] // 定义一个数组里面必须是对象类型的
const list3: Array<number> = [1,2,3] // 泛型定义数组必须是number类型,泛型后面说
class ClassPerson = {}
const person: ClassPerson = new Person();
person.xxx = 123; // 这行代码报错,因为当前类中不存在该xxx属性
  • 函数
const fn: () => string = () => "zzz" // 定义一个变量必须是函数类型的,返回值必须是string类型

函数类型注解

函数是不会自动类型推导的。——但你可以显式地定义注解类型。

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

参数类型设为number,返回类型为string
ps. 表示无返回值的 void 已经在上面说了。
参数是对象的情况:

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

元组Tuple

表示一个已知数组的数量和类型的数组,定义数组中每一个值的类型(不常用

const arr: [string, number] = ["zzz", 1]

枚举Enum

使用场景

控制有些取值是在一些特定的范围的常量,比如

  • 一周七天
  • 方向东南西北或上下左右。

可以设置默认值,不设置则为索引。

简单使用

enum e {
    a,
    b = "B",
    c = "C",
  	d
}

// e["a"] 0
// e["b"] B
// e["d"] 报错

乍一看,有点像json。但是他对于默认值的设置是没有那么灵活的——如上,如果c设置了默认值,d就不知道"C"递增后是啥了,但如果c=10,那么d就会输出11。
还可反查:

enum e {
    a,
    b = 9,
    c = 10,
  	d
}

// e[0] a
// e[9] b
// e[10] c
// e[11] d

深入了解

众所周知,原本的js中不像python是没有这枚举类型的,然后我就有些好奇,它编译后的样子。
ts

enum Direction {
  Up
  Down,
  Left,
  Right,
}

js

var Direction;
(function(Direction){
  Direction[Direction["Up"]= 0] = "Up";
  Direction[Direction["Down"]= 1] ="Down;
 	Direction[Direction["Left"]= 2] ="Left";
  Direction[Direction["Right"] =3] = "Right";
})(Direction ||(Direction = {}))

有点妙哈...通过这样达到双向赋值的效果:
Direction["Up"]= 0,然后赋值运算符返回的是被赋予的值,也就是说Direction[Direction["Up"]= 0] = "Up";也就等于Direction[0] = "Up";

接口Interface

关于接口,我觉得了解鸭子类型也许对你有所帮助。

六个字概述接口作用就是:方便复用代码。
没用接口前:

const testObj: { name: string, age: number } = { name: "okk", age: 18 }
const testObj1: { name: string, age: number } = { name: "joo", age: 18 }

可以发现很多重复的代码——用上接口即可改善:

interface Types {
    name: string, 
    age: number
}
const testObj:Types = { name: "okk", age: 18 }
const testObj1: Types= { name: "joo", age: 18 }

修饰符readonly

只读,不可更改。

? 可选修饰符

interface Types {
    name: string, 
    age: number,
    height?:number
}

接口定义、使用后,name、age属性是必填的,而height则不是。

继承extends

接口也是可以继承的——和 Class 一样

interface ParentType {
    name: string, 
    age: number,
    height?:number
}
interface Child extends ParentType {
    weight:number
}

扩展propName

interface ParentType {
    name: string, 
    age: number,
    height?:number,
    [propName:string]:any //propName字段必须是 string类型 or number类型。 值是any类型
}

如果接口中没有第四个属性,那么定义对象时,是不能私自增加不存在的属性的。

声明类型别名 Type

别名类型只能定义是:基础静态类型、对象静态类型、元组、联合类型。不能定义接口。

type Type1 = string;
type Type12 = string | number

const name: type1 = "zzz"
const age: type12 = 18
const c: type12 = "zzz"

关于 Type 和 interface 的区别

名字

首先从这两个的中文名称入手:

  • interface(接口) 是 TS 设计出来用于定义对象类型的,可以对对象的形状进行描述。
  • type (类型别名),顾名思义,类型别名只是给类型起一个新名字。它并不是一个类型,只是一个别名而已
使用与属性

其次是他们的使用与属性:

  • 定义出来的Type 类型不允许重名
  • 接口是可以重名的,效果就是合并——当然后续接口定义里面的同名属性的类型是要与之前保持一致的,不然编译器会报错
  • type支持表达式;interface不支持
const count: number = 123
type testType = typeof count

const count: number = 123

interface testType {
    [name: typeof count]: any // 报错
}
  • type 支持类型映射,interface不支持
type keys = "name" | "age"  
type KeysObj = {
    [propName in keys]: string
}

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

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

还有显而易见的一点:type定义时使用 = ,interface没有😏

其实我总感觉大部分时候使用都是看心情😛

联合类型 |

就是或——满足其中一个即可。
函数参数尽量不直接使用联合类型(至少要进行类型保护),函数参数类型本身就不能自动推导,如有调用访问参数类型上的方法,系统编译时并不知道有没有该方法,或者该方法长啥样。

类型保护

typeof

判断一下,参数是不是这类型,是就往下走。

function fun(params: string | number) {
    if (typeof params == "string") {
        console.log(params.split)
    }
    if (typeof params == "number") {
        console.log(params.toFixed)
    }
}

in

看看参数里面有没有想操作的东西,有就操作。

interface a {
    name: string
}

interface b {
    age: string
}
function fun(params: a | b) {
    if ("name" in params) {
        console.log(params.name)
    }

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

as断言

可以用来手动指定一个值的类型,后接一个接口
当 ts 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型中共有的属性或方法

interface Cat {
    name: string;
    run(): void;
}
interface Fish {
    name: string;
    swim(): void;
}

function isFish(animal: Cat | Fish) {
    if (typeof (animal as Fish).swim === 'function') {
        return true;
    }
    return false;
}

主要是用于“欺骗”编辑器——"这个animal类型就是Fish啦,往下走就好了"。但是实际运行时会不会出错呢...就看你自己写的对不对了,骗进去了发现真的没有方法就出问题了。

感叹号+点 !.断言

!. 的意思是断言,告诉 ts 该对象里一定有某个值

const inputRef = useRef<HTMLEInputlement>(null);
// 定义了输入框,初始化是 null,但是你在调用他的时候相取输入框的 value,这时候 dom 实例一定是有值的,所以用断言
const value: string = inputRef.current!.value;
// 这样就不会报错了

交叉类型 &

就是与——类型必须存在。每个接口中的所有属性都要传入,如果有同名属性,类型却不一样,类型则都会变成never

⭐泛型

有时想要函数传入的所有参数类型相同:可以都是number,也可以都是string——只要都是就行。如果一个个写,非常麻烦。就可以用上泛型来解决这问题。

function fun<T>(a: T, b: T) {
    console.log(a, b)
}
fun<number>(1, "zzz") // 这时报错,因为在调用的使用类型是number,只能都传入number类型

fun<boolean>(true, false) 

fun<string>("ooo", "kkk")

fun<any>("jjj",111) //如果你又想让他们两不一样,用 any 就好了

有时我们想让他们不一样,但是又想约束一下范围。——使用 extends

function f<T1 extends number | string, T2 extends number | string>(a: T1, b: T2) {
    console.log(a, b)
}
f<number, string>(18, "zzz")
f<string, number>("zzz", 18)

还有需要确保参数有某个属性的情况,就可以配合 接口 使用

interface IWithLength {
  lenght:number,
}

function echoLength<T extends IWithLength>(arg:T):T{
  console.log(arg.length)
  return arg
}

接口、类都是可以配合泛型使用的~

模块

import a,{b} from './c'
export default xx
export const xxx

就是 导入和导出

类 Class

学了C++的应该对下面前三个属性很熟悉😜

public

默认的就是public,哪里都能访问

private

只能在当前类里面访问——且只有类里面的方法才能访问以及对其进行操作。

protected

只能在类和它的子类中访问

implements

就是 继承——可以继承父类_(也可以用extends)_,也可以继承接口

interface Radio{
  switchradio():void
}
//继承Radio接口之后,也就意味着告诉Car要实现这个switchRadio功能
class Car implements Radio{
  switchRadio(){}
}

如果继承多个就用逗号隔开。

abstract

定义抽象类——不能实例化

类中泛型

类也是可以使用泛型的

class Queue<T> {
  private data = []
  push(item:T){
    return this.data.push(item)
  }
  pop():T{
    return this.data.shift()
  }
} 
const queue  = new Queue<number>()

命名空间

项目总有可能会有重名的变量——我们可以使用 namespace 定义一个命名空间来装各种变量,该暴露的就暴露,没暴露的——也就是内部变量,也就没有重名的风险。

namespace SomeNameSpaceName { 
    const x = {}//未暴露的

    export interface xx {//暴露接口
        name: string
    }
}

引入

两种方法:

/// <reference path="./namespace0.ts" />

注意是三条斜杠只能放在最顶端

import { xx } from "./xxx.ts"

声明文件

简单使用:
jQery.d.ts

declare var jQuery:(selector: string) => any

一般声明为.d.ts后缀的文件,所有文件都可以访问到,如果不行则需要手动去配置一下tsconfig.json"include":["**/*"]
另外一般第三方库都会有写好的声明文件以供开发者使用,而不是像我上面那样操作。一般命名类似于@types/jquery,使用npm安装即可。

总结

看到这里了,话说我想问一下,现在还有地方是用 js 而不用 ts 的吗?
好了,阅读本文应该能让用过 TS 的稍微的复习一下简单的使用和基础语法,希望没用过的同学也能就此入门~ 下一篇我将带来 ts 中实用类型的相关知识~ 已更新~ 够用的 TypeScript(2) | 这八种实用类型的实现你知道吗?

🌊如果有所帮助,欢迎点赞关注,一起进步⛵