TypeScript现在的普及度已经很高了,虽然它是一种静态类型定义,但它的庞大已足够我们要向对待一门语言一样对待它了,去深入学习,以便更好的利用它的能力。
当然了,我们首先要明确为什么需要它,而不是把它当作一种负担:
- 编译时类型检查,避免了运行时才检查的数据类型错误,导致系统奔溃。
- 提升开发效率,IDE基于类型系统提供了精准的代码补全、接口提示。减少了查询文档、查询API的成本。
- 增强了代码可读性,通过类型注解帮助成员快速理解函数输入、输出,降低了协作成本。
类型系统基石
像一门语言有基本的语法一样,TypeScript也有基本的类型定义,这些基本类型对应JavaScript中的基本数据类型,包括number、string、boolean、null、undefined、symbol、bigint、object。
const name: string = "hboot";
const age: number = 18;
const message: string = `hello, ${name}`
const uniqueKey: symbol = Symbol("key");
空类型、任意类型、未知类型
void表示空类型,any表示任意类型,unknown表示未知类型。
-
void 常用于函数无返回值时的返回类型声明;
-
any 表示任意类型,失去了类型检查的能力,非必要不要用;
-
unknown 表示未知类型,可以用来代替any,但是它不能直接用,必须通过类型断言或类型守卫明确具体类型才能操作。
-
never 表示没有任何类型,变量和函数返回不会存在任何实际值。它是所有类型的字类型,可以赋值给任意类型。
function noReturn(msg: string): void {
console.log(msg);
}
let name: any = "admin";
// 可以任意赋值
name = 123;
// 甚至是当作函数调用,这导致开发不易发现,运行时才报错
name():
let age: unknown = 18;
// 类型为未知,无法直接操作
age += 10;
// 通过类型守卫,明确类型为 number
if(typeof age === 'number') {
age += 10;
}
复合类型
复合类型就是基础类型的组合,包括数组、对象、元组、枚举、类。
数组
数组的类型定义T[]或Array<T>
// 数组
let names: string[] = ['admin', 'user'];
let names: Array<string> = ['admin', 'user'];
对象
对象类型定义对象的属性名、属性类型。属性支持可选、只读、索引签名。
// 对象
let user: { name: string; age: number } = {
name: 'hboot',
age: 18
}
元组
元组是明确了数组长度以及元素类型的。
// 元组
let userInfo: [string, number] = ['hboot', 18];
枚举
枚举用于定义一组有特殊含义的常量,比如状态码、类型标识等。
// 枚举
const enum Status {
Success = 200,
Fail = 500
}
未赋值时,枚举值从 0 开始递增。如果自定义了开始值,则后面的值递增。
const enum Status {
Success, // 0
Fail // 1
}
const enum Status {
Success = 1, // 1
Fail // 2
}
可以通过const enum修饰枚举定义,在编译时仅保留常量值,减少代码体积。
类Class
类class是面向对象编程的核心。es6中已经增强了对类的支持,可以通过类定义对象,明确属性、方法。类不仅可以用来创建实例,也可以作为类型注解描述实例类型。
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
}
类的定义包括实例属性、实例方法、静态属性、静态方法,同时还支持通过访问修饰符控制成员可见。
子类可以通过extends继承父类的非private成员,重写父类方法。仅能继承一个父类。
class Dog extends Animal {
WangWang() {
return "WangWang";
}
}
类可以通过implements实现接口,用于约束类的实现,包括属性、方法,仅约束结构,不提供实现。可实现多个接口
interface Animal {
name: string;
speak(): void;
}
class Dog implements Animal {
name: string;
speak(): void {
console.log("WangWang");
}
constructor(name: string) {
this.name = name;
}
}
通过abstract关键字可以声明抽象类,抽象类不能被实例化。它只定义了字段、方法结构,未实现具体逻辑,它仅可被子类继承,并需要实现它定义的所有抽象成员。
高级类型
基础类型可以满足绝大多数业务,但面对复杂业务需要类型复用、条件判断、属性筛选则需要高级类型。
复用类型
为了复用类型,可以通过type \ interface定义类型。type可以定义任意类型,包括基础类型、复合类型、联合类型等,比较灵活;interface 只能定义对象类型,但是可以继承和同名合并。
// type
type Age = number;
type User = {
name: string;
age: Age;
}
type Status = 'success' | 200;
// interface
interface User {
name: string;
age: number;
}
interface SuperUser extends User {
role: string;
}
interface 不同于类class,它可以继承多个接口。
联合/交叉类型
联合类型通过|表示变量可以是任意其中一种类型;交叉类型通过&表示变量必须同时满足多个类型。
联合类型使用时需要类型守卫明确类型后才能操作,如果变量已经赋值,则会自动推导出类型;
type Age = number | string;
let age: Age = 18;
age+=2;
function agePlus(age: Age){
if(typeof age === 'number'){
age+=2;
}
return age;
}
交叉类型常用合并对象类型,需要满足所有类型条件。如果无法满足所有类型条件,则该类型为never
type User = {
name: string;
}
type Address = {
address: string;
}
type UserAddress = User & Address;
泛型
泛型是将类型参数化,可以不指定具体类型来定义函数、类、接口。在使用时在传入具体类型,它可以高度抽象定义类型,保障类型复用和类型安全。
// 函数泛型
function getData<T>(data: T): T {
return data;
}
// 接口泛型
interface IData<T> {
data: T;
}
// 类泛型
class Data<T> {
data: T[] = [];
}
因为不知道具体类型,就无法获知这个类型有什么属性,可以调用什么方法。为了使用某个属性或者某个方法,我们使用泛型约束来指定这个类型必须有哪些属性或者方法。
function getData<T extends { name: string }>(data: T): string {
return data.name;
}
在调用具有泛型的类型时,需要传递具体类型,有时候这个泛型我们知道在很多情况下就是某一个类型,避免重复传入,我们可以指定泛型的默认类型,从而在使用时不再需要传入。
type User = {
name: string;
}
function getData<T extends User = User>(data: T): string {
return data.name;
}
泛型约束和默认类型不需要同时出现,也可以定义其一,根据需求使用。
类型运算
除了定义具体类型,还可以通过类型运算从一个类型得到一个新的类型。也可以称之为推导,从一个类型推导为另一个类型。类型运算也是导致一些复杂类型产生的因素。
类型守卫
类型守卫是在运行时判断数据类型,它可以精准到具体类型,用于类型收窄,包括unknown、联合类型、类继承。关键字包括typeof instanceof in is
typeof 用于判断基础类型。但是无法判断 null,typeof null 返回 'object',可通过value!==null 进行判断。
function agePlus(age: string | number) {
if (typeof age === "number") {
age += 2;
}
return age;
}
instanceof 判断是否为某个类实例。
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
}
class Dog extends Animal {
WangWang() {
return "WangWang";
}
}
class Cat extends Animal {
MiaoMiao() {
return "MiaoMiao";
}
}
function speak(animal: Animal) {
if (animal instanceof Dog) {
return `${animal.name}:${animal.WangWang()}`;
}
if (animal instanceof Cat) {
return `${animal.name}:${animal.MiaoMiao()}`;
}
return `${animal.name}`;
}
in 适用于对象类型定义,用于判断是否包含某个属性
type Dog = {
a: string;
b: number;
};
type Cat = {
d: string;
c: number;
};
function getAttr(animal: Dog | Cat) {
if ("b" in animal) {
return animal.a;
}
if ("d" in animal) {
return animal.c;
}
return "unknown";
}
is 用于精准明确是什么类型,但是判断逻辑是我们自己写的,这就要求我们判断逻辑要精确,否则可能导致运行时错误。
type Dog = {
a: string;
b: number;
};
type Cat = {
d: string;
c: number;
};
// 内部逻辑自定义判断是否为某个类型
function isDog(animal: Dog | Cat): animal is Dog {
return "b" in animal;
}
function getAttr(animal: Dog | Cat) {
// 调用时如果为true,则推导为Dog
if (isDog(animal)) {
return animal.a;
} else {
return animal.c;
}
}
类型断言
类型断言是将一个类型临时转为另一个类型,仅在编译阶段告诉TypeScript是什么类型,不能改变变量的实际类型。
类型断言需要开发者明确保证断言类型正确,否则可能会导致运行时异常。对于不确定的类型,因该优先使用类型守卫.
使用as断言类型,可以转unknown类型、父类型转子类型场景。
let age: unknown = 34;
// 可以断言类型为string,从而调用字符串的方法。
// 但是在运行时会报错
(age as string).toUpperCase();
还可以通过as const断言为只读常量,常用于固定值的类型约束。
// 类型为 string
const name = "hboot";
// 类型为字面量 "hboot" 类型
const name = "hboot" as const;
使用!断言非空,常用于访问某个可选属性时,断言非空
type Cat = {
d?: string;
c: number;
};
let cat: Cat = {
c: 20,
};
// 未断言时调用报错属性 d 可能未定义
// cat.d.toUpperCase();
// 断言非空后则不会再提时报错,但是运行时报错
cat.d!.toUpperCase();
所以,断言要谨慎使用、避免滥用,确保断言的类型是是实际数据类型的兼容类型。
还可以通过双层断言将一个明确的类型断言为另一个类型。
这个操作很危险,实例中将string类型断言为Cat对象类型去访问属性c,编译通过,运行时爆炸。
type Cat = {
d?: string;
c: number;
};
let name = "hboot";
(name as unknown as Cat).c;
运算符
运算符可以从一个类型的到另一个类型。类型安全,类型运算的操作有很多
keyof 得到对象类型的所有字段key类型构成的联合类型。
type Cat = {
d?: string;
c: number;
};
// 得到 Cat 的所有字段 key 类型 联合类型 "d" | "c"
type Keys = keyof Cat;
// 如果字段是索引签名,则返回索引签名的类型
type Cat = {
[x: string]: unknown;
};
// 得到的是索引签名类型 string
type Keys = keyof Cat;
typeof 之前再类型守卫里已经介绍过了,它可以返回变量的类型(基本类型)。
索引访问(IndexedAccessType)获取类型,比如我们要获取对象类型里某个字段的类型,就可以使用索引获取。
type Cat = {
d?: string;
c: number;
};
// 获取字段类型为 string,但由于字段是可选 ?,
// 所以返回的是 string | undefined
type D = Cat["d"];
也可以通过联合类型获取到多个字段的类型,结果为一个联合类型。
type D = Cat["d"|"c"]
// 如果想要获取所有字段类型,可以通过keyof 获取到所有key的联合类型
type D = Cat[keyof Cat]
索引最重要的一点是可以对数组、元组元素的类型索引获取.
type names = [string, number];
// 索引第一个元素的类型
type Name = names[0];
type Names = Array<string>;
// 索引数组元素的类型
type Name = Names[number];
还可以搭配typeof 对变量的类型进行索引获取。
条件类型(ConditionalType),通过输入的类型,决定输出类型,通过extends关键字和三元表达式来标识。
type Name<T> = T extends string ? string : number;
// 输入泛型参数返回类型,
// 输出类型为 string
type Admin = Name<string>;
// 输出类型为 number
type User = Name<unknown>;
也可以声明一个函数书写更复杂的判断逻辑。既然通过extends关键字以及泛型参数,那么也可以增加泛型约束、泛型默认值。
映射类型(MappedType),从一个类型创建一个新类型,建立在索引签名语法之上,通过对对象的字段键值、字段类型进行转换操作。
type Cat = {
name?: string;
age: number;
};
type ToFunction<T> = {
[K in keyof T]: () => T[K];
};
// 从基本类型转换成函数类型
type CatFunction = ToFunction<Cat>;
/**
* type CatFunction = {
* name?: (() => string | undefined) | undefined;
* age: () => number;
* }
*/
文本类型(LiteralType),通过扩展字符串生成文本类型,表现同字符串字面值相同。
type Name = "hboot";
type GoodName = `Good, ${Name}`;
利用文本类型扩展,可以结合映射类型,改变键值,得到一个全新的类型。
type ToFunction<T> = {
[K in keyof T as `on_${string & K}`]: () => T[K];
};
// 就会得到不同键值、不同类型的新对象类型
type CatFunction = ToFunction<Cat>;
/**
* type CatFunction = {
* on_name?: (() => string | undefined) | undefined;
* on_age: () => number;
* }
*/
infer 类型推断,通常用于泛型参数推导;函数返回值类型推导
type Name<T> = T extends Array<infer U> ? U : T;
// 条件类型推导 得到数组元素类型 string
type str = Name<string[]>;
// 非数组类型,返回原类型 { name: string }
type info = Name<{ name: string }>;
内置工具函数
除了上述我们要手动去实现逻辑从而创建新的类型外,提供了内置函数可以直接使用,这些工具提供一些常用的类型转换逻辑。
Partial<T> 得到一个新类型,定义类型所有的字段都是可选的,与之相反的是Required<T>
type Cat = {
name?: string;
age: number;
};
type NewCat = Partial<Cat>;
/**
* type NewCat = {
* name?: string;
* age?: number;
* }
*/
type RequiredCat = Required<Cat>;
/**
* type RequiredCat = {
* name: string;
* age: number;
* }
*/
Pick<T,Keys> 从一个类型中选择某些字段得到一个新类型,Omit<T,Keys>从一个类型中删除某些字段得到一个新类型
type Cat = {
name?: string;
age: number;
};
type NewCat = Pick<Cat, 'name'>;
/**
* type NewCat = {
* name?: string;
* }
*/
type NewCat = Omit<Cat, 'age'>;
/**
* type NewCat = {
* name?: string;
* }
*/
Readonly<T> 得到一个新类型,定义类型所有的字段都是只读的,初始化后不可以修改
type Cat = {
name?: string;
age: number;
};
type ReadonlyCat = Readonly<Cat>;
let cat: ReadonlyCat = {
name: "hboot",
age: 18,
};
// 报错,不可以修改再赋值,属性是只读的
// cat.age = 20;
它没有与之相反定义的工具,怎么处理让所有只读属性都变为可编辑的呢?还记得上面讲过的映射类型吗?
在映射类型中通过使用-符号将所有字段的readonly修饰符都移除掉
type MutableType<T> = {
-readonly [K in keyof T]: T[K];
};
let cat: MutableType<ReadonlyCat> = {
name: "hboot",
age: 18,
};
// 通过映射类型 -readonly 移除掉属性的只读修饰符
cat.age = 20;
Record<Keys,T> 可以创建一个由Keys组成的属性对应类型为T的对象类型
type Animal = Record<
"dog" | "cat",
{
name: string;
age: number;
}
>;
/**
* type Animal = {
* dog: {
* name: string;
* age: number;
* };
* cat: {
* name: string;
* age: number;
* };
* }
*/
Exclude<U,T>从指定联合类型U排除某个满足 T类型的成员,得到新类型;与之相反的是Extract<U,T>
type Type = Exclude<"name" | "age" | 32, number | "name">;
/**
* type Type = "age"
*/
type Type = Extract<"name" | "age" | 32, number | "name">;
/**
* type Type = "name" | 32
*/
NonNullable<Type>排除null和undefined的成员,得到新类型
Parameters<Type> 从函数类型提取函数的参数类型,得到一个元组类型,非函数类型定义得到never类型。
type Fun = (name: string, age: number) => void;
type Params = Parameters<Fun>;
/**
* type Params = [name: string, age: number]
*/
ConstructorParameters<Type> 从构造函数类型提取构造函数的参数类型,得到一个元组类型或数组类型
type FunConstructorParams = ConstructorParameters<FunctionConstructor>;
/**
* type FunConstructorParams = string[]
*/
type ErrorConstructorParams = ConstructorParameters<ErrorConstructor>;
/**
* type ErrorConstructorParams = [message?: string, options?: ErrorOptions]
*/
ReturnType<Type> 从函数类型提取函数的返回值类型,对于非函数类型会得到any.
Awaited 用来获取异步函数的返回值类型
type Fun = () => Promise<{ data: string }>;
type FunType = Awaited<ReturnType<Fun>>;
/**
* type FunType = {
* data: string;
* }
*/
InstanceType<Type> 从构造函数类型提取构造函数的实例类型
type Fun = new () => { name: string };
type FunType = InstanceType<Fun>;
/**
* type FunType = {
* name: string;
* }
*/
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
}
type AnimalType = InstanceType<typeof Animal>;
/**
* type AnimalType = Animal
*/
NoInfer<T>阻止从该处推导泛型参数类型
function compare<T extends string>(a: T[], b?: NoInfer<T>) {
console.log(a, b);
}
// 锁定了泛型 T 类型是 "admin" | "test" , 所以传入参数 "hboot" 会提示报错
// 如果没有 NoInfer 则不会报错,推断类型 string
compare(["admin", "test"], "hboot");
ThisParameterType<T> 提取函数类型中的this参数的类型,如果没有this参数则返回unknown
OmitThisParameter<Type> 移除函数类型中的this参数,得到一个新的函数类型,如果没有this参数或不是函数类型则返回类型本身。
ThisType<Type> 用于指定方法上下文中的this类型,不会返回新类型,指定后在方法中就可以通过this访问到指定属性。但是需要开启noImplicitThis(tsconfig.json)标志才可以使用。
interface Animal {
name: string;
}
type Dog = {
speak(): void;
} & ThisType<Animal>;
const dog: Dog = {
speak() {
// 通过this 可以访问到 name 属性
console.log(this.name);
},
};
文本类型工具,用于处理文本,包括:
-
Uppercase<StringType> 将字符串转换为大写
-
Lowercase<StringType> 将字符串转换为小写
-
Capitalize<StringType> 将字符串的第一个字符转换为大写
-
Uncapitalize<StringType> 将字符串的开头字符转换为小写
命名空间
通过关键字namespace 定义空间名,用于分组相关的类型和值,避免命名冲突。并可通过export导出供外部使用
namespace Hboot {
//外部无法访问
const name = "hboot" as const;
// 外部可以访问
export function getName() {
return name;
}
}
Hboot.getName();