理念:
- 先 把书读薄
- 系统化的知识 最牢固
- 摘录 总结 阅读后咀嚼/思考再写出啦
前言:
- TS针对的是编译期,主要是类型的检测等,本质就是通过结构化的类型定义来告知TS编译器如何检查该代码,不会影响运行时的真实代码;
- 但是 TS也额外扩展了一些js的语法
- TS的理念:严格优先
- TS编写的核心职能:描述类型
参考:
OverView
- 这是因为 TypeScript 只会在编译时对类型进行静态检查,如果发现有错误,编译的时候就会报错。而在运行时,与普通的 JavaScript 文件一样,不会对类型进行检查。
- 这是因为 TypeScript 编译的时候即使报错了,还是会生成编译结果,我们仍然可以使用这个编译之后的文件。
基础
不常见的基础类型
void
null undefined
- 与
void的区别是,undefined和null是所有类型的子类型。也就是说undefined类型的变量,可以赋值给number类型的变量 - 而
void类型的变量不能赋值给number类型的变量
任意值
- 在任意值上访问任何属性都是允许的,也允许调用任何方法
- 声明一个变量为任意值之后,对它的任何操作,返回的内容的类型都是任意值
- 变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型
- TypeScript 会在没有明确的指定类型的时候推测出一个类型,这就是类型推论
- 如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成
**any**类型而完全不被类型检查
联合类型
- 当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法
- 访问
string和number的共有属性是没问题的
function getString(something: string | number): string {
something.length // ts error: length在number类型不存在
something.toString() // right
}
interface
- 接口一般首字母大写。有的编程语言中会建议接口的名称加上
[I](https://msdn.microsoft.com/en-us/library/8bc1fexb%28v=vs.71%29.aspx)前缀 - 变量属性一般要和接口定义保持一致,多,少 都是不可以的
- 接口更灵活宽松的方案:可选属性,任意属性
- 任意属性:
interface Person {
name: string;
age?: number;
[propName: string]: any;
}
let tom: Person = {
name: 'Tom',
gender: 'male'
};
- 一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集
- 这句话也可以这样理解:已定义的属性也属于任意属性(其子集)
interface Person {
name: string;
age?: number;
[propName: string]: string;
}
let tom: Person = {
name: 'Tom',
age: 25,
gender: 'male'
};
- 一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型
interface Person {
name: string;
age?: number;
[propName: string]: string | number;
}
let tom: Person = {
name: 'Tom',
age: 25,
gender: 'male'
};
- 只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候
interface Person {
readonly id: number;
name: string;
age?: number;
[propName: string]: any;
}
let tom: Person = {
name: 'Tom',
gender: 'male'
// error 在对 tom 进行赋值的时候,没有给 id 赋值
};
tom.id = 89757; // error 在给 tom.id 赋值的时候,由于它是只读属性,所以报错了
常见的数组类型
- 表示法
- 类型 + 方括号
- 数组泛型 Array
- 接口表示数组(不推荐,接口常用来描述类数组)
interface NumberArray {
[index: number]: number;
}
let fibonacci: NumberArray = [1, 1, 2, 3, 5];
- any在数组中的应用:
let list: any[] = ['xcatliu', 25, { website: 'http://xcatliu.com' }];
- 类数组
- 一般不用接口描述数组,但是常用来描述类数组(不是数组类型)
- 例如函数中的arguments:
- 一般不用接口描述数组,但是常用来描述类数组(不是数组类型)
interface IArguments {
[index: number]: any;
length: number;
callee: Function;
}
函数
声明式定义函数
function sum(x: number, y: number): number {
return x + y;
}
表达式定义函数
let mySum: (x: number, y: number) => number // 这里是对变量mySum的类型定义,一般需要和右侧的函数定义保持一致,注意:这里的=>不是ES6中的=>
= function (x: number, y: number): number { // 这里的类型描述 是对右侧函数定义的描述
return x + y;
};
// 箭头函数定义
var getResult = (username: string, points: number): string => {
return `${ username } scored ${ points } points!`;
}
接口定义函数形状
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string): boolean {
return source.search(subString) !== -1;
}
// 表达式形式的函数类型需要给=左右两端都进行定义:
// 等号右侧的类型定义是右侧匿名函数的类型定义
// 等号左侧是对具名函数(该匿名函数赋值给的对象)的类型定义
let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
return x + y;
};
// ts中的=> 和 es6中的 => 不是一个
可选参数
- 可选参数必须接在必需参数后面。换句话说,可选参数后面不允许再出现必需参数了
参数默认值
剩余参数
function push(array: any[], ...items: any[]) {
// items实质上是一个数组,可以用数组的类型定义它
items.forEach(function(item) {
array.push(item);
});
}
重载
- 概念:重载允许一个函数接受不同数量或类型的参数时,作出不同的处理
- 用处:
/**
eg: 我们需要实现一个函数 reverse,输入数字 123 的时候,输出反转的数字 321,输入字符串 'hello' 的时候,输出反转的字符串 'olleh'。
*/
function reverse(x: number | string): number | string {
if (typeof x === 'number') {
return Number(x.toString().split('').reverse().join(''));
} else if (typeof x === 'string') {
return x.split('').reverse().join('');
}
}
/**
问题:然而上面这种写法有一个缺点,就是不能够精确的表达,输入为数字的时候,输出也应该为数字,输入为字符串的时候,输出也应该为字符串
所以 我们需要借助重载 实现多个定义,如下
*/
function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string {
if (typeof x === 'number') {
return Number(x.toString().split('').reverse().join(''));
} else if (typeof x === 'string') {
return x.split('').reverse().join('');
}
}
/**
注意:TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面
*/
类型别名 type
- 简化类型的名称
type myType = string | number;
let a: myType;
let b: myType;
a = '123';
b = 1;
类型断言的用途
语法
用途
- 将一个联合类型断言为其中一种类型
- 因为联合类型只能访问其各个子类型共有的属性或方法,利用类型断言 可以指定访问其中一种类型的属性或者方法
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
function isFish(animal: Cat | Fish) {
// 指定访问Fish的属性
if (typeof (animal as Fish).swim === 'function') {
return true;
}
return false;
}
b. 类型断言只能够「欺骗」TypeScript 编译器,无法避免运行时的错误,反而滥用类型断言可能会导致运行时错误
c. 总之,使用类型断言时一定要格外小心,尽量避免断言后调用方法或引用深层属性,以减少不必要的运行时错误。
- 将一个父类断言为更加具体的子类
interface ApiError extends Error {
code: number;
}
interface HttpError extends Error {
statusCode: number;
}
function isApiError(error: Error) {
if (typeof (error as ApiError).code === 'number') {
return true;
}
return false;
}
- 将任何一个类型断言为any
window.foo = 1; // error
(window as any).foo = 1 // right
- 将any断言为一个具体的类型
- 背景:在日常的开发中,我们不可避免的需要处理
any类型的变量,它们可能是由于第三方库未能定义好自己的类型,也有可能是历史遗留的或其他人编写的烂代码,还可能是受到 TypeScript 类型系统的限制而无法精确定义类型的场景。 - 遇到
any类型的变量时,我们可以选择无视它,任由它滋生更多的any。 - 我们也可以选择改进它,通过类型断言及时的把
any断言为精确的类型,亡羊补牢,使我们的代码向着高可维护性的目标发展。
- 背景:在日常的开发中,我们不可避免的需要处理
function getCacheData(key: string): any {
return (window as any).cache[key];
}
/**
那么我们在使用它时,最好能够将调用了它之后的返回值断言成一个精确的类型,这样就方便了后续的操作
*/
interface Cat {
name: string;
run(): void;
}
const tom = getCacheData('tom') as Cat;
tom.run();
类型断言 vs 类型转换
类型断言 vs 类型声明
- 类型声明更严格,优先使用类型声明
const tom = getCacheData('tom') as Cat;
// 等价于
const tom: Cat = getCacheData('tom');
类型断言 vs 泛型
// 利用把返回值从any断言为精确类型
function getCacheData(key: string): any {
return (window as any).cache[key];
}
interface Cat {
name: string;
run(): void;
}
const tom = get
// 利用泛型解决(推荐)
function getCacheData<T>(key: string): T {
return (window as any).cache[key];
}
interface Cat {
name: string;
run(): void;
}
const tom = getCacheData<Cat>('tom');
tom.run();
其他
- 其实类型断言利用的就是调用者比定义者更清楚某个定义??
- 另外,类型断言,只是用来帮助通过TS语法静态检查的一种手段,不要妄想对运行时有帮助,运行时我们还是需要通过类型判断,特性嗅探等来进行防御性处理;
- 任何类型都可以被断言为any, any也可以被断言为任何类型
- 将一个变量断言为
any可以说是解决 TypeScript 中类型问题的最后一个手段 , 因为**它极有可能掩盖了真正的类型错误,所以如果不是非常确定,就不要使用 ****as any **``**<br /> - 总之,一方面不能滥用
**as any**,另一方面也不要完全否定它的作用,我们需要在类型的严格性和开发的便利性之间掌握平衡(这也是 TypeScript 的设计理念之一),才能发挥出 TypeScript 最大的价值。 - 要使得
A能够被断言为B,只需要A兼容B或B兼容A即可
interface Animal {
name: string;
}
interface Cat {
name: string;
run(): void;
}
// 等价于(因为TypeScript 是结构类型系统,类型之间的对比只会比较它们最终的结构,而会忽略它们定义时的关系)
interface Animal {
name: string;
}
interface Cat extends Animal {
run(): void;
}
// 此时 Animal和Cat是相互兼容的,其实例也可以互相断言
声明文件的作用
作用
- 例如一个第三方库,有些特殊变量(例如jquery中的$),TS不知道它是什么,所以,无法通过TS的语法检查, 这时候需要
declare var来定义其类型:
declare var jQuery: (selector: string) => any;
- 声明语句会在编译后删除
- **声明文件:**通常我们会把声明语句放到一个单独的文件(
jQuery.d.ts)中
第三方声明文件
npm install @types/jquery --save-dev
书写声明文件
- 背景:当一个第三方库没有提供声明文件时,我们就需要自己书写声明文件了
- 全局变量:
- 例如:jquery的$就是一种全局变量,
- 使用第三方的声明文件,则什么都不需要做
- 如果是将声明文件直接存放于当前项目中,则建议和其他源码一起放到
src目录下(或者对应的源码目录下) - 语法:
declare const - 一般来说,全局变量都是禁止修改的常量,所以大部分情况都应该使用
const - 关于declare namespace
- 防止命名冲突:
- 暴露在最外层的
interface或type会作为全局类型作用于整个项目中,我们应该尽可能的减少全局变量或全局类型的数量。故最好将他们放到namespace下
- 暴露在最外层的
// src/jQuery.d.ts
declare namespace jQuery {
interface AjaxSettings {
method?: 'GET' | 'POST'
data?: any;
}
function ajax(url: string, settings?: AjaxSettings): void;
}
// 使用时 加上jQuery前缀
// src/index.ts
let settings: jQuery.AjaxSettings = {
method: 'POST',
data: {
name: 'foo'
}
};
jQuery.ajax('/api/post_something', settings);
- npm包
内置对象
进阶
字符串字面量类型
type EventNames = 'click' | 'scroll' | 'mousemove';
枚举
类
访问修饰符
- public
- private
- protected
- 子类中可以访问
protected constructor() {}该类只允许被集成
抽象类
- 不允许被实例化, 只允许被继承
- 抽象类中的抽象方法必须被子类实现
abstract class Animal {
public name;
public constructor(name) {
this.name = name;
}
public abstract sayHi();
}
class Cat extends Animal {
public sayHi() {
console.log(`Meow, My name is ${this.name}`);
}
}
let cat = new Cat('Tom');
类的类型使用
// 用class作为类型 和interface类似
// 本质上:在声明一个类Animal时,除了会创建该类Animal外,还会创建一个名为Animal的类型 (当然该该类型不包括constructor)
// 所以 我们既可以将Animal当一个类来使用,也可以当一个类型使用
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
sayHi(): string {
return `My name is ${this.name}`;
}
}
let a: Animal = new Animal('Jack');
类与接口
接口的意义
一个类,一般只能继承一个类。有时候不同类之间可以有一些共有特性,这时候可以把这部分特性提取成接口,用implements实现。
也就是说,一个接口可以用来描述一个类或者某些共有特征(属性,方法)
泛型
泛型的本质:
- 就是通过类似于参数的形式,告知结构化类型其中某个在类型定义时未定义的类型;有些拗口:就是在使用某类型时通过泛型语法传入一个类型给该类型;
- 泛型变量:
- 可以在描述该类型结构的时候使用
- 可以在该函数或者类定义时,在内部使用;
- 内部使用时注意泛型约束
- 如果类型定义是一个函数,那么泛型可以理解为传给该类型函数的一个参数
泛型约束[⏫]
- 解决问题:在函数内部使用泛型变量的时候,由于事先不知道其是哪种类型,所以不能随意操作它的属性和方法,这个时候,通过对泛型进行约束,例如用接口描述该泛型变量的特征:
/**
* 因为函数内部需要使用length属性,通过extends该接口,使T必须具有length属性
*/
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
泛型的提取
- 如果有一个在定义时无法确定的类型,那么就可以使用泛型来描述其类型,同时,要注意如果函数内部需要使用该泛型变量的一些特征等,可以通过
interface对该泛型变量的特征进行描述,并使用该泛型extends该interface
泛型接口
- 使用含有泛型的接口描述一个函数
interface CreateArrayFunc {
<T>(length: number, value: T): Array<T>;
}
let createArray: CreateArrayFunc;
createArray = function<T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
createArray(3, 'x');
泛型类
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
泛型参数默认类型
- 泛型参数默认类型:在没有明确指出该泛型,并且从实际值参数中无法推测出时,该默认值会生效:
function createArray<T = string>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
声明合并
概念
如果定义了两个相同名字的函数、接口或类,那么它们会合并成一个类型
函数合并
就是函数重载
function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string {
if (typeof x === 'number') {
return Number(x.toString().split('').reverse().join(''));
} else if (typeof x === 'string') {
return x.split('').reverse().join('');
}
}
接口合并
interface Alarm {
price: number;
}
interface Alarm {
weight: number;
}
// 相当于
interface Alarm {
price: number;
weight: number;
}
- 合并的属性
interface Alarm {
price: number;
}
interface Alarm {
price: string; // 类型不一致,会报错
weight: number;
}
- 接口中方法的合并和函数的合并一样
interface Alarm {
price: number;
alert(s: string): string;
}
interface Alarm {
weight: number;
alert(s: string, n: number): string;
}
// 相当于
interface Alarm {
price: number;
weight: number;
alert(s: string): string;
alert(s: string, n: number): string;
}
类的合并
- 合并规则和接口的合并规则一致
工程
代码检查
在TS中使用ESLint
- 安装eslint
npm install --save-dev eslint
- 安装typescript
npm install --save-dev typescript
- 安装@typescript-eslint/parser
- 作用:由于ESLint默认使用Expree进行语法解析,无法识别TS的一些语法,所以需要安装@typescript-eslint/parser替代掉默认的解析器
npm install --save-dev @typescript-eslint/parser
- 安装@typescript-eslint/eslint-plugin
- 作用:作为eslint默认规则的补充,提供了一些额外的适用于ts语法的规则
npm install --save-dev @typescript-eslint/eslint-plugin
创建配置文件
在项目根目录下创建一个.eslintrc.js
module.exports = {
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
rules: {
// 禁止使用 var
'no-var': "error",
// 优先使用 interface 而不是 type
'@typescript-eslint/consistent-type-definitions': [
"error",
"interface"
]
}
}
检查整个项目的ts文件
{
"scripts": {
"eslint": "eslint src --ext .ts" // 检查src目录下的.ts文件(eslint默认不会检查.ts文件,所以需要加上--ext .ts)
}
}
VSCode中的配置
- 安装eslint插件
- vscode中的eslint插件默认不会检查.ts文件,
- [文件-首选项-设置-工作区]
- 或者 在项目根目录下创建一个配置文件.vscode/settings.json
团队项目推荐
{
"eslint.validate": [
"javascript",
"javascriptreact",
{
"language": "typescript",
"autoFix": true // 开启自动修复
}
],
"typescript.tsdk": "node_modules/typescript/lib"
}
使用prettier修复格式错误
- 目的:保持代码风格统一
- 安装prettier
npm install --save-dev prettier
- 创建一个prettier.config.js
// 一个推荐配置
// prettier.config.js or .prettierrc.js
module.exports = {
// 一行最多 100 字符
printWidth: 100,
// 使用 4 个空格缩进
tabWidth: 4,
// 不使用缩进符,而使用空格
useTabs: false,
// 行尾需要有分号
semi: true,
// 使用单引号
singleQuote: true,
// 对象的 key 仅在必要时用引号
quoteProps: 'as-needed',
// jsx 不使用单引号,而使用双引号
jsxSingleQuote: false,
// 末尾不需要逗号
trailingComma: 'none',
// 大括号内的首尾需要空格
bracketSpacing: true,
// jsx 标签的反尖括号需要换行
jsxBracketSameLine: false,
// 箭头函数,只有一个参数的时候,也需要括号
arrowParens: 'always',
// 每个文件格式化的范围是文件的全部内容
rangeStart: 0,
rangeEnd: Infinity,
// 不需要写文件开头的 @prettier
requirePragma: false,
// 不需要自动在文件开头插入 @prettier
insertPragma: false,
// 使用默认的折行标准
proseWrap: 'preserve',
// 根据显示样式决定 html 要不要折行
htmlWhitespaceSensitivity: 'css',
// 换行符使用 lf
endOfLine: 'lf'
};
- 安装VSCode prettier插件
- 修改.vscode/setting.json,实现保存文件时对代码进行自动格式化:
{
"files.eol": "
",
"editor.tabSize": 4,
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"eslint.autoFixOnSave": true,
"eslint.validate": [
"javascript",
"javascriptreact",
{
"language": "typescript",
"autoFix": true
}
],
"typescript.tsdk": "node_modules/typescript/lib"
}
使用ESLint检查tsx文件
- 安装eslint-plugin-react
npm install --save-dev eslint-plugin-react
- package.json的scripts.eslint添加.tsx
{
"scripts": {
"eslint": "eslint src --ext .ts,.tsx"
}
}
- VSCode配置新增typescript检查
{
"files.eol": "
",
"editor.tabSize": 4,
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"eslint.autoFixOnSave": true,
"eslint.validate": [
"javascript",
"javascriptreact",
{
"language": "typescript",
"autoFix": true
},
{
"language": "typescriptreact", // 增加对tsx的检查
"autoFix": true
}
],
"typescript.tsdk": "node_modules/typescript/lib"
}
TroubleShooting
编译选项
tsc --init 生成tcconifg文件
- include
- exclude
- compilerOptions:
- target: 'es5', // 指定ts被编译为ES的版本
- module: 'es2015', // 指定被编译为的模块化标准
- lib: // 告诉TS在项目中使用到的库,会有语法提示等。一般不需要改,只有在非浏览器环境下需要用到某些依赖,可以设置。设置后可以有语法提示。例如在node中使用document.
- outDir
- outFile: './dist/app.js' // 会把所有代码合并为一个文件输出, 不怎么用,一般交给打包工具去做
- allowJs: true/false, // 是否对js进行编译,一般给true
- checkJs: true/false, // 是否对js进行实时检查,一般和allowJs保持一致
- removeComments: true/false, // 是否移除注释
- noEmit: false, // true: 不生成编译后的文件,仅仅是为了使用TS检查 不需要生成编译文件
- noEmitOnError: true, // 当有错误的时候 不生产编译后的文件
- strict: true, // 严格模式的总开关,建议为true, 以下的严格选项 都会开启
- alwaysStrict: true, // 生成的代码使用”严格模式“ - 插入 ”use strict“
- noImplicitAny: true // 不允许使用隐式的any,也就是未指定的类型的变量,ts编译器不会默认指定为any类型
- noImplicitThis: true // 不允许不明确类型的this
- strictNullChecks: true // 严格的空值检查
// strictNullChecks: true const box = document.getElementById('id'); box.appendChild(); // 这里的box就可能会是null ts会提醒 我们可以用if判断下box非null之类的
一份编译选项的最佳实践配置 - ??
其他
- 如果我们需要保证运行时的参数类型,还是得手动对类型进行判断;
- TypeScript 编译的时候即使报错了,还是会生成编译结果,我们仍然可以使用这个编译之后的文件。
- 要在报错的时候终止 js 文件的生成,可以在
tsconfig.json中配置noEmitOnError即可
- 要在报错的时候终止 js 文件的生成,可以在
- 使用构造函数
Boolean创造的对象不是布尔值:
疑问
直面内心困惑疑虑
- Q:Promise的类型处理方案??
- A:
// 这里的promise<number> 代表该函数的返回值为一个promise, 该promise的then中的返回值为number类型
function createPromise(isResolve: boolean): Promise<number> {
return new Promise((resolve, reject) => {
if (isResolve) {
resolve(0);
}
reject(1);
});
}
{
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript"
],
"typescript.tsdk": "node_modules/typescript/lib"
}