给团队做次TypeScript分享(一)—— 基本介绍、基础类型、函数类型、类型断言

750 阅读11分钟

typescript

TypeScript 是 JavaScript带类型的超集。

Typescript 要执行需要先转化成 JavaScript 代码,其实合法的 JavaScript 程序就是合法的 Typescript,如果把一段正常的 js 代码转化改文件名后缀为 ts,然后执行,尽管会提示类型错误,对应的 js 文件还是被创建了。 就算你的代码里有错误,你仍然可以使用TypeScript。但在这种情况下,TypeScript会警告你代码可能不会按预期执行。

js 的优点是动态类型,灵活,同时这也是 js 的缺点,在大型项目中,代码量大,团队人员多,就会出现某些奇怪难以发现的bug。

ts 带来了类型标注,定好了规范,让项目更“稳固”,有利于多人开发,团队协作,提高工作效率。

编译器在编译阶段就会将潜在风险提前暴漏,例如变量名的粗心写错,调用对象上不存在的函数或属性,函数参数传递错误等等。

前期准备

安装 TypeScript 包

npm install -g typescript

安装完成就可以使用 TypeScript 编译器(tsc),可将编译结果生成 js 文件

命令:tsc + 文件路径, 例如

tsc .\firstTs.ts

运行之后会在该文件的相同目录下生成一个编译后的 js 文件

image-20211227102213507.png

然后通过运行该 js 文件就可以得到对应的结果

安装 ts-node 包

npm install -g ts-node

如果不想像上面一样先转成 js 文件再运行可以使用 ts-node 包

例如运行

ts-node .\firstTs.ts

可以直接运行 firstTs.ts 文件

tsconfig.json

tsconfig.json文件中指定了用来编译这个项目的根文件和编译选项。

如果一个目录下存在一个tsconfig.json文件,那么它意味着这个目录是TypeScript项目的根目录。

通过命令

tsc --init

可以直接生成在当前目录下生成 tsconfig.json 文件

tsconfig.json 文件选项

主要有:

  • compilerOptions :编译器配置选项,编译器配置选项,配置选项特别多,具体可以参考官网的信息

    www.tslang.cn/docs/handbo…

  • files :指定一个包含相对或绝对文件路径的列表(只能是文件,不能是文件夹),指定需要编译的文件

  • include :指定一个文件glob匹配模式列表,指定需要编译的文件

  • exclude :指定一个文件glob匹配模式列表,指定不需要编译的文件

......

glob匹配模式,支持的glob通配符

  • * 匹配0或多个字符(不包括目录分隔符)
  • ? 匹配一个任意字符(不包括目录分隔符)
  • **/ 递归匹配任意子目录

当然,tsconfig.json 还有其他的一些选项(比如: compileOnSave、extends等等),这些选项使用的比较少

一般项目只需要使用 compilerOptions , include , exclude 即可(files 适合只有几个文件,即文件比较少的项目)

示例

一个简单的tsconfig.json例子

{
  "compilerOptions": {
    // 允许从没有设置默认导出的模块中默认导入。这并不影响代码的输出,仅为了类型检查。
    "allowSyntheticDefaultImports": true,
    // 指定生成哪个模块系统代码
    "module": "commonjs", // "ES6", "AMD" ....
    // 启用所有严格类型检查选项。
    // 启用 --strict相当于启用 --noImplicitAny, --noImplicitThis, --alwaysStrict,
    // --strictNullChecks和 --strictFunctionTypes和--strictPropertyInitialization。
    "strict": true,
    // 指定ECMAScript目标版本
    "target": "es2016",
    // 忽略所有的声明文件( *.d.ts)的类型检查。
    "skipLibCheck": true,
    // 编译过程中需要引入的库文件的列表。
    "lib": ["ESNext", "DOM"]
  },
  "include": [
    "tests/**/*.ts",
    "tests/**/*.tsx",
    "types/**/*.d.ts",
    "types/*.d.ts"
  ],
  "exclude": ["node_modules"]
}
  • allowSyntheticDefaultImports

    ts 代码可以生成CommonJs规范、AMD规范,而这二者的规范无法兼容,TypeScript提供了export =语法。以及对应的导入语法import module = require("module"),将二者给统一,以至于让ts支持以上规范。

    // a.ts
    const name = "杰克";
    export = name;
    
    // b.ts
    import name = require("./a");
    console.log(name); // 杰克
    

    在 allowSyntheticDefaultImports 为 false 时,在 b.ts 中使用 ES6 模块的写法无法默认导入

    import name from './a' // 报错
    

    在 allowSyntheticDefaultImports 为 true 时,可以在 b.ts 中使用 ES6 模块的写法默认导入

    // b.ts
    import name from "./a";
    console.log(name); // 杰克
    
  • "strict": true ,相当于

  • noImplicitAny : 在表达式和声明上有隐含的 any类型时报错。

  • noImplicitThis :当 this表达式的值为 any类型的时候,生成一个错误。

  • alwaysStrict : 以严格模式解析并为每个源文件生成 "use strict"语句。

  • strictNullChecks :在严格的 null检查模式下, nullundefined值不包含在任何类型里,只允许用它们自己和 any来赋值(有个例外, undefined可以赋值到 void)。

  • strictFunctionTypes : 禁用函数参数双向协变检查。

  • strictPropertyInitialization : 确保类的非undefined属性已经在构造函数里初始化。

  • lib : 编译过程中需要引入的库文件的列表

    • ESNext : ESNext 是始终表示 JavaScript 的下一个版本的名称。

    • dom:引入该包可以在 ts 中使用 dom 元素的类型,console对象等等等

      // node_modules/typescript/lib/lib.dom.d.ts 
      // 例如 dom 元素的类型声明
      interface HTMLElementTagNameMap {
          "a": HTMLAnchorElement;
          "abbr": HTMLElement;
          "address": HTMLElement;
          "area": HTMLAreaElement;
          "article": HTMLElement;
          "aside": HTMLElement;
          "audio": HTMLAudioElement;
          "b": HTMLElement;
          "base": HTMLBaseElement;
          "bdi": HTMLElement;
          "bdo": HTMLElement;
          "blockquote": HTMLQuoteElement;
          "body": HTMLBodyElement;
          "br": HTMLBRElement;
          "button": HTMLButtonElement;
          "canvas": HTMLCanvasElement;
          "caption": HTMLTableCaptionElement;
          "cite": HTMLElement;
          "code": HTMLElement;
          "col": HTMLTableColElement;
          "colgroup": HTMLTableColElement;
          "data": HTMLDataElement;
          "datalist": HTMLDataListElement;
          "dd": HTMLElement;
          "del": HTMLModElement;
          "details": HTMLDetailsElement;
          "dfn": HTMLElement;
          "dialog": HTMLDialogElement;
          "dir": HTMLDirectoryElement;
          "div": HTMLDivElement;
          "dl": HTMLDListElement;
          "dt": HTMLElement;
          "em": HTMLElement;
          "embed": HTMLEmbedElement;
          "fieldset": HTMLFieldSetElement;
          "figcaption": HTMLElement;
          "figure": HTMLElement;
          "font": HTMLFontElement;
          "footer": HTMLElement;
          "form": HTMLFormElement;
          "frame": HTMLFrameElement;
          "frameset": HTMLFrameSetElement;
          "h1": HTMLHeadingElement;
          "h2": HTMLHeadingElement;
          "h3": HTMLHeadingElement;
          "h4": HTMLHeadingElement;
          "h5": HTMLHeadingElement;
          "h6": HTMLHeadingElement;
          "head": HTMLHeadElement;
          "header": HTMLElement;
          "hgroup": HTMLElement;
          "hr": HTMLHRElement;
          "html": HTMLHtmlElement;
          "i": HTMLElement;
          "iframe": HTMLIFrameElement;
          "img": HTMLImageElement;
          "input": HTMLInputElement;
          "ins": HTMLModElement;
          "kbd": HTMLElement;
          "label": HTMLLabelElement;
          "legend": HTMLLegendElement;
          "li": HTMLLIElement;
          "link": HTMLLinkElement;
          "main": HTMLElement;
          "map": HTMLMapElement;
          "mark": HTMLElement;
          "marquee": HTMLMarqueeElement;
          "menu": HTMLMenuElement;
          "meta": HTMLMetaElement;
          "meter": HTMLMeterElement;
          "nav": HTMLElement;
          "noscript": HTMLElement;
          "object": HTMLObjectElement;
          "ol": HTMLOListElement;
          "optgroup": HTMLOptGroupElement;
          "option": HTMLOptionElement;
          "output": HTMLOutputElement;
          "p": HTMLParagraphElement;
          "param": HTMLParamElement;
          "picture": HTMLPictureElement;
          "pre": HTMLPreElement;
          "progress": HTMLProgressElement;
          "q": HTMLQuoteElement;
          "rp": HTMLElement;
          "rt": HTMLElement;
          "ruby": HTMLElement;
          "s": HTMLElement;
          "samp": HTMLElement;
          "script": HTMLScriptElement;
          "section": HTMLElement;
          "select": HTMLSelectElement;
          "slot": HTMLSlotElement;
          "small": HTMLElement;
          "source": HTMLSourceElement;
          "span": HTMLSpanElement;
          "strong": HTMLElement;
          "style": HTMLStyleElement;
          "sub": HTMLElement;
          "summary": HTMLElement;
          "sup": HTMLElement;
          "table": HTMLTableElement;
          "tbody": HTMLTableSectionElement;
          "td": HTMLTableCellElement;
          "template": HTMLTemplateElement;
          "textarea": HTMLTextAreaElement;
          "tfoot": HTMLTableSectionElement;
          "th": HTMLTableCellElement;
          "thead": HTMLTableSectionElement;
          "time": HTMLTimeElement;
          "title": HTMLTitleElement;
          "tr": HTMLTableRowElement;
          "track": HTMLTrackElement;
          "u": HTMLElement;
          "ul": HTMLUListElement;
          "var": HTMLElement;
          "video": HTMLVideoElement;
          "wbr": HTMLElement;
      }
      

类型

布尔值

简单的 true/false 值

let isDone: boolean = false;

数值

浮点数,支持十进制,十六进制,二进制,八进制

let amount: number = 17;
let hexAmount: number = 0x11;
let binaryAmount: number = 0b10001;
let octalAmount: number = 0o21;

字符串

let firstName: string = 'mike'
let company:string = `锄禾日当午 ${'汗滴禾下土'}`

字面量类型

字面量类型可以是字符串或者数字

type stringLiteral = 'right' | 'left'
let myString: stringLiteral = 'right' // ok
type numberLiteral = 0 | 1 | 2
let myNumber: numberLiteral = 1 // ok

数组

数组类型可以有两种写法

  • 类型加 '[]' 的方式

    let list: number[] = [1,2,3]
    let hybridList: (number|string)[] = [ 8, '科技', 'js']
    
  • 泛型写法

    let list: Array<number> = [1,2,3]
    let hybridList: Array<number|string> = [ 8, '科技', 'js']
    

元组 Tuple

表示一个已知数量及类型的数组

let list:[number, string] = [1, 'ts']
list[0] = 8 // ok
list[1] = '科技' // ok
list[0] = '税' // error
list[3] = 1 // error, 长度只有2

但是元组是可以越界的,例如利用 .push 等方法,但是类型必须是元组类型的联合类型,

list.push(1) // ok, 因为 1 属于类型 number | string
list.push(true) // error, 因为 true 是布尔值 不属于类型 number | string

any

any 类型本质上是类型系统的一个逃逸舱,代表任意类型,any可以直接绕过类型检查。

尽量不要使用any类型,不然用 ts 就失去了意义

unknown

typescript 3.0 引入的 unknown 类型,它是 any 类型对应的安全类型。

typescript 中所有类型都归于any,同时所有类型也可以归于unknow,any 和 unkonw 是 typescript 中的两种顶级类型

let anyVal:any 
anyVal = false // ok
anyVal = 1 // ok
anyVal = '1' // ok
anyVal = { num:1 } // ok
anyVal = null // ok

let unkonwVal:unknown 
unkonwVal = false // ok
unkonwVal = 1 // ok
unkonwVal = '1' // ok
unkonwVal = { num:1 } // ok
unkonwVal = null // ok

unknown 更安全是因为它不能像 any 一样暴力通过检查

let anyVal:any 
anyVal.name = 'test' // ok
anyVal.run() // ok
new anyVal() // ok

let unkonwVal:unknown
unkonwVal.name = 'test' // error
unkonwVal.run() // error
new unkonwVal() // error

unknown 类型只能被赋值给 any 类型和 unknown 类型本身。

let val: unknown = 1
let num: number = val // error 不能将类型“unknown”分配给类型“number”
let data: unknown = val // ok
let info: any = val // ok

如果不进行类型收窄,无法对unknown类型进行操作

let str:unknown = 'a'
console.log(str.length) // error
if(typeof str === 'string'){
  console.log(str.length) // ok
}
console.log((str as string).length) // ok

从上面可以看出,unknown 比 any 的安全性更高。

可以理解为 any 为任意可变类型,而 unknown 类型是固定的,只是还没确定下来。

开发中,应该优先使用 unknown,避免使用 any

void

void 表示没有任何类型,例如函数如果没有返回值时,我们可以定义其返回值类型是 void

function logSomething(): void{
  console.log('ok')
}

将变量声明为 void 类型只能赋值 undefined 或 null(在 ts 编译选项 strictNullChecks 默认为 false 时,下面会详细讲 strictNullChecks )

let value:void
value = undefined // ok
value = null // ok
value = '' // error

null、undefined

null 类型只能赋值为 null

undefined 类型只能赋值为 undefined

let u: undefined = undefined;
let n: null = null;

在 ts 的默认情况下,nullundefined是所有类型的子类型,所以其他类型的变量可以赋值为 null 和 undefined,例如

let num: number = null;

在 ts 的编译选项中有个 strictNullChecks 配置,默认为false

设置为 true 后表示:null和undefined值不能赋给非这两种类型的值,别的类型也不能赋给他们,除了any类型。还有个例外就是undefined可以赋值给void类型

也就是开启 strictNullChecks 为 true后上面代码会报错,但是有个例外 :

let value: void;
value = null; // error
value = undefined; // ok

建议使用 ts 时,将 strictNullChecks 设置为true,让检查更严格,可以避免很多问题。

never

永不存在的值的类型表示

比如总是会抛出异常或根本就不会有返回值的表达式返回值的类型

function neverReturn(): never {
  throw new Error("");
}

const infiniteLoop: () => never = () => {
  while (true) {}
};

没有类型是never的子类型或可以赋值给never类型(除了never本身之外)。 即使 any也不可以赋值给never

symbol

js es6 中新增了 Symbol 构造函数 用于创建唯一且不可变的 symbol

  let firstSymbol: symbol = Symbol('key1');
  let secondSymbol: symbol = Symbol('key2');
  console.log(firstSymbol === secondSymbol); // false

bigint

ES2020引入了 BigInt 内置对象, 它提供了一种方法来表示大于 253 - 1 的整数。这原本是 Javascript中可以用 Number表示的最大数字。BigInt 可以表示任意大的整数。

可以用在一个整数字面量后面加 n 的方式定义一个 BigInt

let bigNum: bigint = 9999999999999n;

object

表示非原始类型,即除开numberstringbooleansymbolbigint, nullundefined这七个类型之外的类型

let obj:object
obj = {} // ok
obj = {value:''} // ok
obj = '' // error
obj = true // error
obj = 123 // error
obj = undefined // error
obj = null //error
obj = Symbol() // error

object 和 Object

  • object 如上所示,表示非原始类型

  • Object 类型定义了类的原型对象上的属性

    // node_modules/typescript/lib/lib.es5.d.ts
    interface Object {
      constructor: Function;
      toString(): string;
      toLocaleString(): string;
      valueOf(): Object;
      hasOwnProperty(v: PropertyKey): boolean;
      isPrototypeOf(v: Object): boolean;
      propertyIsEnumerable(v: PropertyKey): boolean;
    }
    

函数

定义函数类型

在参数类型和返回类型中间使用( =>)符号

let myAdd: (x: number, y: number) => number = function (
  x: number,
  y: number
): number {
  return x + y;
};

其实不用赋值等号两边都定义类型这么复杂,可以只指定一边的类型,ts 会动推断类型

let myAdd = function (x: number, y: number): number {
  return x + y;
};

// or

let myAdd: (baseValue: number, increment: number) => number = function (x, y) {
  return x + y;
};

可选参数

默认情况下,ts 函数中定义的每个参数都是必须传递的,但是我们可以通过在参数名后加上 ”?“表示该参数”可有可无“

let myAdd = function (x: number, y: number): number {
  return x + y;
};

myAdd(10) // error, 应该有两个参数 
let myAdd = function (x:number, y?:number): number {
  if (y) {
    return x + y;
  }
  return x;
};

myAdd(10); // ok
myAdd(10, 20); // ok

注意:可选参数必须跟在必须参数后面。

函数重载

在 js 中,有时会需要一个函数可以传入不同类型的参数并返回不同类型的数据。

当在 ts 中定义这种函数时,可以为同一个函数提供多个函数类型定义来进行函数重载,编译器会根据函数类型定义列表来处理函数的调用。

function getResults(x: number): number;
function getResults(x: string): {name: string};
function getResults(x: any): any {
  if (typeof x === "number") {
    return (x / 365) * 5;
  }else if(typeof x === 'string'){
    return {
      name: x,
    }
  }
}

// 上述函数中function getResults(x: number): number 和 function getResults(x: string): {name: string} 是函数重载列表
// 上述函数中function getResults(x: any): any 并不是重载的一部分
// 函数调用时,它会从上往下依次查找重载列表,直到遇到匹配的定义

getResults(199) // ok
getResults('jack') // ok
getResults(true) // 报错, 没有与此调用匹配的重载

类型断言

有时,你会比 ts 更确定某个具体值的信息,当 ts 因为某个值的类型发出问题警告时,可以使用类型断言的方式来明确指定具体的类型给某个值。

类型断言可以使用 “<>” 尖括号语法 或者 as 语法

function returnVal(val:any): string | number{
  return val
}

let strLength:number = returnVal('ts').length; // error 类型“string | number”上不存在属性“length”
let assertionsStr:number = (<string>returnVal('ts')).length; // ok
let assertionsAsStr:number = (returnVal('ts') as string).length; // ok

注意并不是所有类型都可以随便断言的,例如

class Student {
  studentId: number
}

class Teacher {
  workId: number
}

let stu:Student = {
  studentId: 2847
};

console.log((stu as Teacher).workId) // 不能强行把学生断言为老师

如果非要让其通过类型检查,可以先把它转化为 unknown(不建议用 any)

console.log((stu as unknown as Teacher).workId)