关于前端:TypeScript学习小结基础使用

4次阅读

共计 35050 个字符,预计需要花费 88 分钟才能阅读完成。

TypeScript 学习小结:根底应用

某册子买了两年多了,到最近才开始学习 TypeScript,迁延症的重大症状了;不过我还是坚信人做一件事是须要一个契机的。
学完之后整体感触是:TypeScript 在 JavaScript 的根底上提供了一套类型零碎,用以在编码时提供类型提醒,并利用类型推断对代码进行查看以及给出谬误提醒,以躲避一些可能潜在的执行 JavaScript 代码时会呈现的谬误;能够晋升团队合作效率,节俭沟通老本、代码浏览老本等等。
对 tsconfig.json 进行配置,咱们能够自定义敞开和开启的查看项以及 TS 的编译配置,比方是否对空值进行查看、编译的指标 JS 版本、源代码反对的 ES 版本等等。

基础知识分为以下几块:

  1. TypeScript 的类型零碎
  2. TypeScript 内置的类型
  3. TypeScript 类型定制

    1. 对象构造的类型:interface 和 class

      1. 根底应用
      2. 定义可选属性和只读属性
      3. 可索引类型
    2. 函数构造的类型

      1. 根底应用
      2. 可选参数、默认参数、残余参数
      3. 函数重载:用于更精准的类型提醒和推导
      4. 可调用类型
      5. 构造函数的注解
    3. 组合构造的类型
    4. 类型别名:type
  4. 利用类型零碎来放大类型范畴:类型守卫

    1. JS 操作符:in、typeof、instanceof
    2. 字面量类型守卫
    3. TS 关键字 is 定制类型守卫
  5. TypeScript 对类型的操作

    1. 类型变量(绝对于值变量)

      1. 泛型
      2. 泛型束缚:应用关键字extends
    2. 操作符

      1. 应用 TS 关键字 keyof 查问索引失去索引的一个联结类型
      2. 应用 [] 获取索引对应的类型
      3. 应用 TS 操作符 in 的映射操作
      4. 应用 TS 关键字 extends 的条件判断:三目运算
      5. 应用 TS 操作符 typeof 获取类型(构造)
      6. 应用 [keyof T] 过滤 never 类型的属性
      7. 应用 TS 关键字 infer 用于标识待推导和应用的类型(用在函数的参数类型和返回值类型)
      8. +- 操作符
    3. 工具类型:应用关键字 type 定义,应用泛型和操作符创立的相似函数性能的类型,通过泛型接管输出的类型,通过一系列操作失去并输入新的类型

前置筹备

1. 学习环境搭建

全局装置 TypeScript

$ yarn global add typescript
yarn global v1.22.10
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...
warning Your current version of Yarn is out of date. The latest version is "1.22.19", while you're on"1.22.10".
info To upgrade, run the following command:
$ curl --compressed -o- -L https://yarnpkg.com/install.sh | bash
success Installed "[email protected]" with binaries:
      - tsc
      - tsserver
✨  Done in 67.21s.

创立我的项目,初始化配置

## 创立我的项目目录
$ mkdir ts_learning && cd ts_learning 
## 创立 src 目录
mkdir src && touch src/index.ts
## 应用 npm 初始化
npm init
## 应用 tsc 初始化 ts 配置
tsc --init // npx typescript --init
$ tsc --init

Created a new tsconfig.json with:                                               
                                                                             TS 
  target: es2016
  module: commonjs
  strict: true
  esModuleInterop: true
  skipLibCheck: true
  forceConsistentCasingInFileNames: true


You can learn more at https://aka.ms/tsconfig

执行 tsc --init 后,目录下会新增一个 tsconfig.json 文件,这是 TypeScript 的配置文件,外面蕴含了官网初始化的一些配置以及正文:

{
  "compilerOptions": {
    /* Visit https://aka.ms/tsconfig to read more about this file */

    /* Projects */
    // "incremental": true,                              /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
    // "composite": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */
    // "tsBuildInfoFile": "./.tsbuildinfo",              /* Specify the path to .tsbuildinfo incremental compilation file. */
    // "disableSourceOfProjectReferenceRedirect": true,  /* Disable preferring source files instead of declaration files when referencing composite projects. */
    // "disableSolutionSearching": true,                 /* Opt a project out of multi-project reference checking when editing. */
    // "disableReferencedProjectLoad": true,             /* Reduce the number of projects loaded automatically by TypeScript. */

    /* Language and Environment */
    "target": "es2016",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
    // "lib": [],                                        /* Specify a set of bundled library declaration files that describe the target runtime environment. */
    // "jsx": "preserve",                                /* Specify what JSX code is generated. */
    // "experimentalDecorators": true,                   /* Enable experimental support for TC39 stage 2 draft decorators. */
    // "emitDecoratorMetadata": true,                    /* Emit design-type metadata for decorated declarations in source files. */
    // "jsxFactory": "",                                 /* Specify the JSX factory function used when targeting React JSX emit, e.g.'React.createElement'or'h'. */
    // "jsxFragmentFactory": "",                         /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g.'React.Fragment'or'Fragment'. */
    // "jsxImportSource": "",                            /* Specify module specifier used to import the JSX factory functions when using'jsx: react-jsx*'. */
    // "reactNamespace": "",                             /* Specify the object invoked for'createElement'. This only applies when targeting'react' JSX emit. */
    // "noLib": true,                                    /* Disable including any library files, including the default lib.d.ts. */
    // "useDefineForClassFields": true,                  /* Emit ECMAScript-standard-compliant class fields. */
    // "moduleDetection": "auto",                        /* Control what method is used to detect module-format JS files. */

    /* Modules */
    "module": "commonjs",                                /* Specify what module code is generated. */
    // "rootDir": "./",                                  /* Specify the root folder within your source files. */
    // "moduleResolution": "node",                       /* Specify how TypeScript looks up a file from a given module specifier. */
    // "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */
    // "paths": {},                                      /* Specify a set of entries that re-map imports to additional lookup locations. */
    // "rootDirs": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */
    // "typeRoots": [],                                  /* Specify multiple folders that act like './node_modules/@types'. */
    // "types": [],                                      /* Specify type package names to be included without being referenced in a source file. */
    // "allowUmdGlobalAccess": true,                     /* Allow accessing UMD globals from modules. */
    // "moduleSuffixes": [],                             /* List of file name suffixes to search when resolving a module. */
    // "resolveJsonModule": true,                        /* Enable importing .json files. */
    // "noResolve": true,                                /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */

    /* JavaScript Support */
    // "allowJs": true,                                  /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
    // "checkJs": true,                                  /* Enable error reporting in type-checked JavaScript files. */
    // "maxNodeModuleJsDepth": 1,                        /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */

    /* Emit */
    // "declaration": true,                              /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
    // "declarationMap": true,                           /* Create sourcemaps for d.ts files. */
    // "emitDeclarationOnly": true,                      /* Only output d.ts files and not JavaScript files. */
    // "sourceMap": true,                                /* Create source map files for emitted JavaScript files. */
    // "outFile": "./",                                  /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
    // "outDir": "./",                                   /* Specify an output folder for all emitted files. */
    // "removeComments": true,                           /* Disable emitting comments. */
    // "noEmit": true,                                   /* Disable emitting files from a compilation. */
    // "importHelpers": true,                            /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
    // "importsNotUsedAsValues": "remove",               /* Specify emit/checking behavior for imports that are only used for types. */
    // "downlevelIteration": true,                       /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
    // "sourceRoot": "",                                 /* Specify the root path for debuggers to find the reference source code. */
    // "mapRoot": "",                                    /* Specify the location where debugger should locate map files instead of generated locations. */
    // "inlineSourceMap": true,                          /* Include sourcemap files inside the emitted JavaScript. */
    // "inlineSources": true,                            /* Include source code in the sourcemaps inside the emitted JavaScript. */
    // "emitBOM": true,                                  /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
    // "newLine": "crlf",                                /* Set the newline character for emitting files. */
    // "stripInternal": true,                            /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
    // "noEmitHelpers": true,                            /* Disable generating custom helper functions like '__extends' in compiled output. */
    // "noEmitOnError": true,                            /* Disable emitting files if any type checking errors are reported. */
    // "preserveConstEnums": true,                       /* Disable erasing 'const enum' declarations in generated code. */
    // "declarationDir": "./",                           /* Specify the output directory for generated declaration files. */
    // "preserveValueImports": true,                     /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */

    /* Interop Constraints */
    // "isolatedModules": true,                          /* Ensure that each file can be safely transpiled without relying on other imports. */
    // "allowSyntheticDefaultImports": true,             /* Allow 'import x from y' when a module doesn't have a default export. */"esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables'allowSyntheticDefaultImports' for type compatibility. */
    // "preserveSymlinks": true,                         /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
    "forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */

    /* Type Checking */
    "strict": true,                                      /* Enable all strict type-checking options. */
    // "noImplicitAny": true,                            /* Enable error reporting for expressions and declarations with an implied 'any' type. */
    // "strictNullChecks": true,                         /* When type checking, take into account 'null' and 'undefined'. */
    // "strictFunctionTypes": true,                      /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
    // "strictBindCallApply": true,                      /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
    // "strictPropertyInitialization": true,             /* Check for class properties that are declared but not set in the constructor. */
    // "noImplicitThis": true,                           /* Enable error reporting when 'this' is given the type 'any'. */
    // "useUnknownInCatchVariables": true,               /* Default catch clause variables as 'unknown' instead of 'any'. */
    // "alwaysStrict": true,                             /* Ensure 'use strict' is always emitted. */
    // "noUnusedLocals": true,                           /* Enable error reporting when local variables aren't read. */
    // "noUnusedParameters": true,                       /* Raise an error when a function parameter isn't read. */
    // "exactOptionalPropertyTypes": true,               /* Interpret optional property types as written, rather than adding 'undefined'. */
    // "noImplicitReturns": true,                        /* Enable error reporting for codepaths that do not explicitly return in a function. */
    // "noFallthroughCasesInSwitch": true,               /* Enable error reporting for fallthrough cases in switch statements. */
    // "noUncheckedIndexedAccess": true,                 /* Add 'undefined' to a type when accessed using an index. */
    // "noImplicitOverride": true,                       /* Ensure overriding members in derived classes are marked with an override modifier. */
    // "noPropertyAccessFromIndexSignature": true,       /* Enforces using indexed accessors for keys declared using an indexed type. */
    // "allowUnusedLabels": true,                        /* Disable error reporting for unused labels. */
    // "allowUnreachableCode": true,                     /* Disable error reporting for unreachable code. */

    /* Completeness */
    // "skipDefaultLibCheck": true,                      /* Skip type checking .d.ts files that are included with TypeScript. */
    "skipLibCheck": true                                 /* Skip type checking all .d.ts files. */
  }
}

做一些自定义的配置:

{
  "compilerOptions": {
    /* Visit https://aka.ms/tsconfig to read more about this file */

    /* Projects */

    /* Language and Environment */
    "target": "es2016",                                  /* 指定 ECMAScript 指标版本。*/
    "lib": ["es6", "dom"],                               /* 指定一组打包的库申明文件,形容指标运行时环境。*/
    "experimentalDecorators": true,                      /* 启用对 TC39 第 2 阶段草案装璜器的实验性反对。*/

    /* Modules */
    "module": "commonjs",                                /* 指定生成哪类模块代码。*/
    "rootDir": "./src",                                  /* 指定源文件的根目录。*/
    "moduleResolution": "node",                          /* 指定 TypeScript 如何从一个给定的模块标识符中查找一个文件(解析策略)。*/
    "typeRoots": ["node_modules/@types"],                /* 指定多个文件夹,这些文件夹的作用相似于 './node_modules/@types'. */

    /* JavaScript Support */

    /* Emit */
    "declaration": true,                                 /* 从我的项目中的 TypeScript 和 JavaScript 文件生成.d.ts 文件. */
    "sourceMap": true,                                   /* 为生成的 JavaScript 文件创建 source map 文件. */
    "outDir": "./dist",                                  /* 为所有生成的文件指定一个输入文件夹. */
    "removeComments": true,                              /* 生成的文件中删除正文. */

    /* Interop Constraints */
    "allowSyntheticDefaultImports": true,                /* 当一个模块没有默认导出时,容许应用 "import x from y"。*/
    "esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
    "forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */

    /* Type Checking */
    "strict": true,                                      /* 启用所有严格的类型查看选项。*/
    "noImplicitAny": true,                               /* 对具备隐含 "any" 类型的表达式和申明启用错误报告。*/
     "alwaysStrict": true,                               /* 确保 "use strict" 总是被加到生成的代码中。*/
    "noImplicitReturns": true,                           /* 启用对函数中没有明确返回的代码门路的错误报告。*/

    /* Completeness */
    "skipLibCheck": true                                 /* Skip type checking all .d.ts files. */
  },
  "include": [                                           // 须要编译的 ts 文件一个 * 示意文件匹配 ** 示意疏忽文件的深度问题
    "./src/**/*.ts"
  ],
  "exclude": [
    "node_modules",
    "dist",
    "**/*.test.ts"
  ]
}

而后在 package.json 中退出 script 命令:

{
  "name": "learn_ts",
  "version": "1.0.0",
  "description": "","main":"src/index.ts","scripts": {"build":"tsc", // 编译"build:w":"tsc -w", // 监听文件,有变动就编译"test":"echo \"Error: no test specified\" && exit 1"},"author":"",
  "license": "ISC"
}

当咱们编写代码后,就能够通过 yarn build 命令进行编译,失去编译后的 JavaScript 代码。

yarn build

2. 感性认识

假如咱们应用 JavaScript 定义一个变量 a:

const a = 1;

如果改为应用 TypeScript 来编写,能够有以下两种形式:

// 1. 和 JavaScript 一样定义
const a = 1;

// 2. 定义变量时显式地指定类型,变量中存储的是什么类型的值
const a: number = 1;

乍一看,仿佛 TS 应用显式的形式指定类型的写法繁琐了一些,然而当咱们用到简单的类型,比方援用类型,益处就非常显著,首先在申明和赋值变量时,会提醒咱们类型外部是什么构造、外部的数据是什么类型,躲避一些低级谬误,在赋值有问题时会给出更精确的谬误提醒,这在开发大型项目和多人合作时非常有用;其次在应用变量时,也能够给出更多更精准的提醒,比方一个对象能够蕴含哪些属性,属性的类型,防止一些谬误的操作。

1. TypeScript 的类型零碎

TypeScript 的类型,与其余语言的类型不同,指的是合乎某种构造的数据分类,查看的是各种约束条件。

假如我定义了一个接口 A 和一个类 B,构造如下:

interface A {
  name: string,
  age: number
}

class B {
  name: string
  age: number
  
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

此时再定义几个变量:

const a: A = {
    name: 'lilei',
    age: 18
};
const b: A = new B('hanmeimei', 18);
const c: B = new B('lily', 18);

能够看到:

将变量 a 的类型指定为接口 A,咱们在定义时,TS 会提醒它的外部须要定义哪些属性;如果它的构造不合乎接口 A 的定义,就会给出谬误提醒。

将变量 b 的类型指定为接口 A,b 的值指向一个 B 的实例化对象,因为实例化对象的构造和接口 A 是统一的,所以也能够通过编译。

2. TypeScript 内置的类型

原始类型

  • 布尔类型:boolean

    const isLoading: boolean = false;
  • 数字类型:number

    TypeScript 中的二进制、十进制、十六进制等数字都能够用 number 类型示意。

    const decLiteral: number = 6; // 十进制
    const hexLiteral: number = 0xf00d; // 十六进制
    const binaryLiteral: number = 0b1010; // 二进制
    const octalLiteral: number = 0o744; // 八进制
  • 字符串类型:string

    const book: string = 'Hello, TypeScript';
  • 空值:void

    示意没有任何类型。

    当一个函数没有返回值时,通常会见到其返回值类型是 void:

    function warnUser() {alert("This is a warning message.");
    } // ts 推导出的类型:function warnUser(): void

    实际上只有 nullundefined能够赋值给void(与 strictNullChecks 设置无关):

    const aVoid: void = undefined;
  • null 和 undefined

    在 TypeScript 中,undefined 和 null 两者各自有本人的类型别离是 undefined 和 null,和 void 类似,它们自身的类型用途不是很大:

    let x: undefined = undefined;
    let y: null = null;
    
    // strictNullChecks: true
    const u2v: void = undefined; // ok
    const n2v: void = null; // TS2322: Type 'null' is not assignable to type 'void'.
    let n2u: undefined = null; // TS2322: Type 'null' is not assignable to type 'undefined'.
    let u2n: null = undefined; // TS2322: Type 'undefined' is not assignable to type 'null'.
    let n2num: number = null; // TS2322: Type 'null' is not assignable to type 'number'.
    let u2num: number = undefined; // TS2322: Type 'undefined' is not assignable to type 'number'.
    
    // strictNullChecks: false
    const u2v: void = undefined; // ok
    const n2v: void = null; // ok
    let n2u: undefined = null; // ok
    let u2n: null = undefined; // ok
    let n2num: number = null; // ok
    let u2num: number = undefined; // ok

    默认状况下,null 和 undefined 是所有类型的子类型,就是说你能够把 null 和 undefined 赋值给 number 类型的变量。

    但在正式我的项目中个别都开启 strictNullChecks 检测,即 null 和 undefined 只能赋值给 any 和各自的类型(undefined 还能赋值给 void),能够躲避十分多的问题。

  • symbol

    注:应用 symbol 时,必须增加 es6 的编译辅助库:

    // tsconfig.json
    {"lib": ["es6", ...]
    }

    Symbol 是在 ES2015 之后成为新的原始类型,它通过 Symbol 函数创立:

    const sym1 = Symbol('key1');
    const sym2 = Symbol('key2'); // 编译出的.d.ts => declare const sym2: unique symbol;

    Symbol 的值是惟一不变的。

    Symbol('key1') === Symbol('key1') // false
  • 大整数类型:bigint

    BigInt类型在 TypeScript3.2 版本被内置,应用 BigInt 能够平安地存储和操作大整数,即便这个数曾经超出了 JavaScript 中 Number 所能示意的平安整数范畴。

    注:应用 BigInt 时,必须增加 ESNext 的编译辅助库

    // tsconfig.json
    {"lib": ["ESNext", ...]
    }

    在 JavaScript 中采纳双精度浮点数,这导致精度无限,比方 Number.MAX_SAFE_INTEGER 示意能够平安递增的最大可能整数,即2*53-1

    const max = Number.MAX_SAFE_INTEGER;
    
    const max1 = max + 1;
    const max2 = max + 2;
    
    max1 === max2; // true

    呈现上述后果的起因就是超过了精度范畴。BigInt 就能够解决此类问题:

    // 此处是 JavaScript 代码,不是 TypeScript
    const max = BigInt(Number.MAX_SAFE_INTEGER);
    
    const max1 = max + 1n;
    const max2 = max + 2n; // TS2737: BigInt literals are not available when targeting lower than ES2020. 须要将 tsconfig.json 中的 target 改为 ES2020 才反对此种写法
    
    max1 === max2; // false

    注:咱们须要用 BigInt(number) 把 Number 转化为 BigInt,同时如果类型是 BigInt 的字面量,数字前面须要加n

    在 TypeScript 中,number类型尽管和 BigInt 都用于示意数字,但实际上两者类型是不同的:

    declare let foo: number;
    declare let bar: bigint;
    
    foo = bar; // TS2322: Type 'bigint' is not assignable to type 'number'.
    bar = foo; // TS2322: Type 'number' is not assignable to type 'bigint'.

顶级类型

也称为通用类型,所有类型都是它的子类型

  • any
  • unknown

any

为那些在编程阶段还不分明类型的变量指定一个类型,这些值可能来自于动静的内容,比方来自用户输出或第三方代码库。

这些状况下,咱们不心愿类型查看器对这些值进行查看而是间接让它们通过编译阶段的查看。此时能够应用 any 类型来标记这些变量:

let notSure: any = 4;
notSure = 'maybe a string instead';

留神:any类型是多人合作我的项目的大忌,很可能把 TypeScript 变成 AnyScript,除非不得已,不应首先思考应用此类型。

unknown

unknown是 TypeScript3.0 引入的新类型,是 any 类型对应的平安类型。

他们的共同点是,能够被赋值任何类型的值。

// any
let notSure: any = 4;
notSure = 'maybe a string instead';
notSure = Symbol('anytype');
notSure = {};
notSure = [];
notSure = true;
// unknown
let notKnown: unknown = 4;
notKnown = 'maybe a string instead';
notKnown = Symbol('anytype');
notKnown = {};
notKnown = [];
notKnown = true;

两者的次要区别是,unknown类型会更加严格:在对 unknown 类型的值执行大多数操作之前,必须进行某种模式的查看,而对 any 类型的值执行操作前,不用做任何查看。

// any
let anyValue: any;
anyValue.foo.bar;
anyValue();
new anyValue();
anyValue[0][1];
// unknown
let unknownValue: unknown;
// unknownValue.foo.bar; // TS2571: Object is of type 'unknown'.
// unknownValue(); // TS2571: Object is of type 'unknown'.
// new unknownValue(); // TS2571: Object is of type 'unknown'.
// unknownValue[0][1]; // TS2571: Object is of type 'unknown'.

能够看到,unknown类型被确定是某个类型之前,它不能被进行任何操作比方实例化、getter、函数调用等等,而 any 是能够的。这也是 unknownany更平安的起因。

any因为过于灵便,导致与 JavaScript 没有太多区别,很容易产生低级谬误,很多场景下能够抉择 unknown 作为更好的替代品。

什么状况下能够对 unknown 执行操作呢?那就是放大其类型范畴之后,比方:

function getValue(value: unknown): string {if (value instanceof Date) { // 把 value 的类型范畴放大至 Date 实例,所以能够调用 Date 的实例办法
        return value.toISOString();}

    return String(value);
}

底部类型

  • never

never类型示意的是那些永不存在的值的类型,是任何类型的子类型,能够赋值给任何类型(?);然而,没有类型能够赋值给 never 类型(除了 never 类型自身之外)。

const never1: never = testNever();
const num2: number = never1;

function testNever(): never {throw 'error';}

let neverValue: never = 1; // TS2322: Type 'number' is not assignable to type 'never'.
let anyValue: any = 1;
neverValue = anyValue; // TS2322: Type 'any' is not assignable to type 'never'.

let neverArray: never[] = [];

never 类型常见的两种应用场景:

// 1. 抛出异样的函数永远不会有返回值
function error(message: string): never {throw new Error(message);
}
let n3: number = error('123'); // ok
// 2. 空数组,而且永远是空的
const empty: never[] = [];
// empty.push(1); // TS2345: Argument of type 'number' is not assignable to parameter of type 'never'.

数组、元组等

数组

数组有两种定义形式:

// 1. 应用泛型
const list: Array<number> = [1, 2, 3];
// 2. 在类型前面加上 `[]`
const table: number[] = [1, 2, 3];
// list.push('123'); // TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

元组

元组与数组相似,示意一个已知长度和元素类型的数组,各元素的类型不用雷同。

元组数据如果与申明的元素类型不统一、或者长度不统一,就会报错,必须严格跟当时申明的类型和程序保持一致。

let tt: [string, number];
// tt = ['hello', 10, false]; // TS2322: Type '[string, number, boolean]' is not assignable to type '[string, number]'. Source has 3 element(s) but target allows only 2.
// tt = [10, 'hello']; // TS2322: Type 'number' is not assignable to type 'string'. Type 'string' is not assignable to type 'number'.
// tt = ['hello']; // TS2322: Type '[string]' is not assignable to type '[string, number]'. Source has 1 element(s) but target requires 2.

能够把元组看成严格版的数组,如 [string, number] 能够看成是:

interface Tuple extends Array<string | number> {
  0: string;
  1: number;
  length: 2;
}

元组继承于数组,但比数组领有更严格的类型查看。

此外,还存在一个元组越界问题,如 TypeScript 容许向元组中应用 push 办法插入新元素,但拜访越界的元素时会报错:

tt = ['hello', 10];
tt.push(2);
console.log(tt);
// console.log(tt[2]); // TS2493: Tuple type '[string, number]' of length '2' has no element at index '2'.

非原始类型 oject

  • object

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

enum Direction {Center = 1}
let oo: object;

oo = Direction;
oo = [1];
oo = tt;
oo = {};

能够看到,一般对象、枚举、数组、元组都是 object 类型。

枚举类型

假如变量的类型只会是某几个常量之一,此时就能够应用枚举类型。枚举成员的值能够是数字或字符串。

enum Direction {
  UP,
  DOWN,
  LEFT,
  RIGHT
}
// 以上定义相当于
enum Direction {
  UP = 0,
  DOWN = 1,
  LEFT = 2,
  RIGHT = 3
}

因为枚举成员对应的值默认是数字类型,而且默认从 0 开始顺次累加。

// 编译后的 JavaScript
var Directions;
(function (Directions) {Directions[Directions["Up"] = 0] = "Up";
    Directions[Directions["Down"] = 1] = "Down";
    Directions[Directions["Left"] = 2] = "Left";
    Directions[Directions["Right"] = 3] = "Right";
})(Directions || (Directions = {}));

当咱们把某个成员赋值数字后,前面也会依据前一个成员的值进行累加。

enum Directions {
    Up = 10,
    Down = 10,
    Left,
    Right
}

console.log(Directions.Up, Directions.Down, Directions.Left, Directions.Right); // 10 10 11 12

枚举类型成员的值也能够是字符串类型。

enum Directionss {
    Up = 'UP',
    Down = 'DOWN',
    Left = 'LEFT',
    Right = 'RIGHT'
}

console.log(Directionss.Up, Directionss['Right']); // UP RIGHT

// 非数字类型的枚举成员,必须给成员赋初始值
enum Directionss {
    Up = 'UP',
    Down = 'DOWN',
    Left = 'LEFT',
    Right // TS1061: Enum member must have initializer.
}

依据下面编译后的 JavaScript 的代码,能够看出,咱们能够通过枚举值获取枚举成员的名字,即反向映射。

enum Directions {
    Up = 10,
    Down = 10,
    Left,
    Right
}

console.log(Directions[10]); // Down

能够看出枚举类型实质上是一个 JavaScript 对象,而因为其非凡的结构,导致其领有正反双向同时映射的个性。

并且咱们即便离开申明枚举类型,也会主动合并到一个对象上,比方:

enum Directions {
    Up = 10,
    Down = 10,
    Left,
    Right
}
enum Directions {Center}
// 编译后的 JavaScript 代码
var Directions;
(function (Directions) {Directions[Directions["Up"] = 10] = "Up";
    Directions[Directions["Down"] = 10] = "Down";
    Directions[Directions["Left"] = 11] = "Left";
    Directions[Directions["Right"] = 12] = "Right";
})(Directions || (Directions = {}));
(function (Directions) {Directions[Directions["Center"] = 0] = "Center";
})(Directions || (Directions = {}));

可见 TypeScript 中反复定义枚举的代码并不抵触。

3. TypeScript 类型定制

定义对象构造的类型:interface 和 class

当咱们想要定义一个更具体的对象构造的类型时,能够应用 interface 或 class 关键字,两者看上去有点类似,但应用上也有不同。

  • class 是 JavaScript 的关键字,是 function 的一种语法糖,能够通过 new 操作来创立实例,能够用 instanceof 操作来判断类型;而 interface 通常被看作为你的代码或第三方代码定义的一种契约,能够用于与后端数据对接的标准。
  • interface 中的函数只有申明,没有具体实现;class 中能够有函数具体实现的代码。

根底应用

// interface
interface User {
    name: string
    age: number
    gender: boolean
    say: (words: string) => string
}

// class
class Vehicle {public startRun(): void {console.log('starting ...');
    }
}

const car = new Vehicle();
car.startRun(); // starting ...

定义可选属性和只读属性

  1. 假如对象的某个属性能够不定义,即存在可选的属性,能够通过 ? 来定义这个属性:
interface User {
    name: string
    age?: number
    gender: boolean
    say: (words: string) => string
}

上述代码示意申明类型为 User 的数据,能够不定义 age 属性,比方:

const u1: User = {
  name: 'lilei',
  gender: true,
  say: (words: string) => {return `message: ${words}`;
  }
}
  1. 假如对象的某个属性不可写,即存在只读属性,能够通过 readonly 来定义这个属性:
interface User {
    name: string
    age?: number
    readonly gender: boolean
    say: (words: string) => string
}

u1.name = 'hanmeimei'; // ok
u1.gender = false; // TS2540: Cannot assign to 'gender' because it is a read-only property.

上述代码中定义 gender 为只读属性,给对象的非只读属性 name 从新赋值没有问题,然而给 gender 从新赋值就会报错,提醒只读属性不能赋值。

可索引类型

假如存在一个对象,所有属性对应的值都是一样的类型,比方是 string,就能够用可索引类型示意。

可索引类型具备一个索引签名,它形容了对象索引的类型,还有相应的索引返回值类型。比方:

interface Email {[name: string]: string
}

上述代码表明 Email 类型的变量,领有 0 个或多个 string 类型的索引并且属性值也是 string 类型。应用:

const email1: Email = {};
const email2: Email = {netease: '[email protected]' };
const email3: Email = {netease: '[email protected]', qq: '[email protected]' };

在理论利用中,能够用于与接口返回数据的约定,比方除了确定存在的属性,也存在一些不确定的数据。比方:

interface Item {
  name: string
  id: number
  status: number
  [key: string]: any
}

上述代码约定了返回的数据对象中肯定有 nameidstatus属性,也有一些不确定的数据。

函数构造的类型

在 JavaScript 中函数的类型就是指Function;而在 TS 中,函数还定义了更具体的构造的类型。

函数的类型具体蕴含了参数个数、各个参数的类型以及返回值类型。

根底应用

// 1. 作为对象类型的属性
interface User {
    name: string
    age: number
    gender: boolean
    say: (words: string) => string 
}

// 2. 独自定义
interface Say { // 应用 interface 定义
    (words: string): string
}
// 3. 或者定义别名(相当于定义了一个简写)type Say = (words: string) => string
// 再应用
interface User {
    name: string
    age: number
    gender: boolean
    say: Say
}

可选参数、默认参数、残余参数

  1. 定义可选参数与定义可选属性一样,都是应用?

    const add: Add = (a:number, b?: number) => a + (b || 0);

    参数 bnumberundefined 两种可能。

  2. 定义默认参数的形式与 JavaScript 一样

    const add: Add = (a:number, b:number = 0) => a + b;
  3. 定义残余参数的形式与 JavaScript 一样

    // 残余参数 `rest` 是一个由 number 组成的数组,在本函数中用 reduce 进行了累加求和。const add2 = (a: number, ...rest: number[]) => rest.reduce((a, b) => a+ b, a);

函数重载:用于更精准的类型提醒和推导

假如定义了一个函数assigned,外表上看能够接管 1~4 个参数,但理论接管 3 个参数是有问题的。

function assigned(a: number, b?: number, c?: number, d?: any) {if (b === undefined && c === undefined && d === undefined) {b = c = d = a;} else if (c === undefined && d === undefined) {
        c = a;
        d = b;
    }
    return {
        top: a,
        right: b,
        bottom: c,
        left: d
    };
}

assigned(1, 2, 3);

但此时 TS 并不会给出谬误提醒,在编写代码中调用 assigned 函数时也不会给出精确的提醒,须要传几个参数,这就减少了类型的不平安。

此时函数重载就派上了用场。函数重载就是指用同样的函数名 申明 别离对应不同的状况。比方:

interface Position {
    top: number,
    right?: number,
    bottom?: number,
    left?: number
}

function assigned(all: number): Position;
function assigned(topAndBottom: number, leftAndRight: number): Position;
function assigned(top: number, right: number, bottom: number, left: number): Position;

function assigned(a: number, b?: number, c?: number, d?: any) {if (b === undefined && c === undefined && d === undefined) {b = c = d = a;} else if (c === undefined && d === undefined) {
        c = a;
        d = b;
    }
    return {
        top: a,
        right: b,
        bottom: c,
        left: d
    };
}

assigned(1);
assigned(1, 2);
assigned(1, 2, 3); // TS2575: No overload expects 3 arguments, but overloads do exist that expect either 2 or 4 arguments.
assigned(1, 2, 3, 4);

上述代码中别离申明了参数为 1、2、4 的状况。能够看到,只有传入 3 个参数的状况下报错了。

能够了解成重载是定义了函数的几种不同的状态,如果不满足下面重载定义的其中一种状态,就会报错!另外函数重载的入参类型和返回值类型须要和函数实现保持一致,否则也会报错!

可调用类型

可调用类型就是后面所说的独自定义一个函数类型

interface Say { // 应用 interface 定义
    (words: string): string
}

此时表明 Say 是一个可调用类型,申明一个变量类型为 Say 时,阐明这个变量是能够执行调用操作的。如下:

declare const say: Say;
say('1');

构造函数的注解

假如咱们要对下面的变量 say 进行实例化操作,会报错,因为没有对应的构造函数签名。

new say('1'); // TS7009: 'new' expression, whose target lacks a construct signature, implicitly has an 'any' type.

此时能够加上 new 来示意此接口能够实例化。比方:

interface Say { // 应用 interface 定义
    (words: string): string
    new (): string}

这样就能够执行结构调用了。

new say();

组合构造的类型

在理论我的项目中,可能会呈现简单类型的应用,咱们能够在现有类型的根底上,发明出更简单的类型来满足理论场景。

  • &造成穿插类型

    应用 & 能够造成穿插类型,& 在逻辑中就是且的意思,示意两侧的条件必须同时满足。

    比方将两个对象构造的类型穿插,就是将两个类型中雷同的属性合并为一个,再与残余的属性一起造成新的类型,这样就是两侧的定义的构造都满足了。比方:

    interface I1 {
      name: string
      age: number
    }
    interface I2 {
      name: string
      status: number
    }
    type T1 = I1 & I2; 
    
    const t1: T1 = {
        name: 'lilei',
        age: 12,
        status: 1
    }; // t1 必须同时满足 I1 和 I2 的束缚
    
    // 如果 I2 的定义中也定义了 age 字段,类型为 string
    interface I2 {
      name: string
      age: string
      status: number
    }
    // 那么 I1&I2 穿插的后果中,age 字段就会变成 never,因为 age 为 number & string,一个数据不可能既是 string 类型又是 number 类型,就变成了不存在的 never 类型数据
    // I1 & I2 失去的后果
    {
      name: string
      age: number & string -> never
      status: number
    }
  • |造成联结类型

    应用 | 能够造成联结类型,| 在逻辑中就是或的意思,示意两侧的条件只有满足其一。

    比方将两个类型进行联合操作,造成新的类型,就示意两侧的构造满足其中之一即可。比方:

    type T2 = number | string;
    const t21: T2 = 1; // ok
    const t22: T2 = '1'; // ok
    const t23: T2 = true;  // TS2322: Type 'boolean' is not assignable to type 'T2'.

    内置类型中的枚举类型能够看作是申明了一个联结类型。

    enum Direction {
        UP,
        DOWN,
        LEFT,
        RIGHT
    }
    //
    type T3 = Direction.UP | Direction.DOWN | Direction.LEFT | Direction.RIGHT;
    
    let enu1: Direction = Direction.UP;
    let enu2: T3 = Direction.UP;

类型别名:type

应用类型别名相当于给类型指定了一个简写。比方:

type AddFunction = (a: number, b: number) => number;
const add: AddFunction = (a: number, b: number) => a + b;
// 👆🏻的写法相当于
const add: (a: number, b: number) => number = (a: number, b: number) => a + b;

也就是说关键字 type 并没有申明一个新的类型,只是给类型设置了一个简写,使代码更简洁可读性更高。

typeinterface 在应用有一些相似之处,比方都能够用于对象构造类型的定义,也都能够被 implement:

type TUser = {
    name: string
    age: number
}
interface IUser {
    name: string
    age: number
}
const tUser: TUser = {
    name: 'a',
    age: 12
};
const iUser: IUser = {
    name: 'b',
    age: 13
};
// 类型别名能够被 implements
class AA implements TUser {
    name = 'aa'
    age = 12
}
// interface 也能够被 implements
class BB implements IUser {
    name = 'bb'
    age = 13
}

但还有很多不同的用法:

  1. interface 只用于定义对象构造的类型,interface 申明时能够应用 extends 关键字来继承其余 interface、type 和 class。比方:

    // interface 申明 extends 类型别名
    interface IUser2 extends TUser {// ...}
    // interface 申明 extends 其余 interface
    interface IUser3 extends IUser {// ...}
    // interface 申明 extends class
    interface IUser4 extends AA {// ...}
    // interface 申明合并接口
    interface IUser5 extends IUser2, IUser3 {// ...}
  2. type 的申明中不能用 extends 关键字,但 type 除了申明对象构造的类型,还能够对类型进行组合操作,比方穿插操作、联合操作,等等,还有前面会提到的与泛型还有其余类型操作符,造成简单的类型。相当于施展相似工具函数的作用,应用范畴更广。

    type Action = {
      type: 'create'
      payload: {
        name: string
        age: number
      }
    } | {
      type: 'delete',
      payload: {id: number}
    }
    type Direction = "UP" | "DOWN" | "LEFT" | "RIGHT"

4. 利用类型零碎来放大类型范畴:类型守卫

假如某个变量可能是多种类型之一,如果要对它进行某种类型的特定操作,就须要放大类型范畴,来保障类型的安全性,比方某个函数的参数,可能是 number 类型或 string 类型,如果要调用 string 类型的办法,就必须在参数为 string 的状况下才能够进行操作,达到缩小报错的目标,缩小编码中可能存在的潜在谬误。

能够通过以下几种形式来放大类型范畴:

JS 操作符:in、typeof、instanceof

instanceof 类型爱护是通过构造函数来细化类型的一种形式:

function getSomething(arg: Date | Date[]) {
    // 细化类型为 Date
    if(arg instanceof Date) {// arg.pop(); // TS2339: Property 'pop' does not exist on type 'Date'.
        console.log(arg.getDate());
    }
    // 细化类型为 Array
    if(arg instanceof Array) {// arg.getDate(); // TS2339: Property 'getDate' does not exist on type 'Date[]'.
        console.log(arg.pop());
    }
}

getSomething(new Date()); // 2
getSomething([new Date()]); // 2022-11-02T03:37:45.874Z

typeof 也是差不多的形式,通过判断数据的 JavaScript 根本类型来放大范畴:

function getMessage(arg: number | string) {if (typeof arg === "number") {// arg.toUpperCase(); // TS2339: Property 'toUpperCase' does not exist on type 'number'.
        console.log(arg + 1);
    }
    if (typeof arg === "string") {console.log(arg.toUpperCase());
    }
}
getMessage(33); // 34
getMessage('excellent'); // EXCELLENT

相似的,x in y示意 x 属性在 y 中存在。

function getSomething2(arg: Date | Date[]) {if ('getDate' in arg) {// arg.pop(); // TS2339: Property 'pop' does not exist on type 'Date'.
        arg.getDate();}
    if ('length' in arg) {// arg.getDate(); // TS2339: Property 'getDate' does not exist on type 'Date[]'.
        arg.pop();}
}

字面量类型守卫

假如咱们定义一个变量 v1:

const v1 = "v1"; // const v1: "v1"
let v2 = "v2"; // let v2: string

此时咱们没有指定类型,但 TS 会推导出 v1 的类型是 "v1",尽管看上去有点奇怪,但这就是 TS 类型零碎中的字面量类型。比方{}[]""123 都是字面量,所以字面量都是单例类型,应用 const 申明一个原始类型就会失去一个字面量类型。比方对象类型就不能失去字面量类型:

const v3 = {
    name: '1',
    address: {
        city: 'hz',
        country: 'China'
    }
}; // const v3: {name: string, address: {city: string, country: string}}

这在联结类型中十分实用,比方两个对象构造的类型造成一个联结类型,如果通过 in 操作符来判断其中某个属性从而辨别两个类型,存在类型危险,比方前期这个属性变动不存在了,此时就能够增加一个属性,指定为字面量类型,从而能够分辨两个类型,就造成了可辨识联结类型。比方:

type Error1 = {
    kind: 'networkError', // 字面量类型
    networkStatus: string
}
type Error2 = {
    kind: 'serverError', // 字面量类型
    serverStatus: string
}

function doHandle(error: Error1 | Error2) {if (error.kind === 'networkError') {// console.log(error.serverStatus); // TS2339: Property 'serverStatus' does not exist on type 'Error1'.
        console.log(error.networkStatus);
    }
    if (error.kind === 'serverError') {// console.log(error.networkStatus); // TS2339: Property 'networkStatus' does not exist on type 'Error2'.
        console.log(error.serverStatus);
    }
}

doHandle({kind: 'serverError', serverStatus: 'server error...'});

能够看出 TypeScript 依据字面量的比拟操作进行了类型推断

TS 关键字 is 定制类型守卫

假如咱们要判断一个数据的类型,除了合乎个性的构造以外,还须要满足一些额定的条件,就能够应用关键字 is 来定制咱们的类型守卫,比方 Student 和 Person 的构造一样,然而 Student 的范畴更小一些,比如说年龄范畴在 12 至 15 岁之间,就能够编写如下代码来进行辨别:

interface Person9 {
    name: string,
    age?: number,
}

interface Student9 extends Person9 {}

function checkValidStudent(p: Person9): p is Student9 {return p.age !== undefined && p.age >= 12 && p.age <= 15;}

const p1: Person9 = {
    name: 'lilei',
    age: 18
}

const p2: Person9 = {
    name: 'hanmeimei',
    age: 15
}

function checkPerson(p: Person9) {if (checkValidStudent(p)) {console.log(`${p.name} is a Student`);
    } else {console.log(Object.prototype.valueOf.call(p));
    }
}

checkPerson(p1);
checkPerson(p2);

上述代码运行的后果是:

{name: 'lilei', age: 18}
hanmeimei is a Student

if 的条件里,p 提醒的类型是 Person9,在 if 判断外部,p 提醒的类型就变成 Student9 了。

在 else 判断里,p 提醒的类型是 never,不懂为啥?兴许是因为 Person9 和 Student9 的构造一样

又比方除了函数类型必须得通过 typeof 验证,从而保障类型的平安:

function isFunction(arg: any): arg is Function {return typeof arg === "function";}

尽管函数编译后显示返回值是布尔类型,但在 TS 的类型零碎中给出的是不同的提醒,间接定义返回类型是布尔类型 boolean,就达不到判断类型的目标。

5. TypeScript 对类型的操作

类型变量(绝对于值变量)

  • 泛型

    泛型就相当于对类型的一种指代,相似于变量是对值的一种指代;所以泛型就是类型变量。

    当咱们在编码时还不确定某些数据的类型,然而须要应用类型,或者依据不同类型做不同的解决,就能够用到泛型,通常泛型应用 T、V、U 等大写字母,通过 <> 来申明。比方:

    // 利用于函数
    function print<T>(a: T) {console.log(a);
    }
    // 假如曾经明确传入的参数是数组,只是不晓得元素的类型未知,则能够这样定义
    function getArrayLength<T>(arg: Array<T>): Array<T> {console.log(arg.length);
    
        return arg;
    }
    // 利用于类
    class Stack<T> {private arr: T[] = [];
        public push(n: T) {this.arr.push(n);
        }
        public pop() {this.arr.pop();
        }
    }
    // 利用于接口
    interface Tree<T> {
        name: string,
        node: T,
        child: Tree<T>
    }
    interface ReturnItemFn<T> {(para: T): T
    }
    // 利用于类型别名
    type T5<T> = {
        name: string,
        node: T,
        child: T5<T>
    }

    在应用时能够显示的指定泛型的类型,TS 也能够通过数据理论值的类型进行推导。

    print(1); // print 推导出 function print<number>(a: number): void
    print<string>('1');
    const returnItem: ReturnItemFn<number> = para => para; // 实现一个接口须要明确传入泛型的类型
  • 泛型束缚:应用关键字extends

    尽管编码时不确定操作的数据类型,但通常实际操作的数据会在肯定的范畴内,此时咱们就能够通过 extends 关键字来限度所指代的类型。比方给下面的 print 的泛型增加束缚:

    function print<T extends boolean | number>(a: T) {console.log(a);
    }
    // 此时就会报错
    print<string>('1'); // TS2344: Type 'string' does not satisfy the constraint 'number | boolean'.

    T 限度为 string 和 number 造成的联结类型,即 a 的值的类型必须满足这个束缚。

    乍一看仿佛没什么用途,为什么不间接定义一个类型 CC,而后将 a 指定为 CC 类型呢?比方:

    type CC = boolean | number
    function printf(a: CC) {console.log(a);
    }

    在这样简略的代码中,的确显示不出用处。

    如果有一个需要,要设计一个函数,接管两个参数,一个参数为对象,另一个参数为对象上的属性,通过这两个参数返回这个属性的值。如果应用以下代码:

    function getObjProperty(obj: object, key: string) {return obj[key];
    }

    会失去 TypeScript 的谬误提醒:

    TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type'{}'.   No index signature with a parameter of type'string'was found on type'{}'.
    元素隐含有一个 'any' 类型,因为 'string' 类型的表达式不能被用于索引 '{}' 类型。在类型 '{}' 上没有找到带有 'string' 类型参数的索引签名。

    因为 obj 默认状况下是 {}key 是无奈在下面取到任何值的。但咱们接管的对象是各种各样的,咱们须要一个泛型来示意传入的对象类型,比方T extends object

    function getObjProperty<T extends object>(obj: T, key: string) {return obj[key];
    }

    但此时仍不能解决问题,因为第二个参数 key 是不是存在于 obj 上是无奈确定的,因而咱们须要对 key 也进行束缚,把它束缚为只存在于 obj 属性的类型。这里能够借助获取索引的类型进行实现 <U extends keyof T>,用索引类型查问操作符keyof T 把传入的对象的属性取出生成一个联结类型,这样泛型 U 就被束缚在这个联结类型中,这样一来函数就被残缺定义了:

    function getObjProperty<T extends object, U extends keyof T>(obj: T, key: U) {return obj[key];
    }

    假如传入以下对象:

    const person = {
        id: 1,
        name: 'lily'
    }

    当咱们向 getObjProperty 传入第一个参数为 person 时,第二个参数 key 的类型就被推导束缚为一个联结类型"name" | "id",只可能是这两者之一,因而能取得良好的类型提醒:

    function getObjProperty<{
        id: number;
        name: string;
    }, "id" | "name">(obj: {id: number, name: string},     
        key: "id" | "name"): string | number

操作符

  • 应用 TS 关键字 keyof 查问索引失去索引的一个联结类型

    class Logo {
        public src: string = 'https://www.google.com.hk/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png'
        public alt: string = 'google'
        public width: number = 500
    }
    
    type propNames = keyof Logo;

    TS 的推导:

    type propNames
    Alias for:
    keyof Logo
    Initial type:
    "src" | "alt" | "width"
  • 应用 [] 获取索引对应的类型

    type propsType = Logo[propNames];

    TS 的推导:

    type propsType
    Alias for:
    Logo[propNames]
    Initial type:
    string | number

    如上 propNames 是一个联结类型,[]操作也是将联结类型中的每个类型去取一遍,再失去一个联结类型。

  • 应用 TS 操作符 in 的映射操作

    假如此时有一个 User 接口,当初有一个需要是把 User 接口中的成员全副变成可选的,要怎么做呢?

    interface User {
        username: string,
        id: number,
        token: string,
        avatar: string,
        role: string
    }

    一个个 : 前加上 ? 会不会太繁琐,有没有更便捷的办法?

    此时映射操作就派上用场了,语法是[K in Keys]

    • K:属性名变量,顺次绑定到每个属性上,对应每个属性名
    • Keys:字符串字面量形成的联结类型,示意一组属性名。

      此处能够应用 keyof 操作符,假如传入的类型是泛型 T,失去keyof T,即传入类型T 的属性名的联结类型,也就是 Keys。

    keyof T 的属性名称一一映射进去就是[K in keyof T]

    要把所有的属性成员变为可选类型,那么须要用 T[K] 取出相应的属性的类型,而后从新生成一个可选的新类型{[K in keyof T]?: T[K] }

    应用类型别名示意:

    type partial<T> = {[K in keyof T]?: T[K]
    }
    
    // 测试
    type partialUser = partial<User>;

    能够看到所有属性都变成了可选类型:

    type partialUser
    Alias for:
    partial<User>
    Initial type:
    {username?: string, id?: number, token?: string, avatar?: string, role?: string}

    应用 in 操作符就相当于数组的 map 操作,对一组联结类型进行遍历的映射操作,失去的后果造成新的类型。

  • 应用 TS 关键字 extends 的条件判断:三目运算

    在 TS 的类型中来能够通过 extends 操作符来实现三目运算,通常在泛型中应用。比方:

    T extends U ? X : Y

    能够这样了解:若 T 可能赋值给U,那么类型是X,否则是Y

    假如咱们申明一个函数 f,它的参数接管一个布尔类型,当布尔类型为true 时返回 string 类型,否则返回 number 类型,代码如下:

    declare function f1<T extends boolean>(arg: T): T extends true ? string : number;
    
    f1(Math.random() < 0.5); // function f1<boolean>(arg: boolean): string | number
    f1(true); // function f1<true>(arg: true): string
    f1(false); // function f1<false>(arg: false): number

    能够看到当参数的类型不同,会推导出不同的返回值类型。

    当类型零碎给出短缺的条件,就能依据条件推断出类型后果。

    extends 前面跟的是一个联结类型,失去的后果也是一个联结类型,相当于联结类型中的每个类型进行了一次判断,最初组合再一起。比方:

    type Diff<T, U> = T extends U ? never : T;
    type R = Diff<"a"| "b" | "c" | "d", "a" | "c" | "f">; // "b" | "d"

    遍历联结类型 "a"| "b" | "c" | "d" 中每个类型做一次判断,是否能赋值给"a" | "c" | "f",最初失去"b"|"d",因为依据条件,能赋值的类型在三目运算后失去的为never

    条件判断和映射操作还能组合做更简单的操作。

  • 应用 [keyof T] 取出对象属性的类型,造成联结类型

    可用于过滤 never 类型的属性。

    假如有一个 interface Part,须要将 interface 中函数类型的属性名称取出来:

    interface Part {
      id: number,
      name: string,
      subparts: Part[],
      updatePart(newName: string): void
    }

    此时咱们能够把 interface 看作是对象字面量;遍历整个对象,找出 value 是函数类型的局部取出 key 即可;TypeScript 的类型编程也是相似操作,遍历 interface,找出类型为 Function 的局部取出 key 即可:

    type FunctionPropertyNames<T> = {[K in keyof T] : T[K] extends Function ? K : never }[keyof T]
    
    type R = FunctionPropertyNames<Part>;

    执行步骤:

    1. Part 代入泛型 T[K in keyof T] 相当于遍历整个 interface,此时 K 相当于 key,T[K]相当于 value
    2. 验证 value 的类型,如果是 Function 那么将 key 作为 value,否则为never,这样就失去一个新的 interface:

      {
        id: never,
        name: never,
        subparts: never,
        updatePart(newName: string): "updatePart"
      }
    3. 最初再用 [keyof T],顺次取出新 interface 的 value,因为idnamesubparts的 value 为 never 就不会返回任何类型了,所以只会返回"update"
  • 应用 TS 操作符 typeof 获取类型(构造)

    比方:

    const a1: number = 1;
    type C = typeof a1; // number
    const c: C = '1'; // TS2322: Type 'string' is not assignable to type 'number'.

    typeof a1失去的就是变量 a1 的类型 number。

    假如定义了一个函数和一个对象:

    function addFunction(a: number, b: number) {return a + b;}
    type addType = typeof addFunction;
    
    const student = {
      name: 'xiaoming',
      age: 12,
      grade: 1
    }
    type Student = typeof student;

    addType失去的类型就是 (a: number, b: number) => numberStudent 失去的类型就是{grade: number, name: string, age: number}

  • 应用 TS 关键字 infer 用于标识待推导和应用的类型(用在函数的参数类型和返回值类型)

    通常配合 extends 三目运算的条件判断来推断类型。

    如果咱们想晓得一个函数的返回值类型,能够应用 infer 关键字:

    type getReturnType<T> = T extends (...args: any[]) => infer P ? P : never;
    
    type addReturnType = getReturnType<typeof addFunction>; // number

    上述代码中定义了一个接管泛型的类型别名 getReturnType,当它接管的泛型满足约束条件,即合乎函数构造(...args: any[]) => infer P 时,就失去类型 P,否则就失去类型 never。infer标识了这个待推导类型。

    接着应用 typeof addFunction 失去 addFunction 函数的类型,将它传递给 getReturnType,最初失去返回值的类型number

    获取函数参数的类型也是一样的。例子:

    type getParamType<T> = T extends (x: infer V) => any ? V : never;
    type addParamType = getParamType<typeof addFunction>; // never

    addFunction 的类型不合乎约束条件,即 (a: number, b: number) => number 不能赋值给(x: infer V) => any,所以失去的参数类型是 never。

    当满足条件时,就失去参数类型。

    type getParamType<T> = T extends (...x: infer V) => any ? V : never;
    type addParamType = getParamType<typeof addFunction>; // [number, number] 元组类型
                                                              
    type getParamType<T> = T extends (x: infer V, y: infer U) => any ? V : never;
    type addParamType = getParamType<typeof addFunction>; // number

    函数类型是否满足约束条件,咱们须要通过参数类型和返回类型同时判断,比方上述代码中,比照参数 (x: infer V),是否传递给 addFunction 执行,显然不行,addFunction 须要接管两个参数;再比照返回类型,addFunction 的返回类型 number 是any 的子类型,所以返回值类型是满足的。综合下来,就是不满足约束条件了。

    总之就是调用满足类型 (x: infer V)=>any 的函数的中央,是否能替换成 addFunction 来执行,如果不能,就是不满足。

  • +- 操作符

    + -这两个关键字用于映射操作中给属性增加修饰符,比方 -? 就是将可选属性变为必选,-readonly就代表将只读属性变为非只读。

    TS 有一个内置的工具类型Required<T>,作用就是将传入的类型属性变为必选项,咱们也能够本人实现这个性能:

    type RawRequired<T> = {[P in keyof T]-?: T[P] };
    
    type Writable<T> = {-readonly [P in keyof T]: T[P] }
    
    // 应用如下
    interface Animal {
        type: string,
        name?: string,
        readonly gender: number
    }
    
    type requiredAnimal = RawRequired<Animal>; // {type: string, name: string, gender: number}
    type writableAnimal = Writable<Animal>; // {type: string, name?: string, gender: number}
    
    const cat1: Animal = { // name 为可选属性,没有初始化也没关系
        type: 'cat',
        gender: 1
    };
    // gender 是只读属性,赋值会报错
    cat1.gender = 0; // TS2540: Cannot assign to 'gender' because it is a read-only property.
    
    const cat2: requiredAnimal = { // name 变成了必选属性,初始化没有设置会报错
        type: 'cat',
        gender: 1
    }; // TS2741: Property 'name' is missing in type '{type: string; gender: number;}' but required in type 'RawRequired<Animal>'.
    
    const cat3: writableAnimal = { // gender 去除了只读限度,能够再次赋值
        type: 'cat',
        gender: 1
    };
    cat3.gender = 0;

工具类型:联合泛型和操作符

通常应用关键字 type 定义,应用泛型和操作符创立的相似函数性能的类型,能够通过泛型接管输出的类型,通过一系列操作失去并输入新的类型。

在后面的示例代码中,就利用了很多工具类型。比方:

type RawRequired<T> = {[P in keyof T]-?: T[P] };

能够通过泛型 T 接管指定的类型,通过操作后,失去新的类型:

type requiredAnimal = RawRequired<Animal>;

requiredAnimal就是失去的新类型{type: string, name: string, gender: number}

正文完
 0