Typescript真香系列

Typescript

一、类型:

number、string、boolean、object、null、undefined、symbol都是比较简单的。

let num: number = 1; // 声明一个number类型的变量
let str: string = 'string'; // 声明一个string类型的变量
let bool: boolean = true; // 声明一个boolean类型的变量
let obj: object = { // 声明一个object类型的变量
  a: 1,
}
let syb: symbol = Symbol(); // 声明一个symbol类型的变量

null和undefined可以赋值给除了never的其他类型。

如果给变量赋予与其声明类型不兼容的值,就会有报错提示。
在这里插入图片描述
Array数组类型

在typescript中,有两种声明数组类型的方式。

方式一:

let arr: Array<number> = [1, 2, 3]; // 声明一个数组类型的变量

方式二:

let arr: number[] = [1, 2, 3]; // 声明一个数组类型的变量

Tuple元组类型

元组类似于数组,只不过元组元素的个数和类型都是确定的。

let tuple: [number, boolean] = [0, false];

any类型

当不知道变量类型的时候,可以先将其设置为any类型。

设置为any类型后,相当于告诉typescript编译器跳过这个变量的检查,因此可以访问、设置这个变量的任何属性,或者给这个变量赋任何值,编译器都不会报错。

let foo: any;
foo.test();
foo = 1;
foo = 'a';

void类型

通常用来声明没有返回值的函数的返回值类型。

function foo(): void {
}

never类型

通常用来声明永远不会正常返回的函数的返回值类型:

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

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

never与void的区别便是,void表明函数会正常返回,但是返回值为空。never表示的是函数永远不会正常返回,所以不可能有值。

enum枚举类型

使用枚举类型可以为一组数值赋予友好的名字。

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;

元素类型也支持字符串类型:

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

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

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

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

类型断言

有点类似于其他强类型语言的强制类型转换,可以将一个 值断言成某种类型,编译器不会进行特殊的数据检查和结构,所以需要自己确保断言的准确性。

断言有两种形式:
(1)尖括号语法

let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;

(2)as语法

let someValue: any = "this is a string";
let strLength: number = (<someValue as string).length;

大部分情况这两种都可以使用,但是在jsx中就只能使用as语法。

二、函数

函数类型:

函数类型主要声明的是参数和返回值的类型。

function sum(a: number, b: number): number {
  return a + b;
}

约等于

const sum: (numberA: number, numberB: number) => number = function(a: number, b: number): number {
  return a + b;
}

注意到类型定义时参数的名称不一定要与实际函数的名称一致,只要类型兼容即可。

可选参数

函数参数默认都是必填的,我们也可以使用可选参数。

function sum(a: number, b: number, c?: number): number {
  return c ? a + b + c : a + b;
}

重载

javascript本身是个动态语言。javascript里函数根据传入不同的参数而返回不同类型的数据是很常见的。

来看个简单但没什么用的例子:

function doNothing(input: number): number;
function doNothing(input: string): string;
function doNothing(input): any {
  return input;
}

console.log(doNothing(123));
console.log(doNothing('123'));

当然也可以使用联合类型,但是编译器就无法准确知道返回值的具体类型。

function doNothing(input: number | string): number | string {
  return input;
}
console.log(doNothing('123').length); // 错误:Property 'length' does not exist on type 'string | number'

如果只是单纯参数的个数不同,返回值类型一样,建议使用可选参数而不是重载。

function sum(a: number, b: number, c?: number) {
  return c ? a + b + c : a + b;
}

三、interface 接口

对于一些复杂的对象,需要通过接口来定义其类型。

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

const square: SquareConfig = {
  color: 'red', width: 0,
};

可选属性:

默认情况下,每个属性都是不能为空的。如果这么写,将会有报错。

interface SquareConfig {
  color: string;
  width: number;
}
const square: SquareConfig = {
  color: 'red',
};// error

可以将用"?"将width标志位可选的属性:

interface SquareConfig {
  color: string;
  width?: number;
}
const square: SquareConfig = {
  color: 'red',
};

只读属性:

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

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

如果在初始化后试图修改只读属性的值,将会报错。

let p: Point = { x: 10, y: 20 };
p.x = 20; // error

函数类型:

接口除了能够描述对象的结构之外,还能描述函数的类型。

interface SumFunc {
  (a: number, b: number): number;
}

let sum: SumFunc;

sum = (numberA: number, numberB: number) => {
  return numberA + numberB;
}

可以看到函数的类型与函数定义时只要参数类型一致即可,参数名不一定要一样。

可索引类型:

可索引类型,实际就是声明对象的索引的类型,与对应值的类型。接口支持两种索引类型,一种是number,一种是string,通过可索引类型可以声明一个数组类型。

interface StringArray {
  [index: number]: string;
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];

四、泛型

泛型在强类型语言中很常见,泛型支持在编写代码时候使用类型参数,而不必在一开始确定某种特定的类型。这样做的原因有两个:

1、有时候没办法在代码被使用之前知道类型。

例如我们封装了一个request函数,用来发起http请求,返回请求响应字段。

我们在实现request函数的时候,实际上是不能知道响应字段有哪些内容的,因为这跟特定的请求相关。

所以我们将类型确定的任务留给了调用者。

async function request<T>(url: string): Promise<T> {
  try {
    const result = await fetch(url).then((response) => {
      return response.json();
    });
    return result;
  } catch (e) {
    console.log('request fail:', e);
    throw e;
  }
}

async function getUserInfo(userId: string): void {
  const userInfo = await request<{
    nickName: string;
    age: number;
  }>(`user_Info?id=${userId}`);
  console.log(userInfo); // { nickName: 'xx', age: xx }
}

getUserInfo('123');

2、提高代码的复用率

如果对于不同类型,代码操作一样下,那么可以使用泛型来提高代码的复用率。

function getLen<T extends Array<any> | string>(arg: T): number {
	return arg ? arg.length : 0;
}

当然,您可能觉得这两点在javascript中都可以轻易做到,根本不需要泛型。是的,泛型本身是搭配强类型食用更佳的,在弱类型下没意义。

在typescript中,泛型有几种打开方式:

泛型函数

function someFunction<T>(arg: T) : T {
	return arg;
}

console.log(someFunction<number>(123));

泛型类型

  • interface
interface Memo<T> {
	id: T;
	age: number;
}
const memo: Memo<number> = {
	id: 123,
	age: 23,
}

  • type
type Memo<T> = { // 同上
  id: T;
  age: number;
}
const memo: Memo<string> = {
  id: '123',
  age: 123,
}

泛型类

class Memo<T> {	
	constructor(private id: T, private age: number) {};
	getId(): T {
		return this .id;
	}
}

我们也可以给类型变量加上一些约束。

泛型约束

有时编译器不能确定泛型里面有什么属性,就会出现报错的情况。

function logLength<T>(arg: T): T {
	console.log(arg.length);
	return arg;
}

解决方法是加上泛型约束。

interface TypeWithLength {
	length: number;
}
function logLength<T extends TypeWithLength>(arg: T): T {
	console.log(arg.length);// ok
	return arg;
}

五、高级类型

交叉类型

交叉类型是将多个类型合并为一个类型。

interface typeA {
	kelly?: number;
}
interface typeB {
	Amy?: number;
}

let value: typeA & typeB = {};
value.kelly = 1;
value.Amy = 2;

联合类型

联合类型表示变量属于联合类型中的某种类型,使用时需要先断言一下。

interface TypeA {
	kelly?: number;
}
interface TypeB{
	Amy?: number;
}

const value: TypeA | TypeB = {};

(<TypeA>value).a = 1;// ok

类型别名type

类型别名可以给一个类型起个新名字。类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。可以将type看做存储类型的特殊类型。

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
......

is

is关键字通常组成类型谓词,作为函数的返回值。谓词为 parameterName is Type这种形式, parameterName必须是来自于当前函数签名里的一个参数名。

function idFish(pet: Fish |Bird): pet is Fish {
	return (<Fish>pet).swim !== undefined;
}

这样子的好处是当函数调用后,如果返回true,编译器会将变量的类型锁定为那个具体的类型。

if (isFish(pet)) {
	pet.swim(); // 进入这里,编译器认为pet 是Fish类型
} else {
	pet.fly(); // 进入这里,编译器认为pet是Bird类型
}

keyof

keyof为索引类型查询操作符

interface Person {
    name: string;
    age: number;
}
type IndexType = keyof Person; // 'name' | 'age'

这样做的好处是使得编译器能够检查到动态属性的类型。

function pick<T, K extends keyof T >(obj: T, keys: K[]): T[K][] {
	return keys.map(key => obj[key]);
}

console.log(pick(person, ['name', 'age'])); // [string, number]

声明合并

为什么需要声明合并呢?

我们思考一下,在javascript中,一个对象是不是可能有多重身份。

例如说,一个函数,它可以作为一个普通函数执行,它也可以是一个构造函数。同时,函数本身也是对象,它也可以有自己的属性。

所以这注定了typescript中的类型声明可能存在的复杂性,需要进行声明的合并。

合并接口

最简单也最常见的声明合并类型是接口合并。从根本上说,合并的机制是把双方的成员放到一个同名的接口里。

interface Box {
    height: number;
    width: number;
}

interface Box {
    scale: number;
}

let box: Box = {height: 5, width: 6, scale: 10};

接口的非函数的成员应该是唯一的。如果它们不是唯一的,那么它们必须是相同的类型。如果两个接口中同时声明了同名的非函数成员且它们的类型不同,则编译器会报错。

对于函数成员,每个同名函数声明都会被当成这个函数的一个重载。同时需要注意,当接口 A与后来的接口 A合并时,后面的接口具有更高的优先级。

合并命名空间

Animals声明合并示例:

namespace Animals {
    export class Zebra { }
}

namespace Animals {
    export interface Legged { numberOfLegs: number; }
    export class Dog { }
}

等同于:

namespace Animals {
    export interface Legged { numberOfLegs: number; }

    export class Zebra { }
    export class Dog { }
}

命名空间与类和函数和枚举类型合并

类和命名空间的合并:

class Album {
  label: Album.AlbumLabel;
}
namespace Album {
  export class AlbumLabel{ }
}

函数与命名空间的合并:

function buildLabel(name: string): string {
  return buildLabel.prefix + name + buildLabel.suffix;
}

namespace buildLabel {
  export let suffix = '';
  export let prefix = 'Hello';
}
console.log(buildLabel('Sam Smith'));

此外,类与枚举、命名空间与枚举等合并也是可以的,这里不再话下。

六、声明文件

声明文件通常是以.d.ts结尾的文件。

如果只有ts、tsx文件,那么其实不需要声明文件。声明文件一般是在用第三方库的时候才会用到,因为第三方库都是js文件,加上声明文件之后,ts的编译器才能知道第三库暴露的方法、属性的类型。

声明语法

  • declare var、declare let、declare const声明全局变量
// src/jQuery.d.ts
declare let jQuery: (selector: string) => any;
  • declare function 声明全局方法
declare function jQuery: (selector: string): any;
  • declare class 声明全局类
// src/Animal.d.ts
declare class Animal {
    name: string;
    constructor(name: string);
    sayHi(): string;
}
  • declare enum 声明全局枚举类型
declare enum Directions {
    Up,
    Down,
    Left,
    Right
}
  • declare namespace 声明(含有子属性的)全局变量
// src/jQuery.d.ts
declare namespace jQuery {
	function ajax(url: string, settings?: any): void;
}
  • interface 和type声明全局类型
interface AjaxSettingsInterface {
	method?: 'GET' | 'POST'
	data?: any;
}

type AjaxSettingsType = {
	method?: 'GET'| 'POST'
	data?: any;
}
  • export 导出变量

在声明文件中只要用到了export、import就会被视为模块声明文件。模块声明文件中的declare关键字不能声明全局变量。

// types/foo/index.d.ts

export const name: string;
export function getName(): string;
export class Animal {
    constructor(name: string);
    sayHi(): string;
}
export enum Directions {
    Up,
    Down,
    Left,
    Right
}
export interface Options {
    data: any;
}

对应的导入和使用模块应该是这样:

// src/index.ts

import { name, getName, Animal, Directions, Options } from 'foo';

console.log(name);
let myName = getName();
let cat = new Animal('Tom');
let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
let options: Options = {
    data: {
        name: 'foo'
    }
};
  • export namespace 导出(含有子属性的)对象
// types/foo/index.d.ts

export namespace foo {
    const name: string;
    namespace bar {
        function baz(): string;
    }
}
// src/index.ts

import { foo } from 'foo';

console.log(foo.name);
foo.bar.baz();
  • export default ES6 默认导出
// types/foo/index.d.ts

export default function foo(): string;
// src/index.ts

import foo from 'foo';

foo();
  • export = commonjs 导出模块
// 整体导出
module.exports = foo;
// 单个导出
export.bar = bar;

在 ts 中,针对这种模块导出,有多种方式可以导入,

第一种方式是 const … = require:

// 整体导入
const foo = require('foo');
// 单个导入
const bar = require('foo').bar;

第二种方式是import … from,注意针对整体导出,需要使用 import * as来导入:

// 整体导入
import * as foo from 'foo';
// 单个导入
import { bar } from 'foo';

第三种方式是 import … require,这也是 ts 官方推荐的方式:

// 整体导入
import foo = require('foo');
// 单个导入
import bar = foo.bar;
  • export as namespace 库声明全局变量

既可以通过

一般使用 export as namespace时,都是先有了 npm 包的声明文件,再基于它添加一条 export as namespace语句,即可将声明好的一个变量声明为全局变量。

// types/foo/index.d.ts

export as namespace foo;
export = foo;

declare function foo(): string;
declare namespace foo {
    const bar: number;
}
  • declare global 扩展全局变量

使用 declare global可以在 npm 包或者 UMD 库的声明文件中扩展全局变量的类型。

// types/foo/index.d.ts

declare global {
    interface String {
        prependHello(): string;
    }
}

export {};
// src/index.ts

'bar'.prependHello();
  • declare module扩展模块

如果是需要扩展原有模块的话,需要在类型声明文件中先引用原有模块,再使用 declare module扩展

// types/moment-plugin/index.d.ts

import * as moment from 'moment';

declare module 'moment' {
    export function foo(): moment.CalendarKey;
}

// src/index.ts

import * as moment from 'moment';
import 'moment-plugin';

moment.foo();

七、项目接入

1、 对于所有的项目,接入ts的第一步就是安装typescript包,typescript包中包含tsc编译工具。

npm i typescript -D

2、 新建tsconfig.js文件,添加编译配置。

示例:

{
  "compilerOptions": {
    "noImplicitAny": false,
    "target": "es5",
    "jsx": "react",
    "allowJs": true,
    "sourceMap": true,
    "outDir": "./out",
    "module": "commonjs",
    "baseUrl": "./src"
  },
  "include": ["./src/**/*"],
  "exclude": ["./out"]
}

include 表示要编译的文件所在的位置

exclude 表示哪些位置的文件夹不需要进行编译,可以优化编译速度。

compilerOptions 中可以配置编译选项。其中nolmplicitAny表示是否禁止隐式声明any, 默认为false。

target 表示要将ts代码转换成的ECMAScript目标版本。

jsx: 可选preserve,react或者react-native。其中preserve表示生成的代码中保留所有的jsx标签,react-native等同于preserve,react表示将jsx标签转换成React.createElement函数调用。

allowJs 表示是否允许编译js文件,默认为false。

sourceMap 表示是否生成sourceMap, 默认false。

outDir 表示生成的目标文件所在的文件夹。

module 指定生成哪个模块系统的代码。

baseUrl 表示解析非相对模块名的基准目录。

详细配置参数文档请见:https://www.tslang.cn/docs/handbook/compiler-options.html

3、有了tsc和tsconfig,实际上就能将ts文件转换为js文件了。但是我们在实际工程的开发中,一般不会直接用tsc,例如在前端项目中,我们希望能与tsc能和webpack结合起来。在node服务端项目中,我们希望修改文件之后,能够只编译修改过的文件,并且重启服务。下面我将分别介绍前端webpack项目和node项目中接入ts的方法:

前端项目:

好了,非常简单就完成了webpack项目接入ts。

node 项目:

在node项目中,可以直接使用tsc编译文件,然后重启服务,但是这样在开发阶段显然是非常低效的。

能不能让node直接执行ts文件呢?这样结合nodemon,就可以很简单地做到修改文件后自动重启服务的效果了。有了ts-node,问题不大!

ts-node支持直接运行ts文件,就像用node直接运行js文件一样。它的原理是对node进行了一层封装,在require ts模块的时候,先调用tsc将ts文件编译成js文件,然后再用node执行。

安装ts-node: npm i ts-node -D

运行ts文件:npx ts-node script.ts

由于ts-node实际上是在运行阶段对于ts文件进行编译的,所以一般不在生产环境中直接使用ts-node,而是用tsc直接编译一遍,就不会有运行时的编译开销了。

(1)安装ts-loader: npm i ts-loader -D

(2)在webpack.config.js中加入相关的配置项
module.exports = {
  mode: "development",
  devtool: "inline-source-map", // 生成source-map
  entry: "./app.ts",
  output: {
    filename: "bundle.js"
  },
  resolve: {
    // 添加.ts,.tsx为可解析的文件后缀
    extensions: [".ts", ".tsx", ".js", ".jsx"]
  },
  module: {
    rules: [
      // 使用ts-loader解析.ts或者.tsx后缀的文件
      { test: /\.tsx?$/, loader: "ts-loader" }
    ]
  }
};

4 .配置eslint

经过上面的配置之后,如果编译报错会在命令行中有提示,并且在vscode中会对出错的代码进行标红。

如果我们想进一步对于代码风格进行规范化约束,需要配置eslint。实际上有专门针对typescript的lint工具ts-lint,但是现在并不推荐使用了,因为为了统一ts和js的开发体验,tslint正在逐步地合并到eslint上(https://medium.com/palantir/tslint-in-2019-1a144c2317a9)。

i. 安装eslint相关依赖

npm i eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin -D

其中:

eslint: js代码检测工具。

@typescript-eslint/parser: 将ts代码解析成ESTree,可以被eslint所识别。

@typescript-eslint/eslint-plugin: 提供了typescript相关的eslint规则列表。

ii. 配置.eslintrc.js文件

module.exports = {
  parser: '@typescript-eslint/parser', // 添加parser
  extends: [
    'eslint-config-imweb',
    'plugin:@typescript-eslint/recommended', // 引入@typescript-eslint/recommended规则列表
  ],
  plugins: ['@typescript-eslint'], // 添加插件
  rules: {
    'react/sort-comp': 0,
    'import/extensions': 0,
    'import/order': 0,
    'import/prefer-default-export': 0,
    'react/no-array-index-key': 1,
  },
};

iii. 进行了以上的步骤后,发现vscode中还是没有将不符合规则的代码标红。这里的原因是,vscode默认不会对.ts,.tsx后缀的文件进行eslint检查,需要配置一下。在vscode的setting.json文件中加入以下配置:

 "eslint.validate": [
      "javascript",
      "javascriptreact",
      {
          "language": "typescriptreact",
          "autoFix": true
      },
       {
          "language": "typescript",
          "autoFix": true
      }
  ]

5、 js项目迁移到ts

对于新的项目,自然不用说,直接开搞。但是对于旧项目,怎么迁移呢?

首先第一步还是要先接入typescript,如前文所述。

接下来就有两种选择:

i. 如果项目不大,或者下定决心并且有人力重构整个项目,那么可以将项目中的.js、.jsx文件的后缀改成.ts、tsx。不出意外,这时编辑器会疯狂报错,耐心地一个个去解决它们吧。

ii. 如果项目很庞大,无法一下子全部重构,实际上也不妨碍使用ts。

在tsconfig.json文件中配置allowJs: true就可以兼容js。

对于项目中的js文件,有三种处理方式。

(1)不做任何处理。

(2)对文件进行改动时候,顺手改成ts文件重构掉。

(3)给js文件附加.d.ts类型声明文件,特别是一些通用的函数或者组件,这样在ts文件中使用到这些函数或者组件时,编辑器会有只能提示,tsc也会根据声明文件中的类型进行校验。

在ts文件中引入npm安装的模块,可能会出现报错,这是因为tsc找不到该npm包中的类型定义文件,因为有些库是将类型定义文件和源码分离的。

有三种方式解决这一问题:

i. 如果该库在@types命名空间下已经有可用的类型定义文件,直接用npm安装即可,例如

npm i @types/react -D

ii. 如果该库在@types命名空间下没有可用的类型定义文件,可以自己写一个,然后给该库的作者提个PR。

iii. 本地创建一个全局的类型定义文件,例如global.d.ts。

declare module 'lib' {
  export const test: () => void;
}

然后在ts文件中就可以使用lib模块中的test方法了。

import { test } from 'lib';

参考文献: https://mp.weixin.qq.com/s/QgW7ScU_GNkIMW9dBFY3sg

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值