从实践角度梳理一下常用的 TS 知识点和使用场景
想要学号 TS,官方文档多读,代码多写,无他。
- https://www.tslang.cn/
- https://ts.xcatliu.com/ xcatliu 的 TS 文章也不错,适合前端工程师阅读
类型和值
要想搞懂 TS,先理解这2个概念
类型就是变量的类型,类的类型(使用 interface 定义),函数的类型(参数和返回值)
声明类型的方法有6种
- 使用类型别名
- x: number
- 使用枚举 enum
- 使用接口 interface
- 类声明 class c {}
什么是值?
声明一个变量,声明一个函数,然后这些变量、函数可以被调用,值是运行时的名字。
可索引类型
interface StringArray {[index: number]: string;}let myArray: StringArray;myArray = ["Bob", "Fred"];let myStr: string = myArray[0];
interface 和 type 关键字
功能非常相近,但是又有点区别
interface 和 type 两个关键字的含义和功能都非常的接近。这里我们罗列下这两个主要的区别:
interface:
- 同名的 interface 自动聚合,也可以跟同名的 class 自动聚合
- 可以给函数挂属性
- 只能表示 object、class、function 类型
- 想要扩展接口,只能用继承的方式
- 不能实现或者继承联合类型的 type
type:
- 不仅仅能够表示 object、class、function
- 不能重名(自然不存在同名聚合了),扩展已有的 type 需要创建新 type
- 支持复杂的类型操作,比如说 & |
- 想要扩展 type 定义的类型,使用 &
枚举
常用的有常量枚举,不生成类型,只使用枚举值。
export const enum Direction {NORTH = "NORTH",SOUTH = "SOUTH",EAST = "EAST",WEST = "WEST",}
断言
有时候,我们比 TS 更清楚变量是什么类型,可以使用断言,通常我们在 React 中使用 as 语法。
let someValue: any = "this is a string";let strLength: number = (someValue as string).length;
尖括号语法
function getLength(something: string | number): number {if ((<string>something).length) {return (<string>something).length;} else {return something.toString().length;}}
类型保护
在特定的区块中保证变量属于某种确定的类型,可以在此区块中放心的引用此类型的属性,或者调用此类型的方法。
- instanceof
- typeof 判断类型
- in 某个 key 存在于实例中
- 保护函数
特殊的返回值 - 类型谓词
function isNumber(x: any): x is number {return typeof x === "number";}function isString(x: any): x is string {return typeof x === "string";}
交叉类型 联合类型
interface IPerson {id: string;age: number;}interface IWorker {companyId: string;}type IStaff = IPerson & IWorker;const sayHello = (name: string | undefined) => {/* ... */};
为函数指定类型
注意:使用变量定义、类型别名、接口约束函数,都是函数类型的声明定义,没有实现该函数
// 返回值可以推断出来,不用写const add = (x: number, y: number) => x + yconst add9 = (x: number, y: number): number => x + y
使用接口约束函数
interface F {(a: number, b: number): number;}let add: F = (a, b) => a + b
类型别名约束函数
type Add = (x: number, y: number) => number;let add: Add = (a, b) => a + b
变量定义函数
let mySum: (x: number, y: number) => numbermySum = function (x: number, y: number): number {return x + y;};
模块
第一,ES6 模块与 CommonJS 规范不要混用
有一种兼容性写法
导出 export =
导入 import from 或者 impot x = require
操作符
extends
type num = {num:number;}interface IStrNum extends num {str:string;}// 与上面等价type TStrNum = A & {str:string;}
keyof
获取某种类型的所有 key 的联合类型
interface Person {name: string;age: number;location: string;}type K1 = keyof Person; // "name" | "age" | "location"type K2 = keyof Person[]; // number | "length" | "push" | "concat" | ...type K3 = keyof { [x: string]: Person }; // string | number
泛型
泛型就是定义一种模板,例如ArrayList
,然后在代码中为用到的类创建对应的ArrayList<类型>。
- 实现了编写一次,万能匹配,又通过编译器保证了类型安全:这就是泛型
- 泛型的好处是使用时不必对类型进行强制转换,它通过编译器对类型进行检查;
- 泛型可以通过参数
决定引入的参数类型 参考 什么是泛型 《Java教程》廖雪峰
通常我们说,泛型就是指定一个表示类型的变量
「泛型就是不预先确定的数据类型,具体的类型在使用的时候再确定的一种类型约束规范」
「我们也可以把泛型变量理解为函数的参数,只不过是另一个维度的参数,是代表类型而不是代表值的参数。」**
泛型可以用于类的实例成员、类的方法、函数参数和函数返回值
泛型可以应用于 function、interface、type 或者 class 中。但是注意,「泛型不能应用于类的静态成员」
interface GenericIdentityFn<T> {(arg: T): T;}class GenericNumber<T> {zeroValue: T;add: (x: T, y: T) => T;}let myGenericNumber = new GenericNumber<number>();myGenericNumber.zeroValue = 0;
泛型类型、泛型接口
type Log = <T>(value: T) => Tlet myLog: Log = loginterface Log<T> {(value: T): T}let myLog: Log<number> = log // 泛型约束了整个接口,实现的时候必须指定类型。如果不指定类型,就在定义的之后指定一个默认的类型myLog(1)
函数中省略尖括号,推断出要传入的类型
TS 会自动推断出要传的参数类型
function identity <T, U>(value: T, message: U) : T {console.log(message);return value;}console.log(identity(68, "Semlinker"));
函数返回一个object,并使用泛型
interface Iidentity<T, U> {value: T,message: U}function identity<T, U>(value: T, message: U): Iidentity<T, U> {return {value,message} ;}console.log(identity([12,3], 'hello'));
常见泛型变量代表的意思
- T(Type):表示一个 TypeScript 类型
- K(Key):表示对象中的键类型
- V(Value):表示对象中的值类型
- E(Element):表示元素类型
泛型类
泛型类可确保在整个类中一致地使用指定的数据类型。
我们来看一个泛型类实现泛型接口的例子
interface GenericInterface<U> {value: UgetIdentity: () => U}class IdentityClass<T> implements GenericInterface<T> {value: Tconstructor(value: T) {this.value = value}getIdentity(): T {return this.value}}// 可以省略 <number>const myNumberClass = new IdentityClass(68);console.log(myNumberClass.getIdentity()); // 68const myStringClass = new IdentityClass<string>("Semlinker!");console.log(myStringClass.getIdentity()); // Semlinker!
什么时候需要使用泛型?
当你的函数、接口或类将处理多种数据类型时
可能刚开始不使用泛型,随着项目越来越复杂,需要支持多种数据类型,你可能要考虑使用泛型了
类型约束,需预定义一个接口
希望限制每个类型变量(T)接受的类型数量
1 只允许函数传入那些包含 length 属性的变量
interface Length {length: number}function logAdvance<T extends Length>(value: T): T {console.log(value, value.length);return value;}// 必须传入具有 length 属性的logAdvance([1])logAdvance('123')logAdvance({ length: 3 })
2 举一个例子,返回一个对象上的 k-v 值
// 限制输入的 key 是对象上的function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {return obj[key];}let tsInfo = {name: "Typescript",supersetOf: "Javascript",}let difficulty = getProperty(tsInfo, 'supersetOf'); // OK
泛型参数指定默认类型
interface A<T=string> {name: T;}const strA: A = { name: "Semlinker" };// 如果想传入 number,需写上<number>const numB: A<number> = { name: 101 };
泛型工具类型 - 映射类型
返回一个新类型
常用的如 Partial、Required、Readonly、Record 和 ReturnType
/*** node_modules/typescript/lib/lib.es5.d.ts* Make all properties in T optional*/type Partial<T> = {[P in keyof T]?: T[P];};/*** node_modules/typescript/lib/lib.es5.d.ts* Construct a type with a set of properties K of type T* 引入新的类型*/type Record<K extends keyof any, T> = {[P in K]: T;};// node_modules/typescript/lib/lib.es5.d.ts/*** From T, pick a set of properties whose keys are in the union K* 把 T 的子属性挑出来,变成包含这个类型部分属性的子类型。*/type Pick<T, K extends keyof T> = {[P in K]: T[P];};// node_modules/typescript/lib/lib.es5.d.ts/*** Exclude from T those types that are assignable to U* 将 T 中某些属于 U 的属性剔除掉**/type Exclude<T, U> = T extends U ? never : T;
类型声明和声明文件
为啥要写声明文件,不写编辑器就会报错,类型检查不通过,代码编译不通过。
声明文件的作用?
比如你引入一个 JS 文件,TS 编译器并不能知道这个模块导出了什么接口,所以你要写一个 .d.ts 来告诉 TS 你所暴露的成员都是什么,接受什么参数等等。
TypeScript 的声明文件是一个以
.d.ts为后缀的 TypeScript 代码文件, 但它的作用是描述一个 JavaScript 模块(广义上的)内所有导出接口的类型信息
前置学习条件
- 学习 declare 语句
- 不同场景下,如何导出
- 普通模块
- 全局JS
- UMD
- 学习声明合并
- 学习 namespace
引入的@types包
默认所有可见的”@types”包会在编译过程中被包含进来。如果指定了 typeRoots和types,结果会有些不同。看这里 https://www.tslang.cn/docs/handbook/tsconfig-json.html
我们结合常见的三种场景梳理一下类型声明文件该怎么写
全局 JS
修改全局作用域 的模块( 模块化引入 )
global-modifying-module.d.ts
当一个全局修改的模块被导入的时候,它们会改变全局作用域里的值
// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]// Project: [~THE PROJECT NAME~]// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>/*~ This is the global-modifying module template file. You should rename it to index.d.ts*~ and place it in a folder with the same name as the module.*~ For example, if you were writing a file for "super-greeter", this*~ file should be 'super-greeter/index.d.ts'*//*~ Note: If your global-modifying module is callable or constructable, you'll*~ need to combine the patterns here with those in the module-class or module-function*~ template files*/declare global {/*~ Here, declare things that go in the global namespace, or augment*~ existing declarations in the global namespace*/interface String {fancyFormat(opts: StringFormatOptions): string;}}/*~ If your module exports types or values, write them as usual */export interface StringFormatOptions {fancinessLevel: number;}/*~ For example, declaring a method on the module (in addition to its global side effects) */export function doSomething(): void;/*~ If your module exports nothing, you'll need this line. Otherwise, delete it */export { };
修改全局作用域 的 JS 插件(通过 script 引入)
global-plugin.d.ts
// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]// Project: [~THE PROJECT NAME~]// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>/*~ This template shows how to write a global plugin. *//*~ Write a declaration for the original type and add new members.*~ For example, this adds a 'toBinaryString' method with to overloads to*~ the built-in number type.*/interface Number {toBinaryString(opts?: MyLibrary.BinaryFormatOptions): string;toBinaryString(callback: MyLibrary.BinaryFormatCallback, opts?: MyLibrary.BinaryFormatOptions): string;}/*~ If you need to declare several types, place them inside a namespace*~ to avoid adding too many things to the global namespace.放到命名空间下,避免太多全局变量*/declare namespace MyLibrary {type BinaryFormatCallback = (n: number) => string;interface BinaryFormatOptions {prefix?: string;padding: number;}}
普通的全局库( script 引入)
使用 global.d.ts 模板
// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]// Project: [~THE PROJECT NAME~]// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>/*~ If this library is callable (e.g. can be invoked as myLib(3)),如果该库可调用 muLib(3)*~ include those call signatures here.*~ Otherwise, delete this section.*/declare function myLib(a: string): string;declare function myLib(a: number): number;/*~ If you want the name of this library to be a valid type name,*~ you can do so here.想让你的类库是一个有效的类型名*~*~ For example, this allows us to write 'var x: myLib';*~ Be sure this actually makes sense! If it doesn't, just*~ delete this declaration and add types inside the namespace below.*/interface myLib {name: string;length: number;extras?: string[];}/*~ If your library has properties exposed on a global variable,你的类库在全局变量下暴露的很多属性*~ place them here.*~ You should also place types (interfaces and type alias) here.*/declare namespace myLib {//~ We can write 'myLib.timeout = 50;'let timeout: number;//~ We can access 'myLib.version', but not change itconst version: string;//~ There's some class we can create via 'let c = new myLib.Cat(42)'//~ Or reference e.g. 'function f(c: myLib.Cat) { ... }class Cat {constructor(n: number);//~ We can read 'c.age' from a 'Cat' instancereadonly age: number;//~ We can invoke 'c.purr()' from a 'Cat' instancepurr(): void;}//~ We can declare a variable as//~ 'var s: myLib.CatSettings = { weight: 5, name: "Maru" };'interface CatSettings {weight: number;name: string;tailLength?: number;}//~ We can write 'const v: myLib.VetID = 42;'//~ or 'const v: myLib.VetID = "bob";'type VetID = string | number;//~ We can invoke 'myLib.checkCat(c)' or 'myLib.checkCat(c, v);'function checkCat(c: Cat, s?: VetID);}
UMD
UMD 指的是既可以通过 script 引入,通过全局变量执行,又可以通过模块化引入的方式。
为什么模块导出要这样写?export = MyClass;
TODO;
如果模块能够作为 function调用
module-function.d.ts
export as namespace myFuncLib;/*~ This declaration specifies that the function*~ is the exported object from the file*/export = MyFunction;/*~ This example shows how to have multiple overloads for your function */declare function MyFunction(name: string): MyFunction.NamedReturnType;declare function MyFunction(length: number): MyFunction.LengthReturnType;/*~ If you want to expose types from your module as well, you can*~ place them in this block. Often you will want to describe the*~ shape of the return type of the function; that type should*~ be declared in here, as this example shows.*/declare namespace MyFunction {export interface LengthReturnType {width: number;height: number;}export interface NamedReturnType {firstName: string;lastName: string;}/*~ If the module also has properties, declare them here. For example,*~ this declaration says that this code is legal:*~ import f = require('myFuncLibrary');*~ console.log(f.defaultName);*/export const defaultName: string;export let defaultLength: number;}
如果模块能够被 new
module-class.d.ts
export as namespace myClassLib;/*~ This declaration specifies that the class constructor function*~ is the exported object from the file*/export = MyClass;/*~ Write your module's methods and properties in this class */declare class MyClass {constructor(someParam?: string);someProperty: string[];myMethod(opts: MyClass.MyClassMethodOptions): number;}/*~ If you want to expose types from your module as well, you can*~ place them in this block.*/declare namespace MyClass {export interface MyClassMethodOptions {width?: number;height?: number;}}
如果模块不能被调用或构造
module.d.ts
下面是 module.d.ts 的例子
// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]// Project: [~THE PROJECT NAME~]// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>/*~ This is the module template file. You should rename it to index.d.ts*~ and place it in a folder with the same name as the module.*~ For example, if you were writing a file for "super-greeter", this*~ file should be 'super-greeter/index.d.ts'*//*~ If this module is a UMD module that exposes a global variable 'myLib' when*~ loaded outside a module loader environment, declare that global here.*~ Otherwise, delete this declaration.*/export as namespace myLib;/*~ If this module has methods, declare them as functions like so.*/export function myMethod(a: string): string;export function myOtherMethod(a: number): number;/*~ You can declare types that are available via importing the module */// 您可以通过导入模块声明可用的类型// 这些类型声明可以在你的代码库中使用export interface someType {name: string;length: number;extras?: string[];}/*~ You can declare properties of the module using const, let, or var */export const myField: number;/*~ If there are types, properties, or methods inside dotted names*~ of the module, declare them inside a 'namespace'.*/export namespace subProp {/*~ For example, given this definition, someone could write:*~ import { subProp } from 'yourModule';*~ subProp.foo();*~ or*~ import * as yourMod from 'yourModule';*~ yourMod.subProp.foo();*/export function foo(): void;}
插件
插件的引入,会改变模块或者全局变量
模块插件或者 UMD 插件
module-plugin.d.ts
比如一个 moment 的插件,会为 moment 对象添加新的方法
了解一下 declare module 扩展模块 的用法
// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]// Project: [~THE PROJECT NAME~]// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>/*~ This is the module plugin template file. You should rename it to index.d.ts*~ and place it in a folder with the same name as the module.*~ For example, if you were writing a file for "super-greeter", this*~ file should be 'super-greeter/index.d.ts'*//*~ On this line, import the module which this module adds to */import * as m from 'someModule';/*~ You can also import other modules if needed */import * as other from 'anotherModule';/*~ Here, declare the same module as the one you imported above */declare module 'someModule' {/*~ Inside, add new function, classes, or variables. You can use*~ unexported types from the original module if needed. */export function theNewMethod(x: m.foo): other.bar;/*~ You can also add new properties to existing interfaces from*~ the original module by writing interface augmentations */export interface SomeModuleOptions {someModuleSetting?: string;}/*~ New types can also be declared and will appear as if they*~ are in the original module */export interface MyModulePluginOptions {size: number;}}
模块
如果我们一个 JS 库,想要发布到 npm 上,并且像为它添加上类型文件。可以直接放一个 index.d.ts 文件(位置在包的根目录下,与 index.js 并列 ),就行了。或者不叫 index.d.ts ,那样就需要通过 package.json 文件的 typings 或者 types 字段,指定这个文件。我们来看一个例子
{"name": "awesome","author": "Vandelay Industries","version": "1.0.0","main": "./lib/main.js","types": "./lib/main.d.ts"}
如果我们引入别人写的模块,而且正好 @types仓库里没有现成的,那自己整一个吧。
创建一个 types 目录,把声明文件都放到这个目录下,然后修改 tsconfig.json
{"compilerOptions": {"module": "commonjs","baseUrl": "./","paths": {"*": ["types/*"]}}}
把上面 UMD 那个示例文件,删掉下面这一行,不用导出全局变量
export as namespace myLib;
关于依赖
如果我们写的类库中依赖的第三方库,没有将自己的声明文件放到包中,那我们需要下载该依赖文件,并且依赖项放到 dependencies,而不是 devDependencies。如果放到 dev 依赖,用户就需要自己 install @types/xxx 了。
{"name": "browserify-typescript-extension","author": "Vandelay Industries","version": "1.0.0","main": "./lib/main.js","types": "./lib/main.d.ts","dependencies": {"browserify": "latest","@types/browserify": "latest","typescript": "next"}}
全局变量
下面的写法用到了声明合并,命名空间和函数合并,相当于为函数添加属性和方法
// global.d.tsdeclare function globalLib(options: globalLib.Options): void;declare namespace globalLib {const version: string;function doSomething(): void;interface Options {[key: string]: any}}// global.tsfunction globalLib(options) {console.log(options);}globalLib.version = '1.0.0';globalLib.doSomething = function() {console.log('globalLib do something');};
自动生成声明文件
如果你的类库是用 TS 写的,在 tsconfig.json 里开启 declaration 选型,或者使用 tsc —declaration
{"compilerOptions": {"module": "commonjs","outDir": "lib","declaration": true,}}
类型别名
type Message = string | string[];
interface
工程知识
常用的 npm 依赖
"dependencies": {"typescript": "3.5.3"},"devDependencies": {"@types/jest": "24.0.15","@types/node": "12.6.8","@types/react": "16.8.23","@types/react-dom": "16.8.5","@types/lodash": "^4.14.136","@types/react-redux": "^7.1.2","@types/react-router-dom": "^4.3.4",}
@types放哪里的问题
假设您正在开发一个包含“A”的包,它在devDependencies中包含@types/some-module包。出于某种原因,您要从@types/some-module导出类型
import {SomeType} from 'some-module';export default class APackageClass {constructor(private config: SomeType) {}}
现在,包“A”的TypeScript使用者无法猜出SomeType是什么,因为未安装包“A”的devDependencies。
在这种特殊情况下,您需要将@ types/*包与常规“依赖”放在一起。对于其他情况,“devDependencies”足够好。
node 执行 ts 代码
node 环境需要使用 ts-node 来编译 ts 代码
编译工具、代码检查工具
配置文件
{"compilerOptions": {/* 基本选项 */"target": "es5", // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'"module": "commonjs", // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'"lib": [], // 指定要包含在编译中的库文件"allowJs": true, // 允许编译 javascript 文件"checkJs": true, // 报告 javascript 文件中的错误"jsx": "preserve", // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'"declaration": true, // 生成相应的 '.d.ts' 文件"sourceMap": true, // 生成相应的 '.map' 文件"outFile": "./", // 将输出文件合并为一个文件"outDir": "./", // 指定输出目录"rootDir": "./", // 用来控制输出目录结构 --outDir."removeComments": true, // 删除编译后的所有的注释"noEmit": true, // 不生成输出文件"importHelpers": true, // 从 tslib 导入辅助工具函数"isolatedModules": true, // 将每个文件做为单独的模块 (与 'ts.transpileModule' 类似)./* 严格的类型检查选项 */"strict": true, // 启用所有严格类型检查选项"noImplicitAny": true, // 在表达式和声明上有隐含的 any类型时报错"strictNullChecks": true, // 启用严格的 null 检查"noImplicitThis": true, // 当 this 表达式值为 any 类型的时候,生成一个错误"alwaysStrict": true, // 以严格模式检查每个模块,并在每个文件里加入 'use strict'/* 额外的检查 */"noUnusedLocals": true, // 有未使用的变量时,抛出错误"noUnusedParameters": true, // 有未使用的参数时,抛出错误"noImplicitReturns": true, // 并不是所有函数里的代码都有返回值时,抛出错误"noFallthroughCasesInSwitch": true, // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)/* 模块解析选项 */"moduleResolution": "node", // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)"baseUrl": "./", // 用于解析非相对模块名称的基目录"paths": {}, // 模块名到基于 baseUrl 的路径映射的列表"rootDirs": [], // 根文件夹列表,其组合内容表示项目运行时的结构内容"typeRoots": [], // 包含类型声明的文件列表"types": [], // 需要包含的类型声明文件名列表"allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入。/* Source Map Options */"sourceRoot": "./", // 指定调试器应该找到 TypeScript 文件而不是源文件的位置"mapRoot": "./", // 指定调试器应该找到映射文件而不是生成文件的位置"inlineSourceMap": true, // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件"inlineSources": true, // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性/* 其他选项 */"experimentalDecorators": true, // 启用装饰器"emitDecoratorMetadata": true // 为装饰器提供元数据的支持}}
