先提一嘴:TypeScript 是结构类型系统,类型之间的对比只会比较它们最终的结构,而会忽略它们定义时的关系。
类型
包含 js 的所有数据类型
-
number
-
string
-
boolean
-
null
-
undefined
-
symbol
-
bigint
-
void(空值)
-
never 永远不会存在的类型,一般死循环/抛错时类型就是 never,是 bottom type
-
any 动态类型,相当于关闭了 ts 的类型检查功能
-
unknown 不可预先定义的类型,在大多数场景下都可以代替 any 并且保留类型检查功能
-
联合类型(|)多个类型的组合 string | number --- 表示该类型可以是 string 类型也可以是 number 类型
-
交叉类型(&)常用于类型合并/继承,最后的类型包含给定类型的所有属性,不能合并的时候返回 never
-
对象(可以使用接口描述对象) object/Object
-
数组(Array<type> / type[])
-
函数 Function
- 函数声明的类型定义
- 函数表达式的类型定义
- 可选参数? 一定要放在函数参数的最后
- 默认参数(默认参数就是可选的,但是不要求位置一定在最后)
- 剩余参数
-
枚举 enum 可以当作值(JS 对象使用)也可以作为类型使用
- 常量枚举 const enum: 在编译的时候会直接被删除且使用的地方替换为值;不可以包含计算成员
- 普通枚举会提供值和键的双向映射
- 枚举计算值的下一个枚举属性一定要对其进行初始化
- declare enum: 外部枚举,只会用于类型检查,在编译结果中被删除,但是需要注意的是,并不会把使用的地方替换为具体值,所以不能作为值使用,因为代码在运行是会报错
- declare const enum: 外部常量枚举,相当于定义了一个值,所以可以作为变量使用,编译之后会删除类型将使用的地方替换为具体的值,所以代码可以正常运行
- 数字类型和数字类型的枚举是兼容的
declare enum Directions {
Up,
Down,
Left,
Right
}
let directions = [
Directions.Up,
Directions.Down,
Directions.Left,
Directions.Right
];
console.log(directions);
编译结果
var directions = [
Directions.Up,
Directions.Down,
Directions.Left,
Directions.Right
];
console.log(directions);
⚠️ 运行报错
-
元祖 [string, number] 类型和个数确定的数组
- 元祖的越界操作插入的元素是已有元素类型的联合类型
-
字面量类型:类型是一个值
-
类:访问修饰符 public、private、protected
补充说明
-
null/undefined 是任意类型的子类型(除了 never),在为指定
--strictNullChecks
未指定的时候 -
any 和 unknown 的区别
- any 类型的值可以赋值给任意类型的值,任意类的值也可以赋值给 any 类型,可以访问 any 类型变量上的任意属性和方法,声明一个变量但是没有用指定类型或者是赋值时,他的类型就是 any
- unknown 类型是不可预先定义的类型,任意类型的值都可以赋值给他,但是他在未指定具体类型之前不能赋值给其他类型(除了 any),在未指定类型之前也不能访问该类型变量上面的属性和方法。建议使用他来代替使用 any
-
any 和 unknown 的共同点
- 可以将任意类型的值转换为 any/unknown
- 也可以将 any/unknown 转换为任意类型的值
- 任意类型的值都可以赋值给 any/unknown
-
访问联合类型的属性和方法的时候,该属性和方法必须是联合类型共有的
-
给联合类型赋值的时候,会根据赋的值推断出该联合类型的具体类型
object、Object、{} 类型:
Object 类型
代表 Object 实例的类型,也就是说该类型的变量可以访问 Object 接口上定义的类型,也就是 Object.prototype 上的类型,而且接受原始类型的值
Object 类的所有实例都继承了 Object 接口中的所有属性。
为什么?Object.prototype 的属性也可以通过原始值访问
-
Object 接口定义了 Object.prototype 原型对象上的属性;
-
ObjectConstructor 接口定义了 Object 类的属性。
// node_modules/typescript/lib/lib.es5.d.ts
interface Object {
constructor: Function;
toString(): string;
toLocaleString(): string;
valueOf(): Object;
hasOwnProperty(v: PropertyKey): boolean;
isPrototypeOf(v: Object): boolean;
propertyIsEnumerable(v: PropertyKey): boolean;
}
// node_modules/typescript/lib/lib.es5.d.ts
interface ObjectConstructor {
/** Invocation via `new` */
new (value?: any): Object;
/** Invocation via function calls */
(value?: any): any;
readonly prototype: Object;
getPrototypeOf(o: any): any;
// ···
}
declare var Object: ObjectConstructor;
object
object 表示非原始类型,只能访问该类型的变量的 Object.prototype 上的属性和方法(通过继承访问的)
可以对 object 类型赋值任意的非原始数据类型,不会进行类型检查,相反 Object 复制的非原始数据类型,如果值对象属性名与 Object 接口中的属性冲突,则 TypeScript 编译器会提示相应的错误
// Type '() => number' is not assignable to type
// '() => string'.
// Type 'number' is not assignable to type 'string'.
const obj1: Object = {
toString() {
return 123;
} // Error
};
const obj2: object = {
toString() {
return 123;
}
};
{} 类型
和 object 类型类似,但是它可以被赋值为原始数据类型。只能访问该类型变量 Object.prototype 上的属性和方法
总结
-
object 类型表示的是非原始数据类型,不接受原始数据类型的值,只能访问 Object 接口上的属性/方法,即 Object.prototype 上的属性/方法
-
{} 没有任何属性的对象,只能访问 Object 接口上的属性/方法,即 Object.prototype 上的属性/方法,但是可以接受原始数据类型的值
-
Object 类型,表示 Object 类的实例。可以接受原始数据类型,可以访问 Object 接口上定义的属性/方法,而且当赋值的时候还会检查类型是否与 Object 接口定义的类型匹配
接口(interface)
用来描述对象的形状
可以用来为对象、数组、函数定义类型
但是常用来定义对象的类型
当为具体接口的变量赋一个字面量的值的时候,属性不能多也不能少,但是如果赋的值是一个变量,那么只要该变量包含接口定义的必选属性即可
特点:
-
可选属性?
-
只读属性 readonly
-
任意属性 [propName: string]: any 任意类型的属性值的类型必须满足已经定义的有具体属性名的属性
-
接口继承 extends
-
声明合并(会检查接口的相同属性名的类型定义是否兼容,不兼容则会报错)
类型别名(type)
给类型起一个别名,和 interface 功能很类似,简述区别
type 和 interface 的异同
相同点
-
都可以描述对象
-
都可以实现类型继承 type 使用 & 符来实现继承;interface 使用 extends 来实现继承。(type 可以继承 interface,interface 可以继承 type)
区别
-
type 可以给重命名原始类型、联合类型、交叉类型、与类型操作符(keyof typeof Exclude 等)配合使用得到新类型
-
相同名称的 interface 可以进行声明合并,重名的 type 则会报错
-
类型别名不会创建一个真正的类型
-
继承时的表现不同
- 扩展接口的时候,TS 会检查类型是否兼容
- 但是交叉类型并不会进行检查,TS 将尽其所能把扩展和被扩展的类型组合在一起,而不会抛出编译时错误。
类型别名和接口应该用哪一个?
建议在可以使用接口的情况下,尽可能使用接口
原因:
-
接口会创建一个单一扁平对象类型来检测属性冲突,当有属性冲突时会提示,而交叉类型只是递归的进行属性合并,在某种情况下可能产生 never 类型
-
在检查一个目标交叉类型时,在检查到目标类型之前会先检查每一个组分
-
交叉类型会认为是两个基本类型的组合,而不是一个新的类型
泛型
泛型就是带有类型参数的类型
这个类型参数我们必须在使用该类型的时候指定或者是在我们的值中使用 TS 才可以为其推断出正确的类型
泛型约束 extends
泛型的默认值
infer
表示在 extends 条件语句中待推断的类型变量。
type ParamType<T> = T extends (...args: infer P) => any ? P : T;
整句表示为:如果 T 能赋值给 (...args: infer P) => any,则结果是 (...args: infer P) => any 类型中的参数 P,否则返回为 T。
类型断言
as / <type>
可以进行类型断言的条件:
- A 类型是 B 类型的子类型或者是 B 类型是 A 类型的子类型,也就是 A、B 类型是兼容的(两者类型是具有包含关系)那么他们可以相互断言
interface A {
a: string;
b: string;
}
interface B {
b: string;
}
const aa: A = { a: "1", b: "2" };
const bb: B = aa as B; // ok
interface A {
a: string;
b: string;
}
interface B {
b: string;
c: string;
}
const aa: A = { a: "1", b: "2" };
const bb: B = aa as B; // no ok A 类型和 B 类型不兼容
- 将联合类型断言为其中的一个类型
类型断言需要谨慎使用,因为可能会导致一些运行时的错误
-
任何类型可以断言为 any/unknown
-
any/unknown 可以断言为任何类型
-
父类可以断言为子类(可能会有运行时错误),子类可以断言为父类
双重断言
anytype as unknown/any as othertype
上面这个万能语句可以帮助我们实现任意类型之间的转换,而且不用管是否兼容,但是很危险 ⚠️,谨慎使用 😂
类型断言 vs 类型转换
类型断言并不会改变变量的类型,只是让跳过 TS 的类型检查
声明合并
-
接口声明可以合并(接口仅仅定义类一个类型)
接口声明合并是相同属性的类型必须要兼容 -
命名空间可以合并(是类型也是值)
- 命名空间和类的合并(类的定义需要在前)
- 命名空间和函数的合并(函数的定义需要在前)
- 命名空间和枚举的合并(顺序不重要)
-
类也可以合并(是类型也是值)
声明文件
-
.ts
/.d.ts
的文件都可以作为声明文件 -
需要注意的是如果我们希望 ts 可以识别到这些声明文件,我们需要在 tsconfig.json 文件的 include/file 选项中配置添加这些文件所在的目录
-
默认所有目录下的@types 下的
.ts
/.d.ts
文件都会被包含进来,我们不需要显示在 include 中定义,前提是我们没有指定 typeRoots 选项 -
模块声明文件
.ts
/.d.ts
中如果包含 import/export 语句的话,里面的声明都是模块级别的,使用的时候需要在使用的文件中进行导入 -
全局声明文件
.ts
/.d.ts
中没有 import/export 语句,那么它定义的类型就是全局可见的 -
建议全局的类型声明文件以
.d.ts
作为后缀,而且推荐使用declare
关键字来声明变量、类、函数、模块、命名空间类型 -
declare
只能用来声明类型,不能给其赋值,常量 declare 必须进行初始化declare const b = 1;
-
三斜线指令 可以通过三斜线指令来导入其他的类型声明文件,而且不会将该文件变成模块声明文件。只能出现在文件的开头
-
decalre global 可以在模块文件中扩展全局声明,在这个块中添加的声明可以在全局访问到
FAQ
- declare module A 和 declare module 'a' 的区别?
前者已经被废弃,使用 declare namespace A 代替;后者用于扩展一个已有的模块 a。全局文件下的 declare module 'xx' 会在全局环境生成一个名为 xx 的模块,并且可以在里面定义这个 xx 模块应该有的导出,一般用来添加或补充 node_modules 中的模块的声明文件。同名的 declare module 里面的导出会合并。
声明空间
变量声明空间
声明的都是变量,不能作为类型使用
类型声明空间
声明的都是类型,不能作为值来使用,例如 interface
命名空间(namespace)
为了防止变量污染,在命名空间内部定义的变量/函数/类,只有 export 出来的才可以被外界所访问。
js 在有模块化之前,我们也会使用全局对象+立即执行函数来避免全局变量的污染
namespace Utility {
export function log(msg) {
console.log(msg);
}
export function error(msg) {
console.log(msg);
}
}
// usage
Utility.log("Call me");
Utility.error("maybe");
编译之后
"use strict";
var Utility;
(function(Utility) {
function log(msg) {
console.log(msg);
}
Utility.log = log;
function error(msg) {
console.log(msg);
}
Utility.error = error;
})(Utility || (Utility = {}));
// usage
Utility.log("Call me");
Utility.error("maybe");
var something;
(function(something) {
something.foo = 123;
})(something || (something = {}));
console.log(something);
// { foo: 123 }
(function(something) {
something.bar = 456;
})(something || (something = {}));
console.log(something); // { foo: 123, bar: 456 }
快速为第三方库或者是模块定义类型
- 当我们想让我们的 css 文件可以被作为类型导入时。
declare module "*.css";
现在你可以使用 import * as foo from './some/file.css'
。
@types
npm install -D @types/jquery
可以通过这个方式来为项目添加 jquery 的类型声明文件,然后我们可以在全局文件中(没有 import/export)使用 $
在模块(有 export/import 的文件)中我们可以通过下面的方式来使用
import * as $ from 'jquery'`
环境声明
环境声明允许你安全的使用现在已有的 JS 库,用于为已经存在的变量、函数等添加类型定义。使用declare
来添加类型定义
lib.d.ts
在我们安装 typescript 库的时候,自动为我们安装了一些声明文件,这些文件为添加了一些 JS/浏览器中全局变量/类型/构造函数的定义
此文件包含了 JavaScript 运行时以及 DOM 中存在各种常见的环境声明。可以让我们书写经过类型检查的 JS 代码。
我们可以在 tsconfig.json 中配置 lib 选项来配置
"compilerOptions": {
"lib": ["dom", "es6"]
}
类型保护
常用来缩小联合类型的范围与 if...else 搭配使用
-
typeof
-
instanceof
-
对象的字面量属性
-
自定义类型保护
// 用户自己定义的类型保护!
function isFoo(arg: Foo | Bar): arg is Foo {
return (arg as Foo).foo !== undefined;
}
- in
辨析联合类型
刚好可以使用这个特性来实现上面的类型保护功能,指的是联合类型中的每个类型都有一个字面量类型的属性,我们可以通过这个属性来区分具体类型
typeof
keyof
属性访问操作符
[]
interface T {
name: string;
}
type Name = T["name"]; // string
映射类型
通过in
操作符实现
type Index = "a" | "b" | "c";
type FromIndex = { [k in Index]?: number };
高级类型
TS 内部实现了一些操作符
Partial
将属性变成可选
type Partial<T> = {
[K in keyof T]?: T[K];
};
Readonly
将属性变为只读
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
Pick
从一个类型中选出几个 k 组成一个新类型
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
类型的兼容性
变体
-
逆变:父类型赋值给子类型
-
协变:子类型赋值给父类型
-
双向协变:包含逆变和协变
-
不变:类型需要完全相同
函数
参数个数
个数少的可以赋值给个数多的
返回值
协变
参数类型
逆变(开启"strictFunctionTypes"),否则是双变的
可选参数和 rest 参数
都是兼容的
let foo = (x: number, y: number) => {};
let bar = (x?: number, y?: number) => {};
let bas = (...args: number[]) => {};
foo = bar = bas;
bas = bar = foo;
可选的(上例子中的 bar)与不可选的(上例子中的 foo)仅在选项为 strictNullChecks 为 false 时兼容。
类
-
子类实例可以赋值给父类
-
父类实例也可以赋值给子类
-
私有的和受保护的成员必须来自于相同的类。
泛型
仅当类型参数在被一个成员使用时,才会影响兼容性
对象
要赋值的变量必须包含被赋值变量的所有属性,可以多,但是不可以少;而且这个检测是深层次的。
如果赋值的是一个字面量,那么这个字面量必须严格按照接口定义的形状实现
协变和逆变
函数的协变和逆变
函数的返回值是协变的,参数是逆变的,在开启 strictFunctionTypes 选项的时候
解释
假如我们有三种类型
Greyhound ≼ Dog ≼ Animal
可以得到:
Animal → Greyhound 是 Dog → Dog 的子类型
返回值是协变的:返回值 Greyhound 类型可以保证我们操作其任何 Dog 类型的属性/方法都是没有问题的,因为我们实际返回的类型是 Greyhound
参数是逆变的:可以保证我们在函数内部操作的任何 Animal 类型上的属性/方法都是可以的,因为我们在实际使用的时候传入的参数是 Dog 类型。Dog → Dog 要求传递的参数必须是 Dog 或者是 Dog 的子类,但是他们都是 Animal 的子类,所以在赋值的函数的内部其实使用的参数都是 Animal 类型且传递的参数必须是 Dog 或者是 Dog 的子类,所以他们一定包含 Animal 类型需要的值。
网友评论