TypeScript语法进阶

TypeScript中的配置文件

编译文件指定

tsc --init

初始化一个新文件夹并且生成一个tsconfig.json文件

直接 tsc指定编译文件 不会应用tsconfig.json的配置的

tsc demo.ts

直接运行tsc则会读取配置文件(ts-node demo.ts 也会使用tsconfig.json)

tsc

运行 tsc 会先到tsconfig.json中查找配置中需要编译的文件(include),没有则默认把根目录下的ts文件编译

  • include / files指定需要编译的文件(正则匹配)
  • exclude指定不编译的文件(正则匹配)
{
    "include"["./demo.ts"],
	"compilerOptions"{
        ...
    }
}

compilerOptions

  • removeComments:移除ts对应的注释项
  • strict:对应的strict以下的配置都是true
  • noImplicitAny:不允许隐士配置any(true: 需要手动设置any类型)
  • strictNullChecks:强制检查null类型 (true : const str:string = null报错)
  • rootDir/outDir:指定编译的文件和输出的位置
  • incremental:增量编译,对已经编译的文件不再编译
  • target:es5编译后的语法
  • allowJs:对js文件的编译,(e.g. es6=>es5)
  • checkJs: 是否检查JS文件的语法
  • sourceMap:是否生成代码的.map文件
  • noUnusedLocals:有定义但未使用的变量检查
  • noUnusedParameters:有定义但未使用的函数参数检查
  • baseUrl:根路径指定

......

联合类型和类型保护

联合类型

type a | type b

interface Bird {
  fly: boolean;
  sing: () => {};
}

interface Dog {
  fly: boolean;
  bark: () => {};
}

function trainAnimal(animal: Bird | Dog) {
  // 语法只会提示共有的fly,typescript无法判断具体是哪种
  animal.fly...
}

unknow any never

unknow与any类型是有区别的,any是没有类型,unknown是有类型,但是不知道;

never是永远没有返回类型,比如执行一半报错的函数;

let abc: unknown = "hello"// Type 'unknown' is not assignable to type 'string'
// let b: string = abc;
let b: string = abc as string;

let str = "string";
let a: number = str as unknown as number;

类型保护

类型断言

function trainAnimal(animal: Bird | Dog) {
  if (animal.fly) {
    (animal as Bird).sing();
  } else {
    (animal as Dog).bark();
  }
}

let abc: unknown = "hello";
let b: string = abc as string;

let str = "string";
//  类型 "string" 到类型 "number" 的转换可能是错误的,因为两种类型不能充分重叠。如果这是有意的,请先将表达式转换为 "unknown"
//  let a: number = str as number;
let a: number = str as unknown as number;

类型断言有两种形式,两种形式是等价的。 至于使用哪个大多数情况下是凭个人喜好;然而,当你在TypeScript里使用JSX时,只有as语法断言是被允许的。

let strLength: number = (<string>someValue).length;
let strLength: number = (someValue as string).length;

as const

断言为const数据,把数据的基本类型转化为了值类型

// let a: number
let a = 99;
// const b: 99
const b = 99;
// let c: 99   
let c = 99 as const;
c = 99;

// let arr: number[] 对于数组对象这种引用类型,let和const不影响类型推断
let arr = [a];

// const arr1: (string | number | boolean)[]
const arr1 = [a, 'string', 123, false];
// const arr2: readonly [number, "string", 123, false] 转化成了元组
// <const>[a, 'string', 123, false] 也可以达到和as断言一样的效果
const arr2 = [a, 'string', 123, false] as const;

// let obj: { age: number; }
let obj = { age: 11 };
// let obj1: { readonly age: 11; }
let obj1 = { age: 11 } as const;

实例

  • 断言
// function func(): (string | ((x: number, y: number) => number))[]
function func() {
  let a = "Jerry";
  let b = (x: number, y: number): number => x + y;
  return [a, b];
}
const [m, n] = func();
// m可能是string或者Function
console.log((n as Function)(1, 2));   // 3
console.log((n as (x: number, y: number) => number)(1, 2));   //3

// as [string, (x: number, y: number) => number]
const [a, b] = func() as [string, Function];
console.log(b(4, 5));   // 9
  • typeof
function func() {
  let a = "Jerry";
  let b = (x: number, y: number): number => x + y;
  return [a, b] as [typeof a, typeof b];
}

const [m, n] = func();
console.log(n(4, 5));   // 9
  • as const
function func() {
  let a = "Jerry";
  let b = (x: number, y: number): number => x + y;
  // return [a, b] as [typeof a, typeof b];
  return [a, b] as const;
}

const [m, n] = func();
console.log(n(4, 5));   // 9

非空断言

// const el: HTMLDivElement = document.querySelector(".container") as HTMLDivElement;
// !是非空断言,而as能具体断言为某个类型
const el: HTMLDivElement = document.querySelector(".container")!;
console.log(el.innerHTML);

in 语法

function trainAnimal(animal: Bird | Dog) {
  if ('sing' in animal) {
    animal.sing();
  } else {
    animal.bark();
  }
}

typeof 语法

function add(first: string | number, second: string | number) {
  // 运算符“+”不能应用于类型“string | number”和“string | number”
  // return first + second
  if (typeof first === 'string' || typeof second === 'string') {
    return `${first}${second}`
  } else {
    return first + second
  }
}

instanceof 语法

Intanceof做类型保护(class才可以使用instanceof , interface不可以)

class NumberObj {
  count: number;
}

function addSecond (first : object | NumberObj, second : object | NumberObj) {
  if(first instanceof NumberObj && second instanceof NumberObj) {
    return first.count + second.count
  }
  return 0
}

Enum 枚举类型

npm init -y
npm install ts-node -D
npm install typescript -D
  • JavaScript(场景:某个状态的值固定)
const Status = {
  OFFLINE: 0,
  ONLINE: 1,
  DELETED: 2
};

function getResult(status) {
  if (status === Status.OFFLINE) {
    return 'offline';
  } else if (status === Status.ONLINE) {
    return 'online';
  } else if (status === Status.DELETED) {
    return 'deleted';
  }
  return 'error';
}

const result = getResult(Status.ONLINE);
console.log(result);

  • 枚举类型
// 枚举类型的默认值从0开始,  0,1,2...
enum Status {
  OFFLINE,
  ONLINE,
  DELETED
}

function getResult(status: any) {
  if (status === Status.OFFLINE) {
    return 'offline';
  } else if (status === Status.ONLINE) {
    return 'online';
  } else if (status === Status.DELETED) {
    return 'deleted';
  }
  return 'error';
}

const result = getResult(1);     //	online
console.log(Status.OFFLINE);     // 0
console.log(Status.ONLINE);		 // 1
console.log(Status.DELETED);	 // 2
console.log(Status[0]);	   // OFFLINE     反向映射   
// 如果中间的某个值被设置了,后续的+1自增
enum Status {
  OFFLINE,
  ONLINE = 4,
  DELETED
}
// 0 4 5
// Status[0]    undefined

函数泛型

  • 场景
function join(first: string | number, second: string | number) {
  return `${first}${second}`;
}

join('1', 1);

// 需求如果fisrt是number或者string,second也要一样?
  • 泛型 generic 泛指的类型,在使用的时候才会知道具体的类型,可以显式得声明,也可以让typescript进行类型推断
// ABC 泛型(任意类型)
function join<ABC>(first: ABC,second: ABC) {
  return `${first}${second}`
}

// 报错:类型“1”的参数不能赋给类型“string”的参数
join("1",1)
join<string>('1',1)
// success
join<string>('1','1')
join<number>(1,1)
  • 泛型的使用
function map<ABC>(params: ABC) {
  return params
}
map<string>('123')     //  123

// T[]  与  Array<T>  等价
function map1<T>(params: T[]) {
  return params
}
map1<string>(['123'])     //  ['123']

function map1<T>(params: Array<T>) {
  return params
}
map2<string>(['123'])     //  ['123']
  • 多个泛型的定义
function join<T, P>(first: T, second: P) {
  return `${first}${second}`
}

join<number, string>(1,'1')
// 如果没有写类型,typescript会类型推断
join(1,'1')

类中的泛型以及泛型类型、

类中的泛型

class DataManager {
	// 可以传string或者number类型的数组...
	constructor (private data: string [] | number[]) {}
	getItem(index: number): string|number {
		return this.data[index]
	}
}

const data = new DataManager(['1']);
data.getItem(0)           //   '1'

为了能传入各种类型的参数,并且返回和传入相应的类型,减少代码量

class DataManager<T> {
	constructor (private data: T[]) {}
	getItem(index: number): T {
		return this.data[index]
	}
}

// const data = new DataManager(['1']);    类型推断
const data = new DanaManager<number>([1]);
data.getItem(0)           //   '1'
  • 泛型类型约束(extends)

如果要求this.data[index].name有值,传入T类型一定要包含name属性,此时返回值是string

interface Item {
	name: string;
}

class DataManager<T extends Item> {
	constructor (private data: T[]) {}
	getItem(index: number): string {
		return this.data[index].name;
	}
}

const data = new DanaManager([
	{
		name: 'caffrey'
	}
]);


//  interface Test {
//  	name: string
//  }
//  const data = new DanaManager<Test>([{name: 'caffrey'}]);
  • 类接收的泛型的具体类型只能是number和string
class DataManager<T extends number | string> {
	constructor (private data: T[]) {}
	getItem(index: number): T {
		return this.data[index]
	}
}

const data = new DataManager<string>([1])

泛型声明类型

// 如何使用泛型作为一个具体的类型注解
function hello<T>(params: T) {
    return params;
}

//  const func: (T)=>string = <T>() => {
//  	return '123'
//  }
const func: <T>(patams: T) => T = hello;

命名空间

npm init -y
tsc -init

tsconfig.json

"outDir": "./dist",
"rootDir": "./src"

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./page.js"></script>
  </head>
  <body>
    <script>
      new Page();
    </script>
  </body>
</html>

page.ts

class Header {
  constructor() {
    const elem = document.createElement('div');
    elem.innerText = 'this is header';
    document.body.appendChild(elem);
  }
}

class Content {
  constructor() {
    const elem = document.createElement('div');
    elem.innerText = 'this is content';
    document.body.appendChild(elem);
  }
}

class Footer {
  constructor() {
    const elem = document.createElement('div');
    elem.innerText = 'this is footer';
    document.body.appendChild(elem);
  }
}

class Page {
  constructor() {
    new Header();
    new Content();
    new Footer();
  }
}

运行 tsc -w (自动检测变化编译)

|-- namespace
|   |-- dist
|   |   |-- page.js
|   |   |-- index.html
|   |-- src
|   |   |-- page.ts
|   |-- package.json
|   |-- tsconfig.json

问题:编译后的page.js文件暴露了很多的全局变量(控制台能够拿到Header、Content、Footer、Page),而实际上需要保留的只有Page这个全局变量,需要在typescript中融入一些模块化的思想。

namespace

namespace Home {
  class Header {
    constructor() {
      const elem = document.createElement('div');
      elem.innerText = 'this is header';
      document.body.appendChild(elem);
    }
  }

  class Content {
    constructor() {
      const elem = document.createElement('div');
      elem.innerText = 'this is content';
      document.body.appendChild(elem);
    }
  }

  class Footer {
    constructor() {
      const elem = document.createElement('div');
      elem.innerText = 'this is footer';
      document.body.appendChild(elem);
    }
  }
  // 如果没有export,此时在控制台只能拿到一个空对象Home,export编译后(Home.Page = Page)
  // new Home.page()
  export class Page {
    constructor() {
      new Header();
      new Content();
      new Footer();
    }
  }
}

全局变量只有一个Home,而且Home只暴露了一个Page方法(export)

命名空间相互引用

将组件放在单独的文件中去管理,与页面逻辑分开(component / page两个全局变量)

// Components
namespace Components {
  export class Header {
    constructor() {
      const elem = document.createElement('div');
      elem.innerText = 'this is header';
      document.body.appendChild(elem);
    }
  }

  export class Content {
    constructor() {
      const elem = document.createElement('div');
      elem.innerText = 'this is content';
      document.body.appendChild(elem);
    }
  }

  export class Footer {
    constructor() {
      const elem = document.createElement('div');
      elem.innerText = 'this is footer';
      document.body.appendChild(elem);
    }
  }
}
// Home
namespace Home {
  export class Page {
    constructor() {
      new Components.Header();
      new Components.Content();
      new Components.Footer();
    }
  }
}

引用

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./dist/components.js"></script>
    <script src="./dist/page.js"></script>
  </head>
  <body>
    <script>
      new Home.Page();
    </script>
  </body>
</html>

打包生成单个文件

这样导致出现了多个js文件的引入,如果希望只生成一个单独的文件,修改tsconfig.json(这种情况下module只支持amd和system),不支持commonjs的模块化规范(修改后由于IDE延迟需要重新打开编辑器)

"module": "amd",
"outFile": "./build/page.js"

打包后:

|-- namespace
|   |-- build
|   |   |-- page.js
|   |-- src
|   |   |-- components.ts
|   |   |-- page.ts
|   |   |-- index.html
|   |-- package.json
|   |-- tsconfig.json

命名空间依赖声明

namespace依赖之前的相互引用可以做一个声明

Home这个命名空间依赖于Components

///<reference path='./components.ts' />

namespace Home {
  export class Page {
    constructor() {
      new Components.Header();
      new Components.Content();
      new Components.Footer();
    }
  }
}

内部其他声明

  • 命名空间内部声明interface

  • 命名空间导出子命名空间

namespace Components {
  // new Components.SubComponents.Test()
  export namespace SubComponents {
    export class Test {}
  }
  // 声明interface
  export interface User {
    name: string;
  }
  
  ...Header Content Footer...
}
///<reference path='./components.ts' />

namespace Home {
  export class Page {
    // interface使用
    user: Components.User = {
      name: 'caffrey'
    }
    constructor() {
      new Components.Header();
      new Components.Content();
      new Components.Footer();
    }
  }
}

import对应的模块化

修改Components

export class Header {
  constructor() {
    const elem = document.createElement('div');
    elem.innerText = 'this is header';
    document.body.appendChild(elem);
  }
}

export class Content {
  constructor() {
    const elem = document.createElement('div');
    elem.innerText = 'this is content';
    document.body.appendChild(elem);
  }
}

export class Footer {
  constructor() {
    const elem = document.createElement('div');
    elem.innerText = 'this is footer';
    document.body.appendChild(elem);
  }
}
import { Header, Content, Footer } from './components';

class Page {
  constructor() {
    new Header();
    new Content();
    new Footer();
  }
}

打包生成AMD某块语法的js文件(module: amd),浏览器不支持AMD(define => require)语法,需要引入能够识别define语法支持的文件。而且现在是在page这个模块定义了Page这个class

import { Header, Content, Footer } from './components';

export default class Page {
  constructor() {
    new Header();
    new Content();
    new Footer();
  }
}
export class Header {
  constructor() {
    const elem = document.createElement('div');
    elem.innerText = 'this is header';
    document.body.appendChild(elem);
  }
}

export class Content {
  constructor() {
    const elem = document.createElement('div');
    elem.innerText = 'this is content';
    document.body.appendChild(elem);
  }
}

export class Footer {
  constructor() {
    const elem = document.createElement('div');
    elem.innerText = 'this is footer';
    document.body.appendChild(elem);
  }
}
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.js"></script>
    <script src="./build/page.js"></script>
  </head>
  <body>
    <script>
      require([
        'page',
        function(page) {
          new page.default();
        }
      ]);
    </script>
  </body>
</html>

这种引入require.js文件的方式有些麻烦,可以结合webpack进行处理

使用parcel打包代码

npm install parcel@next -D
"scripts": {
	"start": "parcel ./src/index.html"
}

运行parcel对TS代码进行编译,然后在本地(localhost:1234)起一个服务器,然后我们便可以直接访问index.html文件,引入的ts代码以及被编译

// index.html
<script src="./page.ts"></script>

描述文件中的全局类型

  • 定义全局变量 declare var
  • 定义全局函数 declare function
  • 定义全局对象 declare namespace

仍然使用parcel对代码进行编译,引入jquery代码

<--! index.html -->
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script>
<script src="./page.ts"></script>
// page.ts
$(function() {
    alert(123)
})
// $里面传一个字符串,如果没继续声明会报错,但是parcel会尝试打包,正常运行
$(function() {
    $('body').html('<div>123</div>')
})

此时运行npm run start代码可以运行,但是typescript下编辑器会对$进行报错;此时需要类型定义文件**.d.ts**

类型定义文件如果不引入,如何自己写一个类型定义文件

定义全局变量/函数

// jquery.d.ts
// $是一个返回值为空的函数,接收的参数也是一个返回值为空的函数
// 定义全局变量
// declare var $: (param: () => void) => void;

interface JqueryInstance {
    html: (html: string) => JqueryInstance;
}

// 定义全局函数
declare function $(readyFuc: () => void): void;
// 函数重载,$还可以接收一个字符串,返回也是一个jquery对象,有html方法
declare function $(selector: string): JqueryInstance;

interface进行函数重载

interface里面如果定义了多个函数的声明,就是函数的重载

当$既要是一个函数也是对象,使用declare声明;而如果只是函数的重载,interface也支持

interface JQuery {
    (readyFunc: ()=> void): void;
    (selector: string): JqueryInstance;
}

declare $:JQuery;

定义全局对象

$(function() {
    $('body').html('<div>123</div>');
    // $ 还是一个对象, new一个class
    new $.fn.init();
})
declare namespace {
    namespace fn {
    	class init {}
	}
}

模块代码的类型描述文件

使用模块包的方式安装jquery

npm install --save jquery

使用import引入模块包,需要声明juqery的模块declare module 'jquery'

// Es6 模块化 (jquery.d.ts)
declare module 'jquery' {
  interface JqueryInstance {
    html: (html: string) => JqueryInstance;
  }
  // 混合类型,不需要再declare
  function $(readyFunc: () => void): void;
  function $(selector: string): JqueryInstance;
  namespace $ {
    namespace fn {
      class init {}
    }
  }
  // 模块化定义.d.ts,需要export
  export = $;
}
// page.ts
import $ from 'jquery';

$(function() {
  $('body').html('<div>123</div>');
  new $.fn.init();
});

common和umd模块的声明和ES6略有不同,需查阅

泛型中keyof语法的使用

// page.ts
interface Person {
  name: string;
  age: number;
  gender: string;
}

class Teacher {
  constructor(private info: Person) {}
    
  getInfo(key: string) {
    // getInfo返回值为any
    // return this.info[key];
      
    // (method) Teacher.getInfo(key: string): string | number | undefined
   	if (key === 'name' || key === 'age' || key === 'gender') {
      return this.info[key];
    }
  }
}

const teacher = new Teacher({
  name: 'caffrey',
  age: 18,
  gender: 'male'
});

// const test = teacher.getInfo('name') as string;
const test = teacher.getInfo('name');
console.log(test);

如果有类是一个对象,希望通过key值获取对象的内容,并且能够获取到正确的类型推断:

通过泛型结合keyof来确定返回值类型

interface Person {
  name: string;
  age: number;
  gender: string;
}

// T extends 'name'
// type T = 'name'
// key: 'name'
// Person['name']

// type T = 'age'
// key: 'age'
// Person['age']
class Teacher {
  constructor(private info: Person) {}
  getInfo<T extends keyof Person>(key: T): Person[T] {
    return this.info[key];
  }
}

const teacher = new Teacher({
  name: 'caffrey',
  age: 18,
  gender: 'male'
});

// const test: string
const test = teacher.getInfo('name');
// const test: number
const test = teacher.getInfo('age');
// 类型“"abc"”的参数不能赋给类型“"name" | "age" | "gender"”的参数
const test = teacher.getInfo('abc');
console.log(test);

类型的值可以是一个字符串

type NAME = 'name';
const abc: NAME = 'name';

// 不能将类型“"hello"”分配给类型“"name"”
const abc: NAME = 'hello';
上次更新:
贡献者: Ahon-pan