14-TypeScript

352 阅读15分钟
  • js项目如何升级为ts?有何影响?
  • tslint都能配置哪些功能?对开发流程有何影响?
  • ts 基础类型都哪些,他们跟js的区别
  • ts为什么会流行?与ECMA新规范的关系?
  • 如何理解接口,泛型?

一、TypeScript核心语法

TypeScript是JavaScript的超集,它可编译为纯JavaScript,是一种给 JavaScript 添加特性的语言扩展。

TypeScript开发环境搭建

  • 安装typescript并初始化配置
npm i typescript -g
tsc --init
npm init -y
  • 编译ts文件
tsc ./src/index.ts
  • 工程化

    • 安装相关依赖
    npm i webpack webpack-cli webpack-dev-server ts-loader typescript html-webpack-plugin -D
    
    • 配置文件(build/webpack.config.js)
     const HtmlWebpackPlugin = require("html-webpack-plugin")
    
      module.exports = {
        entry: "./src/index.ts",
        output: {
          filename: "app.js"
        },
        resolve: {
          extensions: [".js", ".ts", ".tsx"]
        },
        devtool: "cheap-module-eval-source-map",
        module: {
          rules: [
            {
              test: /\.tsx?$/i,
              use: [
                {
                  loader: "ts-loader"
                }
              ],
              exclude: /node_modules/
            }
          ]
        },
        plugins: [
          new HtmlWebpackPlugin({
            template: "./public/index.html"
          })
        ]
      }
    
    • 添加开发脚本(package.json)
    "scripts": {
      "dev": "webpack-dev-server --config ./build/webpack.config.js"
    },
    

基础类型

类型注解:变量后面通过冒号+类型来做类型注解

布尔值

最基本的数据类型就是简单的true/false值,在JavaScript和TypeScript里叫做boolean(其它语言中也一样)。

let isDone: boolean = false;

数字

和JavaScript一样,TypeScript里的所有数字都是浮点数。 这些浮点数的类型是 number。 除了支持十进制和十六进制字面量,TypeScript还支持ECMAScript 2015中引入的二进制和八进制字面量。

let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
let binaryLiteral: number = 0b1010;
let octalLiteral: number = 0o744;

字符串

JavaScript程序的另一项基本操作是处理网页或服务器端的文本数据。 像其它语言里一样,我们使用 string表示文本数据类型。 和JavaScript一样,可以使用双引号( ")或单引号(')表示字符串。

let name: string = "bob";
name = "smith";

你还可以使用模版字符串,它可以定义多行文本和内嵌表达式。 这种字符串是被反引号包围( `),并且以${ expr }这种形式嵌入表达式

let name: string = `Gene`;
let age: number = 37;
let sentence: string = `Hello, my name is ${ name }.

I'll be ${ age + 1 } years old next month.`;

这与下面定义sentence的方式效果相同:

let sentence: string = "Hello, my name is " + name + ".\n\n" +
    "I'll be " + (age + 1) + " years old next month.";

数组

TypeScript像JavaScript一样可以操作数组元素。 有两种方式可以定义数组。 第一种,可以在元素类型后面接上 [],表示由此类型元素组成的一个数组:

let list: number[] = [1, 2, 3];

第二种方式是使用数组泛型,Array<元素类型>:

let list: Array<number> = [1, 2, 3];

元组 Tuple

元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。 比如,你可以定义一对值分别为 string和number类型的元组。

// Declare a tuple type
let x: [string, number];
// Initialize it
x = ['hello', 10]; // OK
// Initialize it incorrectly
x = [10, 'hello']; // Error

当访问一个已知索引的元素,会得到正确的类型:

console.log(x[0].substr(1)); // OK
console.log(x[1].substr(1)); // Error, 'number' does not have 'substr'

当访问一个越界的元素,会使用联合类型替代:

x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型

console.log(x[5].toString()); // OK, 'string' 和 'number' 都有 toString

x[6] = true; // Error, 布尔不是(string | number)类型

枚举

enum类型是对JavaScript标准数据类型的一个补充。 像C#等其它语言一样,使用枚举类型可以为一组数值赋予友好的名字。

enum Color {Red, Green, Blue}
let c: Color = Color.Green;

默认情况下,从0开始为元素编号。 你也可以手动的指定成员的数值。 例如,我们将上面的例子改成从 1开始编号:

enum Color {Red = 1, Green, Blue}
let c: Color = Color.Green;

或者,全部都采用手动赋值:

enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green;

枚举类型提供的一个便利是你可以由枚举的值得到它的名字。 例如,我们知道数值为2,但是不确定它映射到Color里的哪个名字,我们可以查找相应的名字:

enum Color {Red = 1, Green, Blue}
let colorName: string = Color[2];

console.log(colorName);  // 显示'Green'因为上面代码里它的值是2

Any

有时候,我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。 这些值可能来自于动态的内容,比如来自用户输入或第三方代码库。 这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。 那么我们可以使用 any类型来标记这些变量:

let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean

在对现有代码进行改写的时候,any类型是十分有用的,它允许你在编译时可选择地包含或移除类型检查。 你可能认为 Object有相似的作用,就像它在其它语言中那样。 但是 Object类型的变量只是允许你给它赋任意值 - 但是却不能够在它上面调用任意的方法,即便它真的有这些方法:

let notSure: any = 4;
notSure.ifItExists(); // okay, ifItExists might exist at runtime
notSure.toFixed(); // okay, toFixed exists (but the compiler doesn't check)

let prettySure: Object = 4;
prettySure.toFixed(); // Error: Property 'toFixed' doesn't exist on type 'Object'.

当你只知道一部分数据的类型时,any类型也是有用的。 比如,你有一个数组,它包含了不同的类型的数据:

let list: any[] = [1, true, "free"];

list[1] = 100;

Void

某种程度上来说,void类型像是与any类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是 void:

function warnUser(): void {
    console.log("This is my warning message");
}

声明一个void类型的变量没有什么大用,因为你只能为它赋予undefined和null:

let unusable: void = undefined;

Never

never类型表示的是那些永不存在的值的类型。 例如, never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型; 变量也可能是 never类型,当它们被永不为真的类型保护所约束时。

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

下面是一些返回never类型的函数:

// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
    throw new Error(message);
}

// 推断的返回值类型为never
function fail() {
    return error("Something failed");
}

// 返回never的函数必须存在无法达到的终点
function infiniteLoop(): never {
    while (true) {
    }
}

Object

object表示非原始类型,也就是除number,string,boolean,symbol,null或undefined之外的类型。

使用object类型,就可以更好的表示像Object.create这样的API。例如:

declare function create(o: object | null): void;

create({ prop: 0 }); // OK
create(null); // OK

create(42); // Error
create("string"); // Error
create(false); // Error
create(undefined); // Error

类型断言

有时候你会遇到这样的情况,你会比TypeScript更了解某个值的详细信息。 通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。

通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。 类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。 TypeScript会假设你,程序员,已经进行了必须的检查。

类型断言有两种形式。 其一是“尖括号”语法:

let someValue: any = "this is a string";

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

另一个为as语法:

let someValue: any = "this is a string";

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

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

类型别名

使用类型别名自定义类型,与接口几乎是一样的(interface)

// 可以用下面这样方式定义对象类型
const objType: { foo: string, bar: string }

// 使用type定义类型别名,使用更便捷,还能复用 
type Foobar = { foo: string, bar: string } 
const aliasType: Foobar

示例:

export type Feature = {
  id: number,
  name: string
}
<template>
  <div>
<!--修改模板-->
    <li v-for="feature in features" :key="feature.id">{{feature.name}}</li>
  </div>
</template>
<script lang='ts'>
// 导入接口
import { Feature } from "@/types";
@Component
export default class Hello extends Vue {
  // 修改数据结构
  features: Feature[] = [{ id: 1, name: "类型注解" }]; 
}
</script>

联合类型

希望某个变量或参数的类型是多种类型中之一

let union: string | number;
union = '1'; // ok
union = 1; // ok

交叉类型

想要定义某种由多种类型合并而成的类型使用交叉类型

type First = {first: number};
type Second = {second: number};
// FirstAndSecond将同时拥有属性first和second 
type FirstAndSecond = First & Second;

函数

完整函数类型

函数类型包含两部分:参数类型和返回值类型。

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

第二部分是返回值类型。 对于返回值,我们在函数和返回值类型之前使用( =>)符号,使之清晰明了。 如之前提到的,返回值类型是函数类型的必要部分,如果函数没有返回任何值,你也必须指定返回值类型为 void而不能留空。

// myAdd has the full function type
let myAdd = function(x: number, y: number): number { return x + y; };

// The parameters `x` and `y` have the type number
let myAdd: (baseValue: number, increment: number) => number =
    function(x, y) { return x + y; };

尝试这个例子的时候,你会发现如果你在赋值语句的一边指定了类型但是另一边没有类型的话,TypeScript编译器会自动识别出类型。

必填参数

function greeting(person: string): string {
  return "Hello, " + person;
}
greeting('tom')

可选参数

function greeting(person: string, msg?: string): string {
  return "Hello, " + person;
}

默认值

function greeting(person: string, msg = ''): string {
  return "Hello, " + person;
}

剩余参数

必要参数,默认参数和可选参数有个共同点:它们表示某一个参数。 有时,你想同时操作多个参数,或者你并不知道会有多少参数传递进来。 在JavaScript里,你可以使用 arguments来访问所有传入的参数。

在TypeScript里,你可以把所有参数收集到一个变量里:

function buildName(firstName: string, ...restOfName: string[]) {
  return firstName + " " + restOfName.join(" ");
}

let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");

剩余参数会被当做个数不限的可选参数。 可以一个都没有,同样也可以有任意个。

函数重载

以参数数量或类型区分多个同名函数

// 重载1
function watch(cb1: () => void): void;
// 重载2
function watch(cb1: () => void, cb2: (v1: any, v2: any) => void): void; // 实现

function watch(cb1: () => void, cb2?: (v1: any, v2: any) => void) {
  if (cb1 && cb2) { 
  	console.log('执行watch重载2');
  } else { 
  	console.log('执行watch重载1');
  } 
}

ts中的类和es6中大体相同,这里重点关注ts带来的访问控制等特性

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

let greeter = new Greeter("world");

我们声明一个 Greeter类。这个类有3个成员:一个叫做 greeting的属性,一个构造函数和一个 greet方法。

你会注意到,我们在引用任何一个类成员的时候都用了 this。 它表示我们访问的是类的成员。

最后一行,我们使用 new构造了 Greeter类的一个实例。 它会调用之前定义的构造函数,创建一个 Greeter类型的新对象,并执行构造函数初始化它。

class Parent {
  private _foo = "foo"; // 私有属性,不能在类的外部访问 
  protected bar = "bar"; // 保护属性,可以在子类中访问
  // 构造函数参数加修饰符,能够定义为成员属性 
  constructor(public tua = "tua") {}
  // 方法也有修饰符
  private someMethod() {}
  // 存取器:属性方式访问,可添加额外逻辑,控制读写性 
  get foo() {
    return this._foo;
  }
  set foo(val) {
    this._foo = val;
  } 
}
class Child extends Parent {
    baz() {
      this.foo;
      this.bar;
      this.tua; 
	}
}

接口

接口仅约束结构,不要求实现,使用更简单

// Person接口定义了解构 
interface Person {
  firstName: string;
  lastName: string;
}
// greeting函数通过Person接口约束参数解构 
function greeting(person: Person) {
  return 'Hello, ' + person.firstName + ' ' + person.lastName;
}
greeting({firstName: 'Jane', lastName: 'User'}); // 正确 
greeting({firstName: 'Jane'}); // 错误

可选属性

接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。 可选属性在应用“option bags”模式时很常用,即给函数传入的参数对象中只有部分属性赋值了。

interface SquareConfig {
  color?: string;
  width?: number;
}

function createSquare(config: SquareConfig): {color: string; area: number} {
  let newSquare = {color: "white", area: 100};
  if (config.color) {
    newSquare.color = config.color;
  }
  if (config.width) {
    newSquare.area = config.width * config.width;
  }
  return newSquare;
}

let mySquare = createSquare({color: "black"});

只读属性

一些对象属性只能在对象刚刚创建的时候修改其值。 你可以在属性名前用 readonly来指定只读属性:

interface Point {
    readonly x: number;
    readonly y: number;
}

你可以通过赋值一个对象字面量来构造一个Point。 赋值后, x和y再也不能被改变了。

let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!

TypeScript具有ReadonlyArray<T>类型,它与Array<T>相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改:

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!

上面代码的最后一行,可以看到就算把整个ReadonlyArray赋值到一个普通数组也是不可以的。 但是你可以用类型断言重写:

a = ro as number[];

readonly vs const
最简单判断该用readonly还是const的方法是看要把它做为变量使用还是做为一个属性。 做为变量使用的话用 const,若做为属性则使用readonly

额外的属性检查

如果 SquareConfig带有上面定义的类型的colorwidth属性,并且还会带有任意数量的其它属性,那么我们可以这样定义它:

interface SquareConfig {
  color?: string;
  width?: number;
  [propName: string]: any;
}

SquareConfig可以有任意数量的属性,并且只要它们不是colorwidth,那么就无所谓它们的类型是什么。

泛型

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。以此增加代码通用性。

// 不用泛型
// interface Result {
// ok:0|1;
//   data: Feature[];
// }
// 使用泛型
interface Result<T> {
  ok: 0 | 1;
  data: T; 
}
// 泛型方法
function getResult<T>(data: T): Result<T> {
  return {ok:1, data};
}
// 用尖括号方式指定T为string 
getResult<string>('hello') 
// 用类型推断指定T为number 
getResult(1)
// 进一步约束类型变量 
interface Foo { 
  foo: string
}
// 约束T必须兼容Foo
function getResult<T extends Foo>(data: T): Result<T> {
  return {ok:1, data};
}
// 这样上面的两个调用就非法了

泛型优点:

  • 函数和类可以支持多种类型,更加通用
  • 不必编写多条重载,冗长联合类型,可读性好
  • 灵活控制类型约束
    不仅通用且能灵活控制,泛型被广泛用于通用库的编写

模拟接口

  module.exports = {
    devServer: {
        before(app) {
            app.get('/api/list', (req, res) => {
              res.json([
              	{ id: 1, name: "类型注解", selected: false },
  				{ id: 2, name: "编译型语言", selected: true }
              ]) 
  			})
        } 
   }
}

调用接口

async mounted() {
  const resp = await axios.get<FeatureSelect[]>('/api/list')
  this.features = resp.data
}

二、使用ts开发vue应用

新建一个基于ts的vue项目

在已存在项目中安装typescript

vue add @vue/typescript

从以下几点来示例ts特性

  • 特性列表(类型系统、类等)
  • 新增特性(函数)
  • 特性总数统计(存取器)
  • 异步数据获取(接口、泛型)
  • 获取属性、派发新增事件(装饰器)

特性列表

interface Feature {
  id: number;
  name: string;
  version: string;
}
export default Feature;
<template>
  <div>
    <ul>
      <li v-for="feature in features" :key="feature.id">
        {{feature.name}} <span class="tag">{{feature.version}}</span>
      </li>
	</ul> 
  </div>
</template>
<script lang='ts'>
import { Component, Vue } from "vue-property-decorator";
import Feature from "@/models/feature";
@Component
export default class Hello extends Vue {
	features: Feature[] = [{ id: 1, name: "类型注解", version: "1.0" }]; 
}
</script>
<style scoped>
li {
  padding: 8px;
}
.tag{
  background-color: rgb(30, 151, 199);
  color: white;
  border-radius: 4px;
  padding: 5px 10px;
}
</style>

新增特性

<template>
  <div>
	<!-- 1.添加输入框 --> 
    <div>
		<input type="text" placeholder="输入新特性" @keyup.enter="addFeature" /> 
    </div>
  </div>
</template>
<script lang='ts'>
@Component
export default class Hello extends Vue {
// 2.添加回调函数
addFeature(event: KeyboardEvent) {
    const input = event.target as HTMLInputElement;
    this.features.push({
      id: this.features.length + 1,
      name: input.value,
      version: "1.0"
	});
    input.value = "";
  }
}
</script>

利用getter设置计算属性

<template> 
  <li>特性数量:{{count}}</li>
</template>
<script lang="ts">
export default class HelloWorld extends Vue { 
  // 定义getter作为计算属性
  get count() {
    return this.features.length;
  }
}
</script>

获取异步数据

import axios from 'axios';
import Feature from '@/models/feature';
export function getFeatures() {
  // 通过泛型约束返回值类型,这里是Promise<AxiosResponse<Feature[]>> 
  return axios.get<Feature[]>('/api/list')
}

声明文件

使用ts开发时,如果使用第三方js库的同事,还想利用ts类型检查等特性,就需要声明文件,类似xx.d.ts

同时,vue项目中还可以在shims-vue.d.ts中对已存在模块进行补充:
npm i @types/xxx

  • 范例:利用模块补充$axios属性到Vue实例,从而在组件里面直接用
// main.ts
import axios from 'axios'
Vue.prototype.$axios = axios;
  
// shims-vue.d.ts
import Vue from "vue";
import { AxiosInstance } from "axios";
declare module "vue/types/vue" {
  interface Vue {
    $axios: AxiosInstance;
  }
}
  • 范例:给krouter/index.js编写声明文件,index.d.ts
import VueRouter from "vue-router";
declare const router: VueRouter
export default router

三、装饰器原理

装饰器用于扩展类或者它的属性和方法。

属性声明:@Prop

采用@Prop的方式声明组件属性

export default class HelloWorld extends Vue { 
  // Props()参数是为vue提供属性选项
  // !称为明确赋值断言,它是提供给ts的 
  @Prop({type: String, required: true}) 
  private msg!: string;
}

事件处理:@Emit

新增特性时派发事件通知

// 通知父类新增事件,若未指定事件名则函数名作为事件名(羊肉串形式) 
@Emit()
private addFeature(event: any) {// 若没有返回值形参将作为事件参数
  const feature = { name: event.target.value, id: this.features.length + 1 }; 
  this.features.push(feature);
  event.target.value = "";
  return feature;// 若有返回值则返回值作为事件参数
}

变更监测:@Watch

@Watch('msg')
onMsgChange(val:string, oldVal:any){
    console.log(val, oldVal);
}

装饰器原理

装饰器是工厂函数,它能访问和修改装饰目标。

类装饰器

类装饰器在类声明之前被声明(紧靠着类声明)。 类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。 类装饰器不能用在声明文件中(.d.ts),也不能用在任何外部上下文中(比如declare的类)。

类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。

function log(target: Function) {
  // target是构造函数
  console.log(target === Foo); // true 
  target.prototype.log = function() {
      console.log(this.bar);
    }
}
 
@log
class Foo {
	bar = 'bar' 
}
const foo = new Foo();

// @ts-ignore
foo.log();  

方法装饰器

方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。 它会被应用到方法的 属性描述符上,可以用来监视,修改或者替换方法定义。 方法装饰器不能用在声明文件(.d.ts),重载或者任何外部上下文(比如declare的类)中。

方法装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 成员的属性描述符。
function rec(target: any, name: string, descriptor: any) { 
  // 这里通过修改descriptor.value扩展了bar方法
  const baz = descriptor.value;
  descriptor.value = function(val: string) {
    console.log('run method', name); 
    baz.call(this, val);
  }
}
class Foo {
  @rec
  setBar(val: string) {
    this.bar = val
  } 
}
const foo=new Foo()  
foo.setBar('lalala')

属性装饰器

属性装饰器声明在一个属性声明之前(紧靠着属性声明)。 属性装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如 declare的类)里。

属性装饰器表达式会在运行时当作函数被调用,传入下列2个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
function mua(target, name) {
    target[name] = 'mua~~~'
}
class Foo {
  @mua ns!:string;
}
console.log(foo.ns)

四、学习ts源码

--待更新--

五、参考

TS