本文为本人学习 TS 时阅读资料整理的笔记,若有不完善或者不准确的部分,还请谅解。
入门
进阶
TS 是一种静态类型语言,有这几方面的优点
也有一些缺点
基本语法是,类型后置,这种类型声明方式在 rust、kotlin 和 python3 等语言中都有使用
tslet foo:string
如果没有声明类型,ts 会自动声明类型
tslet foo = 123; // 这个时候foo已经被推断为了number类型
foo = 'hello'; // 报错
ts 也可以推断函数的返回值
tsfunction toString(num:number) {
return String(num);
}
ts 在这里的设计思想是,类型声明是可选的,即使不加类型声明,也会自动推断变量或者函数的类型
any
类型
any
类型表示没有任何限制,该类型的变量可以赋予任意类型的值any
的使用场景
any
js
项目,先设置为 any
,再一步一步迁移到 ts
any
类型可以看作其他类型的全集,ts 将其叫做"顶层类型"any
类型any
类型,可以开启 noImplicitAny
(不能存在隐式 any
)unknown
类型
unknown
跟 any
的相似之处,在于所有类型的值都可以分配给 unknown
类型unknown
类型跟 any
类型的不同之处在于,它不能直接使用,主要有以下几个限制
unknown
类型的变量,不能直接赋值给其他类型的变量(除了 any
类型和 unknown
类型)unknown
类型变量的方法和属性unknown
类型变量能够进行的运算是有限的,只能进行比较运算(运算符 ==
、===
、!=
、!==
、||
、&&
、?
)、取反运算(运算符 !
)、typeof
运算符和 instanceof
运算符这几种,其他运算都会报错unknown
类型的使用需要经过"类型缩小",缩小 unknown
变量的类型范围,确保不会出错tslet a: unknown = 1;
// 缩小为 number 类型
if (typeof a === "number") {
a.toFixed(1);
}
// 缩小为 string 类型
if (typeof a === "string") {
a.trim();
}
unknown
也可以视为所有其他类型(除了 any
)的全集,所以它和 any
一样,也属于 TypeScript 的顶层类型never
类型
never
类型)never
never
可以赋值给任意类型,因为在集合论中,never
为空集,空集是所有集合的子集,所以可以赋值never
是一个底层类型ts 中的基本类型和 js 的基本类型是一样的
boolean
包含 true
和 false
string
字符串number
数字类型,包括整数和浮点数bigint
大整数symbol
object
包括对象、数组、函数undefined
、null
,各自代表一种类型
noImplicitAny
和 strictNullChecks
的时候,如果变量设置为 undefined
或者 null
,变量类型为 any
,因此需要开启包装类型
js 的8种类型,undefined
和 null
是两个特殊值,object
属于复合类型,剩下的5种属于原始类型,代表最基本的,不可再分的值
这5种基本类型,又有对应的包装类型,而 Symbol
和 bigint
不能作为构造函数使用,也就没有对应的包装类型,所以 ts 中基本类型的包装类型包括以下
Boolean
String
Number
包装类型和字面量类型 TypeScript 对五种原始类型分别提供了大写和小写两种类型。
注意:symbol 和 bigint 的大小写没有区别,原因上面解释了 (不能作为构造函数使用)
Object
和 object
大写 Object
Object
类型代表 JavaScript 语言里面的广义对象。所有可以转成对象的值,都是 Object
类型。undefined
和 null
这两个值不能转为对象,其他任何值都可以赋值给 Object
类型{}
是 Object
类型的简写形式小写 object
object
类型代表 JavaScript 里面的狭义对象,即可以用字面量表示的对象,只包含对象、数组和函数,不包括原始类型的值undefined
和 null
undefined
和 null
既是值,又是类型。
undefined
或 null
。但是可能导致无法取到变量上的一些属性和方法。strictNullChecks
选项,让 undefined
和 null
不能赋值给其他类型变量,防止报错TypeScript 规定,单个值也是一种类型,称为“值类型”。
TypeScript 推断类型时,遇到 const
命令声明的变量,如果代码里面没有注明类型,就会推断该变量是值类型。
ts// x 的类型是 "https"
const x = 'https';
只包含单个值的值类型,用处不大。实际开发中,往往将多个值结合,作为联合类型使用。
联合类型:一个值可以是多种类型中的任意一种(“或”的关系)
联合类型(union types)指的是多个类型组成的一个新类型,使用符号 |
表示。
联合类型 A|B
表示,任何一个类型只要属于 A
或 B
,就属于联合类型 A|B
。
“类型缩小”是 TypeScript 处理联合类型的标准方法,凡是遇到可能为多种类型的场合,都需要先缩小类型,再进行处理。实际上,联合类型本身可以看成是一种“类型放大”(type widening),处理时就需要“类型缩小”(type narrowing)。
交叉类型:一个值必须同时满足所有类型(“且”的关系),拥有所有成员
交叉类型(intersection types)指的多个类型组成的一个新类型,使用符号 &
表示。
交叉类型 A&B
表示,任何一个类型必须同时属于 A
和 B
,才属于交叉类型 A&B
,即交叉类型同时满足 A
和 B
的特征。
交叉类型的主要用途是表示对象的合成。
tslet arr: number[] = [1,2,3]
tslet arr: Array<number> = [1,2,3]
声明方式:
readonly
关键字tsconst arr: readonly number[] = [0, 1]
tsconst arr: Readonly<number[]> = [0, 1]
const arr: ReadonlyArray<number[]> = [0, 1]
const
断言tsconst arr = [0, 1] as const;
它表示成员类型可以自由设置的数组,即数组的各个成员的类型可以不同。
比如:
tsconst s:[string, string, boolean] = ['a', 'b', true];
元组有以下语法:
?
),表示该成员是可选的。注意,问号只能用于元组的尾部成员,也就是说,所有可选成员必须在必选成员之后。
tslet a:[number, number?] = [1];
...
拓展运算符,可以创建表示无限成员数量的元组扩展运算符(
...
)用在元组的任意位置都可以,它的后面只能是一个数组或元组
tstype t1 = [string, number, ...boolean[]];
type t2 = [string, ...boolean[], number];
type t3 = [...boolean[], string, number];
tstype Color = [
red: number,
green: number,
blue: number
];
const c:Color = [255, 255, 255];
number
,所以可以像下面这样读取。tstype Tuple = [string, number];
type Age = Tuple[1]; // number
type Tuple = [string, number, Date];
type TupleEl = Tuple[number]; // string|number|Date
只读元组,语法和只读数组一样,这里不重复了
成员数量的推断
正常使用元组可以推断出成员数量,但是如果使用了可选成员或者拓展运算符,就获取不到成员数量,会爆 ts 错误
ts 中对象包括以下常用语法
tstype User = {
firstName: string;
lastName?: string;
};
// 等同于
type User = {
firstName: string;
lastName: string|undefined;
};
使用方式
ts// 写法一
let firstName = (user.firstName === undefined)
? 'Foo' : user.firstName;
let lastName = (user.lastName === undefined)
? 'Bar' : user.lastName;
// 写法二
let firstName = user.firstName ?? 'Foo';
let lastName = user.lastName ?? 'Bar';
只读属性,属性名前面加上 readonly
关键字,表示这个属性是只读属性,不能修改。
属性名索引,除了 string
,还可以是 number
、symbol
tstype MyObj = {
[property: string]: string
};
解构赋值声明类型,只能给整体声明类型,不能给单个变量指定类型
“结构类型“原则,只要对象 B 满足对象 A 的结构特征,TypeScript 就认为对象 B 兼容对象 A 的类型,这称为“结构类型”原则。比较抽象,反正大概就是下面这样的代码
tsconst B = {
x: 1,
y: 1
};
const A:{ x: number } = {
x: 1,
y: 1 // 字面量方式声明报错
}
const A: {x: number } = B // 变量方式赋值不报错
tsconst point:{
x:number;
y:number;
} = {
x: 1,
y: 1,
z: 1 // 报错
};
ts 函数类型的两种声明方式
tsfunction add(x: number, y: number): number {
return x + y
}
ts// 方式1
const add = (x: number, y: number): number => {
return x + y
}
// 方式2
const add: (x: number, y: number) => number = (x, y) => {
return x + y
}
// 方式3
type Add = (x: number, y: number) => number
const add: Add = (x, y) => {
return x + y
}
函数也可以使用 typeof
关键字获取类型
tsfunction add(x: number, y: number): number {
return x + y
}
const add2: typeof add = (x, y) => {
return x + y
}
可选参数
tsfunction f(x?:number) {
// ...
}
f() // 正确
function f(x:number|undefined) {
return x;
}
f() // 错误 如果是这样定义,需要传入undefined
参数默认值,设置了默认值的参数,就是可选的。如果不传入该参数,它就会等于默认值。
tsfunction createPoint(
x:number = 0,
y:number = 0
):[number, number] {
return [x, y];
}
createPoint() // [0, 0]
参数解构,写法如下
tsfunction f(
[x, y]: [number, number]
) {
// ...
}
function sum(
{ a, b, c }: {
a: number;
b: number;
c: number
}
) {
console.log(a + b + c);
}
// 默认值写法
function add({ x = 10, y = 10 }: { x?: number, y?: number }): number {
return x + y
}
console.log(add({}))
rest 参数,rest 参数表示函数剩余的所有参数,它可以是数组(剩余参数类型相同),也可能是元组(剩余参数类型不同)。
ts// rest 参数为数组
function joinNumbers(...nums:number[]) {
// ...
}
// rest 参数为元组
function f(...args:[boolean, number]) {
// ...
}
// 最后一个参数可选
function f(
...args: [boolean, string?]
) {}
// 利用灵活的元组的rest参数
function f(...args:[boolean, ...string[]]) {
// ...
}
f(false, '1', '2', '4')
readonly
只读参数
tsfunction arraySum(
arr:readonly number[]
) {
// ...
arr[0] = 0; // 报错
}
void
类型,void
类型表示函数没有返回值,并不是说函数不返回值,而是说返回值不重要
tstype voidFunc = () => void;
const f:voidFunc = () => {
return 123; // 不报错
};
function f():void {
return true; // 报错
}
const f3 = function ():void {
return true; // 报错
};
never
类型,表示肯定不会出现的值,比如函数的返回值,包括
never
类型不同于 void
类型。前者表示函数没有执行结束,不可能有返回值;后者表示函数正常执行结束,但是不返回值,或者说返回 undefined
高阶函数,一个函数的返回值还是一个函数,那么前一个函数就称为高阶函数(higher-order function)
函数重载,有些函数可以接受不同类型或不同个数的参数,并且根据参数的不同,会有不同的函数行为
tsfunction reverse(str:string):string;
function reverse(arr:any[]):any[];
function reverse(
stringOrArray:string|any[]
):string|any[] {
if (typeof stringOrArray === 'string')
return stringOrArray.split('').reverse().join('');
else
return stringOrArray.slice().reverse();
}
构造函数的类型
new
命令Date()
,可以两种函数的类型定义可以写在对象里tsclass Foo {
b: string;
}
// 构造函数的类型
type FooContructor = new () => Foo;
// 构造函数类型的对象写法
type FooContructor = {
// 构造函数的定义
new (): Foo;
// 普通函数的定义
(): string;
};
function create(c: FooContructor): Foo {
return new c();
}
const foo = create(Foo);
Enum
结构,用来将相关常量放在一个容器里面,方便使用,默认如果不赋初始值,枚举值将依次增加,比如
tsenum Color {
Red, // 0
Green, // 1
Blue // 2
}
Enum
结构本身也是一种类型。比如,上例的变量 c
等于 1
,它的类型可以是 Color,也可以是 number
。
tslet c:Color = Color.Green; // 正确
let c:number = Color.Green; // 正确
Enum
编译之后会变成 JS 代码,这是一个很特别的点,因为 TS 的定位是 JS 语言的类型增强,所以谨慎使用 Enum
结构,很大程度上,Enum
可以被对象的 as const
断言替代,它也可以作为类型
tsenum Foo {
A,
B,
C,
}
const Bar = {
A: 0,
B: 1,
C: 2,
} as const;
// 对象的as const 断言写法也可以作为类型,即使用 typeof
const Bar = {
A: 0,
B: 1,
C: 2,
} as const;
const a: typeof Bar = {
A: 0,
}
Enum
之前也可以加上
注意:TypeScript 5.0 之前,Enum
有一个 Bug,就是 Enum
类型的变量可以赋值为任何数值。
tsenum Bool {
No,
Yes
}
function foo(noYes:Bool) {
// ...
}
foo(33); // TypeScript 5.0 之前不报错
enum 支持字符串和数字两种,数字可以是除了 bigint 之外的所有数字
对于数字类型的枚举来说,enum
支持反向映射 (即反向查找),而字符串类型不行
tsenum A {
white=4
}
console.log(A[4]) // "white"
多个同名的 Enum 结构会自动合并 作用:
即使用数字类型的 enum
,不仅可以通过键找到值,还可以通过值找到键
tsenum A {
Red,
Blue,
Green,
}
console.log(A.Red) // 0
console.log(A[0]); // Red
enum
本质是将枚举编译成了一个对象
比如如下代码
tsenum A {
Red,
Blue=2,
Green
}
console.log(A)
// 打印结果为:
/**
{
"0": "Red",
"2": "Blue",
"3": "Green",
"Red": 0,
"Blue": 2,
"Green": 3
}
*/
enum
和 const enum
的编译区别是
ts// ts代码
const enum A {
Red,
Blue,
Green,
}
console.log(A.Red)
console.log(A.Blue)
console.log(A.Green)
// enum编译结果
var A;
(function (A) {
A[A["Red"] = 0] = "Red";
A[A["Blue"] = 1] = "Blue";
A[A["Green"] = 2] = "Green";
})(A || (A = {}));
console.log(A.Red);
console.log(A.Blue);
console.log(A.Green);
// const enum 编译结果
console.log(0 /* A.Red */);
console.log(1 /* A.Blue */);
console.log(2 /* A.Green */);
其中 enum
的编译结果咋一眼看过去看不懂,实际上可以分开分析
tsvar A // 定义了 和enum名称一样的全局变量
// 立即执行函数,对A赋值
(function (A) {
...
})(A || (A = {})) // 如果A有值传入A(用于枚举合并的场景),否则传入兜底的空对象
// 对A键的赋值
A[A["Red"] = 0] = "Red";
// 分为两步分析
// 1. 包括 A["Red"] = 0 所以字符串类型的键名就获得了键值
// 2. 使用键值再进行赋值,实现反向映射,A[A["Red"] = 0] = "Red",相当于A[0] = "Red"
// 而如果是字符串作为键值,只有这一层,没有反向映射
A["Red"] = "red";
interface
语法,它成员有5种形式
tsinterface Point {
x: number;
y: number;
}
tsinterface A {
[prop: string]: number;
}
ts// 写法一
interface A {
f(x: boolean): string;
}
// 写法二
interface B {
f: (x: boolean) => string;
}
// 写法三
interface C {
f: { (x: boolean): string };
}
// 方法重载
interface A {
f(): number;
f(x: boolean): boolean;
f(x: string, y: string): string;
}
interface
也可以用于声明独立的函数tsinterface Add {
(x:number, y:number): number;
}
const myAdd:Add = (x,y) => x + y;
tsinterface ErrorConstructor {
new (message?: string): Error;
}
interface
可以继承其他类型,主要有以下几种情况
语法
extends
关键字会从继承的接口里面拷贝属性类型,这样就不必书写重复的属性。interface
允许多重继承。tsinterface Style {
color: string;
}
interface Shape {
name: string;
}
interface Circle extends Style, Shape {
radius: number;
}
interface
可以继承type
命令定义的对象类型
interface
还可以继承 class
,即继承该类的所有成员
多个同名接口会合并成一个接口
tsinterface Box {
height: number;
width: number;
}
interface Box {
length: number;
}
// 两个`Box`接口会合并成一个接口,同时有`height`、`width`和`length`三个属性。
interface 与 type 的区别有下面几点。
(1)type
能够表示非对象类型,而interface
只能表示对象类型(包括数组、函数等)。
(2)interface
可以继承其他类型,type
使用交叉类型实现继承,但是没有 extends
语法。
(3)同名 interface
会自动合并,同名 type
则会报错。也就是说,TypeScript 不允许使用 type
多次定义同一个类型
(4)interface
只能使用索引签名,type
可以使用索引签名和更灵活的映射类型
(5)this
关键字只能用于 interface
(6)type
可以扩展原始数据类型,interface
不行。
(7)interface
无法表达某些复杂类型(比如交叉类型和联合类型),但是 type
可以。
综上所述,如果有复杂的类型运算,那么没有其他选择只能使用 type
;一般情况下,interface
灵活性比较高,便于扩充类型或自动合并,建议优先使用。
class
待补充
泛型(Generics)是 TypeScript 提供的一种工具,允许在定义函数、接口、类时不预先指定具体类型,而是在使用时再指定类型参数,从而实现类型的灵活复用。
优势:
ts 的泛型可以写在:函数、接口、类、类型别名上
ts// 写法一
function fn1<T>(args: T) {
}
// 写法二
let fn2 = <T>(args: T) => {}
tsinterface Box<T> {
prop: T
}
let a: Box<string>
比如 Array
的泛型写法
tsconst arr: Array<number> = [1,2,3]
接口的继承有一个示例,就是 compareTo
接口
tsinterface Comparator<T> {
compareTo(value:T): number;
}
class Rectangle implements Comparator<Rectangle> {
compareTo(value:Rectangle): number {
// ...
}
}
这个和接口的写法差不多
tsclass Pair <K, V> {
key: K;
value: V;
}
注意,泛型类描述的是类的实例,不包括静态属性和静态方法,因为这两者定义在类的本身。因此,它们不能引用类型参数。
tsclass C<T> {
static data: T; // 报错
constructor(public value:T) {}
}
比如:本质还是和接口的泛型写法差不多
tstype Tree<T> = {
value: T;
left: Tree<T> | null;
right: Tree<T> | null;
};
如果没有给 <T>
写类型,就会使用默认类型,但是默认类型会进行类型推导,实际参数类型会覆盖默认类型
tsfunction fn<T = string>(args: T[]): T {
return args[0]
}
const r1 = fn(123) // 报错
const r2 = fn([123]) // 不报错,因为进行了类型推导,使用了实际类型
一旦有了默认值,就表示为可选参数,多个参数时,可选参数需要在必选参数之后
ts<T = boolean, U> // 错误
<T, U = boolean> // 正确
比如,传入的参数必须满足有 length
属性,才能比较,否则会报错
tsfunction comp<T extends { length: number }>(
a: T,
b: T
) {
if (a.length >= b.length) {
return a;
}
return b;
}
泛型的类型参数可以同时设置约束和默认参数
tstype Fn<A extends string, B extends string = "world"> = (a: A, b: B) => void;
const fn: Fn<"hello"> = (a, b) => {};
fn("hello", "world");
如果有多个类型参数,一个类型参数的约束条件,可以引用其他参数,但是不能引用自身
ts<T, U extends T>
// 或者
<T extends U, U>
这个可以比较好的一个例子是,手写 ts 工具函数
tstype CustomPick<T, K extends keyof T> = {
[P in K]: T[K];
};
(1)尽量少用泛型。
泛型虽然灵活,但是会加大代码的复杂性,使其变得难读难写。一般来说,只要使用了泛型,类型声明通常都不太易读,容易写得很复杂。因此,可以不用泛型就不要用。
(2)类型参数越少越好。
多一个类型参数,多一道替换步骤,加大复杂性。因此,类型参数越少越好。
(3)类型参数至少需要出现两次。
如果类型参数在定义后只出现一次,那么很可能是不必要的。
(4)泛型可以嵌套。
类型参数可以是另一个泛型。
ts 支持让模块导出类型,比如
ts// 第一种写法
export type Bool = true | false;
// 第二种写法
type Bool = true | false;
export { Bool };
在 ts 模块中,可以同时输入类型和正常接口,但是不利于区分类型和正常接口 解决方法有:
type
关键字tsimport { type A, a } from './a';
ts// 可以输入类型
import type { A } from './a';
let b:A = 'hello';
// 输入正常接口会报错
import type { a } from './a';
let b = a;
同样的,export
语句也有两种方法,表示输出的是类型
tstype A = 'a';
type B = 'b';
// 方法一
export {type A, type B};
// 方法二
export type {A, B};
可以使用 importsNotUsedAsValues
编译选项
remove
:这是默认值,自动删除输入类型的 import 语句。preserve
:保留输入类型的 import 语句。error
:保留输入类型的 import 语句(与 preserve
相同),但是必须写成 import type
的形式,否则报错。ts 使用 import =
语句输入 commonjs
模块
import =
和常见的 const =
是一样的
tsimport fs = require('fs');
const code = fs.readFileSync('hello.ts', 'utf8');
同时还兼容 import * as from
语法
tsimport * as fs from 'fs'
// 等同于
import fs = require('fs')
TypeScript 使用 export =
语句,输出 CommonJS 模块的对象,等同于 CommonJS 的 module.exports
对象
使用
export =
语句输出的 commonjs 模块,只能使用import =
导入
tslet obj = { foo: 123 };
export = obj;
模块定位(module resolution)指的是一种算法,用来确定 import 语句和 export 语句里面的模块文件位置。
默认值
node
: module
为 commonjs
时,moduleResolution
默认值为 node
,即采用 Node.js 的模块定位算法classic
: module
设为 es2015、 esnext、amd, system, umd 等等时,采用 Classic
定位算法bundler
: 写相对路径时,不用补充拓展名nodenext
、node16
这一段可以直接看文档:
TypeScript 模块 - TypeScript 教程 - 网道
加载模块时,目标模块分为相对模块(relative import)和非相对模块两种(non-relative import)
/
、./
、../
开头的模块非相对模块的定位,是由baseUrl
属性或模块映射而确定的,通常用于加载外部模块
TypeScript 模块 - TypeScript 教程 - 网道
Node 方法就是模拟 Node. js 的模块加载方法,也就是 require()
的实现方法
TypeScript 模块 - TypeScript 教程 - 网道
baseUrl
: 可以手动指定脚本模块的基准目录paths
: 指定非相对路径的模块与实际脚本的映射,常见的用来设置 @
、@components
等路径的映射rootDirs
: 指定模块定位时必须查找的其他目录TypeScript 模块 - TypeScript 教程 - 网道
装饰器(Decorator)是一种语法结构,用来在定义时修改类(class)的行为。
在语法上,装饰器有如下几个特征。
(1)第一个字符(或者说前缀)是@
,后面是一个表达式。
(2)@
后面的表达式,必须是一个函数(或者执行后可以得到一个函数)。
(3)这个函数接受所修饰对象的一些相关值作为参数。
(4)这个函数要么不返回值,要么返回一个新对象取代所修饰的目标对象。
装饰器分为
装饰器是一个函数,使用时接收两个参数
value
:所装饰的对象context
:上下文对象,根据所装饰对象的不同而不同,其中只有两个属性(kind``和name
)是必有的,其他都是可选的。
kind
:字符串,表示所装饰对象的类型,可能取以下的值name
:字符串或者 Symbol 值,所装饰对象的名字,比如类名、属性名等。addInitializer()
:函数,用来添加类的初始化逻辑。private
:布尔值,表示所装饰的对象是否为类的私有成员。static
:布尔值,表示所装饰的对象是否为类的静态成员。access
:一个对象,包含了某个值的 get 和 set 方法。装饰器的执行分为两个阶段。
(1)评估(evaluation):计算@
符号后面的表达式的值,得到的应该是函数。
(2)应用(application):将评估装饰器后得到的函数,应用于所装饰对象。
TypeScript 装饰器 - TypeScript 教程 - 网道
declare
关键字用来告诉编译器,某个类型是存在的,可以在当前文件中使用
declare
可以描述以下类型
global
全局感觉一般常用与,全局声明的变量、函数、类等等,比如:document
、或者其他引入的库
declare 关键字 - TypeScript 教程 - 网道
单独使用的模块,一般会同时提供一个单独的类型声明文件(declaration file),把本模块的外部接口的所有类型都写在这个文件里面,便于模块使用者了解接口,也便于编译器检查使用者的用法是否正确
类型声明文件也可以包括在项目的 tsconfig.json
文件里面,这样的话,编译器打包项目时,会自动将类型声明文件加入编译,而不必在每个脚本里面加载类型声明文件
ts{
"compilerOptions": {},
"files": [ // 或者使用include
"src/index.ts",
"typings/moment.d.ts"
]
}
类型声明文件主要有以下三种来源。
只要使用编译选项 declaration
,编译器就会在编译时自动生成单独的类型声明文件。
之后再补充
keyof
是一个单目运算符,接受一个对象类型作为参数,返回该对象的所有键名组成的联合类型。
注意:由于 JavaScript 对象的键名只有三种类型,所以对于任意对象的键名的联合类型就是
string|number|symbol
。
ts// string | number | symbol
type KeyT = keyof any;
TypeScript 中的 typeof
运算符接受一个值作为参数,返回该值的 TypeScript 类型
JavaScript 的 typeof 遵守 JavaScript 规则,TypeScript 的 typeof 遵守 TypeScript 规则。它们的一个重要区别在于,编译后,前者会保留,后者会被全部删除。
可以在 https://www.typescriptlang.org/play/ 尝试一下
TypeScript 中的 in
运算符用来取出联合类型的每一个成员类型
tstype U = 'a'|'b'|'c';
type Foo = {
[Prop in U]: number;
};
// 等同于
type Foo = {
a: number,
b: number,
c: number
};
一个灵活用法,比如手写 Optional、Pick 类型工具
tstype Optional<T> = {
[P in keyof T]?: T[P];
};
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
方括号运算符([]
)用于取出对象的键值类型,比如 T[K]
会返回对象T的属性K的类型。
tstype Person = {
age: number;
name: string;
alive: boolean;
};
// number|string
type T = Person['age'|'name'];
// number|string|boolean
type A = Person[keyof Person];
上面的手写工具函数中,就使用到了方括号运算符
注意:方括号中不能有值的运算
条件运算符 extends...?:
可以根据当前类型是否符合某种条件,返回不同的类型。
tsT extends U ? X : Y
比如
ts// true
type T = 1 extends number ? true : false;
infer
关键字用来定义泛型里面推断出来的类型参数,而不是外部传入的类型参数。
它通常跟条件运算符一起使用,用在 extends
关键字后面的父类型之中。
比如
tstype Flatten<Type> =
Type extends Array<infer Item> ? Item : Type;
// string
type Str = Flatten<string[]>;
// number
type Num = Flatten<number>;
is
运算符用于描述函数的返回值类型,写法采用parameterName is Type
的形式,即左侧为当前函数的参数名,右侧为某一种类型。它返回一个布尔值,表示左侧参数是否属于右侧的类型。
函数返回布尔值的时候,可以使用is
运算符,限定返回值与参数之间的关系。
is
运算符用来描述返回值属于 true
还是 false
tsfunction isFish(
pet: Fish|Bird
):pet is Fish { // 这里使用 is 运算符,当pet 是Fish 的时候类型是true
return (pet as Fish).swim !== undefined;
}
satisfies
运算符用来检测某个值是否符合指定类型
看文档这个例子即可: TypeScript 类型运算符 - TypeScript 教程 - 网道
TypeScript 类型守卫(或者叫类型保护)(Type Guards)是一种在运行时检查变量类型的机制,它可以帮助 TypeScript 在特定的代码块中缩小变量的类型范围
类型守卫包括如下几种
typeof
类型守卫ts// 1. typeof 类型守卫
function printValue(value: string | number | boolean): void {
if (typeof value === "string") {
console.log(`字符串: ${value.toUpperCase()}`);
} else if (typeof value === "number") {
console.log(`数字: ${value.toFixed(2)}`);
} else {
console.log(`布尔值: ${value ? "真" : "假"}`);
}
}
instanceof
类型守卫ts// 2. instanceof 类型守卫
class Animal { constructor(public name: string) {} }
class Dog extends Animal { bark() { console.log("汪汪!"); } }
class Cat extends Animal { meow() { console.log("喵!"); } }
function makeSound(animal: Animal): void {
if (animal instanceof Dog) animal.bark();
else if (animal instanceof Cat) animal.meow();
}
ts// 3. 自定义类型守卫
interface Bird { fly(): void; name: string; }
interface Fish { swim(): void; name: string; }
function isFish(pet: Bird | Fish): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function move(pet: Bird | Fish): void {
if (isFish(pet)) pet.swim();
else pet.fly();
}
in
操作符类型守卫ts// 4. in 操作符类型守卫
interface Square { kind: 'square'; size: number; }
interface Rectangle { kind: 'rectangle'; width: number; height: number; }
function calculateArea(shape: Square | Rectangle): number {
return 'size' in shape ? shape.size * shape.size : shape.width * shape.height;
}
ts// 5. 字面量类型守卫
type Direction = 'north' | 'south' | 'east' | 'west';
function move2(direction: Direction): void {
switch (direction) {
case 'north': console.log('向北移动'); break;
case 'south': console.log('向南移动'); break;
case 'east': console.log('向东移动'); break;
case 'west': console.log('向西移动'); break;
default:
const exhaustiveCheck: never = direction;
throw new Error(`未处理的方向: ${exhaustiveCheck}`);
}
}
好处
类型谓词(Type Predicates)是 TypeScript 中的一种特殊返回类型注解,它用于自定义类型守卫函数
类型谓词的形式为 parameterName is Type
,其中 parameterName
必须是当前函数的参数名
比如下面代码
ts// 自定义类型守卫处理可能为null/undefined的值
function isNonNullish<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined;
}
// 应用示例
const values = ['a', 'b', null, 'c', undefined];
const nonNullValues = values.filter(isNonNullish); // 类型为 string[]
注意:ts 中的类型转换概念一般可用指类型断言,只有在 js 中才有类型转换概念
TypeScript 提供了“类型断言”这样一种手段,允许开发者在代码中“断言”某个值的类型,告诉编译器此处的值是什么类型。
TypeScript 一旦发现存在类型断言,就不再对该值进行类型推断,而是直接采用断言给出的类型。(断言不是万能的,它会先检查断言类型是否能够兼容,不能兼容的话会报错)
比如
tsinterface Duck {
name: string;
age: number;
city: string;
}
interface Bowl {
price: number;
}
const bowl: Bowl = {
price: 100,
};
// Error 类型错误,Bowl 和 Duck 是完全不能兼容的,这种情况不允许变型,同时也不能断言
const duck: Duck = bowl as Duck;
类型兼容性用于确定一个类型是否能赋值给其他类型,ts 中判断类型是否能够兼容,会通过四种方式判断,分别是
子类型
参考集合论中,父集和子集的关系即可,父类型属性更少,属于子集;子类型属性更多,属于父集。
注意:父类型相当于 子集 而不是父集,因为他的属性更少
可赋值性
范围更大的类型可以赋值给范围更小的类型,也就是子类型可以赋值给父类型,而父类型不能赋值给子类型
实际应用
ts// 比如使用泛型的场景,extends约束了,T必须包含a
function fn<T extends { a: string }>(args: T) {
return args.a;
}
fn({ a: "123", b: 123 });
fn({ b: 123 }); // 报错
注意:这里的
T
是类型构造器,比如这里的普通类型定义
如果 A 是 B 的子类型,那么 T<A>
也是 T<B>
的子类型,这种情况就叫做协变
TypeScript 中数组和对象的属性是协变的
ts// 假设已经定义了 Dog 和 Animal,这个在很多教程中都是一样的
// 数组的协变
let animal: Animal[] = []
let dog: Dog[] = []
animal = dog // 协变,animal 可以兼容 dog
// 对象的协变
let obj1 = {
prop: Dog // 仅用来表示类型
}
let obj2 = {
prop: Animal
}
obj2 = obj1
注意:这里的
T
是类型构造器,比如这里的函数类型定义
如果 A 是 B 的子类型,那么 T<B>
反而是 T<A>
的子类型,这种情况就叫做逆变
TypeScript 中函数参数是逆变的
ts// 逆变
// T<Animal>
var getAnimal = function (animal: Animal): void {
console.log(animal.name);
};
// T<Dog>
var getDog = function (dog: Dog): void {
console.log(dog.name);
};
getAnimal = getDog; // X
getDog = getAnimal; // 这里类型逆变了
在 TypeScript 中,由于灵活性等权衡,对于函数参数默认的处理是 双向协变
的。也就是既可以 visitAnimal = visitDog
,也可以 visitDog = visitAnimal
。在开启了 tsconfig
中的 strictFunctionType
后才会严格按照 逆变
来约束赋值关系。
不变就是不允许变型。如果两个类型完全不相同,它们是不能兼容的。
tsinterface Duck {
name: string;
age: number;
city: string;
}
interface Bowl {
price: number;
}
const bowl: Bowl = {
price: 100,
};
// Error 类型错误,Bowl 和 Duck 是完全不能兼容的,这种情况不允许变型
const duck: Duck = bowl;
参考资料
索引签名可以让对象可以具有任意数量的属性,只要属性名和值符合指定的类型即可
在 TypeScript 中,我们可以使用
string
、number
和symbol
作为索引签名
比如:
tstype K = {
[name: string | number]: string;
};
const k: K = { x: 'x', 1: 'b' };
类型映射是指基于一个现有类型创建新类型,通过遍历现有类型的属性来转换它们
比如:
ts// 类型映射示例
interface Person {
name: string;
age: number;
}
// 将所有属性变为可选
type PartialPerson = {
[K in keyof Person]?: Person[K];
}
// 将所有属性变为只读
type ReadonlyPerson = {
readonly [K in keyof Person]: Person[K];
}
TypeScript 引入了两个映射修饰符,用来在映射时添加或移除某个属性的 ?
修饰符和 readonly
修饰符
+
修饰符:写成+?
或+readonly
,为映射属性添加?
修饰符或readonly
修饰符。–
修饰符:写成 -?
或 -readonly
,为映射属性移除 ?
修饰符或 readonly
修饰符。比如
ts// 添加可选属性
type Optional<Type> = {
[Prop in keyof Type]+?: Type[Prop];
};
// 移除可选属性
type Concrete<Type> = {
[Prop in keyof Type]-?: Type[Prop];
};
ts// 添加 readonly
type CreateImmutable<Type> = {
+readonly [Prop in keyof Type]: Type[Prop];
};
// 移除 readonly
type CreateMutable<Type> = {
-readonly [Prop in keyof Type]: Type[Prop];
};
TypeScript 4.1 引入了键名重映射,允许改变键名。 语法为
ts{
[P in K as 新类型] // as 可以修改映射的键名
}
实际的应用比如:定义getter/setter 函数
tsinterface Person {
name: string;
age: number;
location: string;
}
type Getters<T> = {
[P in keyof T
as `get${Capitalize<string & P>}`]: () => T[P];
};
type LazyPerson = Getters<Person>;
// 等同于
type LazyPerson = {
getName: () => string;
getAge: () => number;
getLocation: () => string;
}
比如,可以 as
写一个 Filter
工具
tstype Filter<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P];
};
type A = {
a: string,
b: number,
c: boolean
}
/**
* {
* c: boolean
* }
*/
type FA = Filter<A, 'a' | 'b'>
Awaited<Type>
用来取出 Promise
的返回值类型,适合用在描述 then()
方法和 await
命令的参数类型。
ts// string
type A = Awaited<Promise<string>>;
Exclude
用来从联合类型里,删除某些类型,组成一个新的类型返回
tstype T1 = Exclude<'a'|'b'|'c', 'a'>; // 'b'|'c'
从联合类型中删除 null
和 undefined
类型返回,暂时没看出用处
tstype A = NonNullable<string | null | undefined>; // string
InstanceType<Type>
提取构造函数的返回值的类型(即实例类型),参数 Type
是一个构造函数,等同于构造函数的 ReturnType<Type>
。
从对象类型中删除一些键
让对象类型所有键变为可选
从对象类型中选出一些键作为新的类型
将所有属性转为只读属性
Record
返回一个对象类型
用法如下
ts// 1.
// { a: number }
type T = Record<'a', number>;
// 2.
// { a: number, b: number }
type T = Record<'a'|'b', number>;
// 3.
// { a: number|string }
type T = Record<'a', number|string>;
Parameters<Type>
从函数类型 Type
里面提取参数类型,组成一个元组返回。
ReturnType<Type>
提取函数类型 Type
的返回值类型,作为一个新类型返回。
// @ts-nocheck
告诉编译器不对当前脚本进行类型检查,可以用于 TypeScript 脚本,也可以用于 JavaScript 脚本。// @ts-check
,那么编译器将对该脚本进行类型检查,不论是否启用了 checkJs
编译选项。// @ts-ignore
告诉编译器不对下一行代码进行类型检查,可以用于 TypeScript 脚本,也可以用于 JavaScript 脚本。// @ts-expect-error
主要用在测试用例,当下一行有类型错误时,它会压制 TypeScript 的报错信息(即不显示报错信息),把错误留给代码自己处理。如果下一行没有报错,则会显示 Unused '@ts-expect-error' directive.
报错TODO
TODO
本文作者:pepedd864
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!