ts类型梳理
类型声明的写法,一律为在标识符后面添加“冒号 + 类型”。函数参数和返回值,也是这样来声明类型。
function toString(num: number): string {
return String(num);
}
上面示例中,函数toString()的参数num的类型是number。参数列表的圆括号后面,声明了返回值的类型是string。
值代码和类型代码
TypeScript 代码只涉及类型,不涉及值。所有跟“值”相关的处理,都由 JavaScript 完成。
运行环境网址
https://www.typescriptlang.org/play/?#code/PTAEHUFMBsGMHsC2lQBd5oBYoCoE8AHSAZVgCcBLA1UABWgEM8BzM+AVwDsATAGiwoBnUENANQAd0gAjQRVSQAUCEmYKsTKGYUAbpGF4OY0BoadYKdJMoL+gzAzIoz3UNEiPOofEVKVqAHSKymAAmkYI7NCuqGqcANag8ABmIjQUXrFOKBJMggBcISGgoAC0oACCbvCwDKgU8JkY7p7ehCTkVDQS2E6gnPCxGcwmZqDSTgzxxWWVoASMFmgYkAAeRJTInN3ymj4d-jSCeNsMq-wuoPaOltigAKoASgAywhK7SbGQZIIz5VWCFzSeCrZagNYbChbHaxUDcCjJZLfSDbExIAgUdxkUBIursJzCFJtXydajBBCcQQ0MwAUVWDEQC0gADVHBQGNJ3KAALygABEAAkYNAMOB4GRonzFBTBPB3AERcwABS0+mM9ysygc9wASmCKhwzQ8ZC8iHFzmB7BoXzcZmY7AYzEg-Fg0HUiQ58D0Ii8fLpDKZgj5SWxfPADlQAHJhAA5SASPlBFQAeS+ZHegmdWkgR1QjgUrmkeFATjNOmGWH0KAQiGhwkuNok4uiIgMHGxCyYrA4PCCJSAA
any
1.使用场景,某些需要关闭验证
2.为了适配老的js项目
只要开发者使用了any类型,就表示开发者想要自己来处理这些代码,所以就不对any类型进行任何限制,怎么使用都可以。
从集合论的角度看,any类型可以看成是所有其他类型的全集,包含了一切可能的类型。
TypeScript 将这种类型称为“顶层类型”(top type),意为涵盖了所有下层。
缺点
any类型除了关闭类型检查,还有一个很大的问题,就是它会“污染”其他变量。
let x: any = "hello";
let y: number;
y = x; // 不报错
y * 123; // 不报错
y.toFixed(); // 不报错
unknown
为了解决any类型“污染”其他变量的问题
它与any含义相同,表示类型不确定,可能是任意类型,但是它的使用有一些限制,不像any那样自由,可以视为严格版的any。
首先,unknown类型的变量,不能直接赋值给其他类型的变量(除了any类型和unknown类型)。
let v: unknown = 123;
let v1: boolean = v; // 报错
let v2: number = v; // 报错
其次,不能直接调用unknown类型变量的方法和属性。
let v1: unknown = { foo: 123 };
v1.foo; // 报错
let v2: unknown = "hello";
v2.trim(); // 报错
let v3: unknown = (n = 0) => n + 1;
v3(); // 报错
再次,unknown类型变量能够进行的运算是有限的,只能进行比较运算(运算符==、=、!=、!、||、&&、?)、取反运算(运算符!)、typeof运算符和instanceof运算符这几种,其他运算都会报错。
答案是只有经过“类型缩小”,unknown类型变量才可以使用。所谓“类型缩小”,就是缩小unknown变量的类型范围,确保不会出错
never
ypeScript 还引入了“空类型”的概念,即该类型为空,不包含任何值。
由于不存在任何属于“空类型”的值,所以该类型被称为never,即不可能有这样的值。
使用场景
1 主要是在一些类型运算之中,保证类型运算的完整性
2 不可能返回值的函数,返回值的类型就可以写成never
3 如果一个变量可能有多种类型(即联合类型),通常需要使用分支处理每一种类型。这时,处理所有可能的类型之后,剩余的情况就属于never类型。
function fn(x: string | number) {
if (typeof x === "string") {
// ...
} else if (typeof x === "number") {
// ...
} else {
x; // never 类型
}
}
never类型的一个重要特点是,可以赋值给任意其他类型。
为什么never类型可以赋值给任意其他类型呢?这也跟集合论有关,空集是任何集合的子集。TypeScript 就相应规定,任何类型都包含了never类型。因此,never类型是任何其他类型所共有的,TypeScript 把这种情况称为“底层类型”(bottom type)。
总之,TypeScript 有两个“顶层类型”(any和unknown),但是“底层类型”只有never唯一一个。
包装对象类型与字面量类型
"hello"; // 字面量
new String("hello"); // 包装对象
了区分这两种情况,TypeScript 对五种原始类型分别提供了大写和小写两种类型。
Boolean 和 boolean
String 和 string
Number 和 number
BigInt 和 bigint
Symbol 和 symbol
const s1: String = "hello"; // 正确
const s2: String = new String("hello"); // 正确
const s3: string = "hello"; // 正确
const s4: string = new String("hello"); // 报错
建议只使用小写类型,不使用大写类型。因为绝大部分使用原始类型的场合,都是使用字面量,不使用包装对象
Object 类型与 object 类型
大写的Object类型代表 JavaScript 语言里面的广义对象。所有可以转成对象的值,都是Object类型,这囊括了几乎所有的值。
let obj: Object;
obj = true;
obj = "hi";
obj = 1;
obj = { foo: 123 };
obj = [1, 2];
obj = (a: number) => a + 1;
上面示例中,原始类型值、对象、数组、函数都是合法的Object类型。
小写的object类型代表 JavaScript 里面的狭义对象,即可以用字面量表示的对象,只包含对象、数组和函数,不包括原始类型的值。
let obj: object;
obj = { foo: 123 };
obj = [1, 2];
obj = (a: number) => a + 1;
obj = true; // 报错
obj = "hi"; // 报错
obj = 1; // 报错
值类型
let x: 5 = 5;
let y: number = 4 + 1;
x = y; // 报错
y = x; // 正确
联合类型 |
联合类型A|B表示,任何一个类型只要属于A或B,就属于联合类型A|B
let x: string | number;
x = 123; // 正确
x = "abc"; // 正确
交叉类型
指的多个类型组成的一个新类型,使用符号&表示。
let obj: { foo: string } & { bar: string };
obj = {
foo: "hello",
bar: "world",
};
上面示例中,变量obj同时具有属性foo和属性bar
type类型
type命令用来定义一个类型的别名。
type Age = number;
let age: Age = 55;
上面示例中,type命令为number类型定义了一个别名Age。这样就能像使用number一样,使用Age作为类型
typeof
console.log(typeof undefined); // "undefined"
console.log(typeof 0); // "number"
console.log(typeof 123); // "number"
console.log(typeof -1.5); // "number"
console.log(typeof NaN); // "number" (注意:尽管 NaN 表示“不是数字”,但其类型仍然是 number)
console.log(typeof true); // "boolean"
console.log(typeof false); // "boolean"
console.log(typeof ""); // "string"
console.log(typeof "hello"); // "string"
console.log(typeof [1, 2, 3]); // "object" (数组被视为对象)
console.log(typeof {a: 1}); // "object"
console.log(typeof null); // "object" (这是一个历史遗留问题,null 实际上表示空值)
console.log(typeof function(){}); // "function"
console.log(typeof class {}); // "function" (类也是函数的一种形式)
另外,typeof命令的参数不能是类型
块级类型声明
if (true) {
type T = number;
let v: T = 5;
} else {
type T = string;
let v: T = "hello";
}
上面示例中,存在两个代码块,其中分别有一个类型T的声明。这两个声明都只在自己的代码块内部有效,在代码块外部无效。
类型的兼容
type T = number | string;
let a: number = 1;
let b: T = a;
上面示例中,变量a和b的类型是不一样的,但是变量a赋值给变量b并不会报错。这时,我们就认为,b的类型兼容a的类型。
数组的类型
1 数组的类型有两种写法。
let arr: number[] = [1, 2, 3];
如果数组成员的类型比较复杂,可以写在圆括号里面
let arr: (number | string)[];
2 数组类型的第二种写法是使用 TypeScript 内置的 Array 接口。
let arr: Array<number> = [1, 2, 3];
只读数组,const 断言
TypeScript 允许声明只读数组,方法是在数组类型前面加上readonly关键字。
const arr: readonly number[] = [0, 1];
arr[1] = 2; // 报错
arr.push(3); // 报错
delete arr[0]; // 报错
Index signature in type ‘readonly number[]’ only permits reading.
Property ‘push’ does not exist on type ‘readonly number[]’.
Index signature in type ‘readonly number[]’ only permits reading.
元组
元组(tuple)是 TypeScript 特有的数据类型,
JavaScript 没有单独区分这种类型。
它表示成员类型可以自由设置的数组,即数组的各个成员的类型可以不同。
元组必须明确声明每个成员的类型。
const s: [string, string, boolean] = ["a", "b", true];
元组类型的写法,与上一章的数组有一个重大差异
数组的成员类型写在方括号外面(number[]),
元组的成员类型是写在方括号里面([number])。
TypeScript 的区分方法是,成员类型写在方括号里面的就是元组,写在外面的就是数组
let a: [number] = [1];
上面示例中,变量a是一个元组,只有一个成员,类型是number。
元组成员的类型可以添加问号后缀(?),表示该成员是可选的
let a: [number, number?] = [1];
symbol类型
函数的类型
function hello(txt: string): void {
console.log("hello " + txt);
}
,函数hello()在声明时,需要给出参数txt的类型(string),以及返回值的类型(void),后者写在参数列表的圆括号后面。void类型表示没有返回值。
函数返回 void 类型
代表函数没有返回值
function f(): void {
return 123; // 报错
}
如果返回其他值,就会报错。
函数返回 never类型
// 正确
function sing(): void {
console.log("sing");
}
// 报错
function sing(): never {
console.log("sing");
}
上面示例中,函数sing()虽然没有return语句,但实际上是省略了return undefined这行语句,真实的返回值是undefined。所以,它的返回值类型要写成void,而不是never,写成never会报错。
高阶函数
一个函数的返回值还是一个函数,那么前一个函数就称为高阶函数(higher-order function)
(someValue: number) => (multiplier: number) => someValue * multiplier;
函数重载
有些函数可以接受不同类型或不同个数的参数,并且根据参数的不同,会有不同的函数行为。这种根据参数类型不同,执行不同逻辑的行为,称为函数重载(function overload)。
reverse("abc"); // 'cba'
reverse([1, 2, 3]); // [3, 2, 1]
对象
一旦声明了类型,对象赋值时,就不能缺少指定的属性,也不能有多余的属性。
type MyObj = {
x: number;
y: number;
};
const o1: MyObj = { x: 1 }; // 报错
const o2: MyObj = { x: 1, y: 1, z: 1 }; // 报错
上面示例中,变量o1缺少了属性y,变量o2多出了属性z,都会报错。
接口赋值类型
const {
id,
name,
price,
}: {
id: string;
name: string;
price: number;
} = product;
interface
interface 是对象的模板,可以看作是一种类型约定,中文译为“接口”。
使用了某个模板的对象,就拥有了指定的类型结构。
interface 继承 interface
interface Shape {
name: string;
}
interface Circle extends Shape {
radius: number;
}
interface 继承 type
type Country = {
name: string;
capital: string;
};
interface CountryWithPop extends Country {
population: number;
}
接口合并
interface Box {
height: number;
width: number;
}
interface Box {
length: number;
}
两个Box接口会合并成一个接口,同时有height、width和length三个属性。
interface 与 type 的异同
interface命令与type命令作用类似,都可以表示对象类型。
很多对象类型即可以用 interface 表示,也可以用 type 表示。而且,两者往往可以换用,几乎所有的 interface 命令都可以改写为 type 命令。
- 它们的相似之处,首先表现在都能为对象类型起名。 —创造一个值,编译后依然存在。如果只是单纯想要一个类型,应该使用type或interface
interface 与 type 的区别有下面几点。
(1)type能够表示非对象类型,而interface只能表示对象类型(包括数组、函数等)。
(2)interface可以继承其他类型,type不支持继承。
继承的主要作用是添加属性,type定义的对象类型如果想要添加属性,只能使用&运算符,重新定义一个类型。
type Animal = {
name: string;
};
type Bear = Animal & {
honey: boolean;
};
上面示例中,类型Bear在Animal的基础上添加了一个属性honey。
非空断言!
let value: string | null = "Hello";
// 使用非空断言
let length = value!.length;
console.log(length); // 输出:5
在这个例子中,value 的类型是 string | null,但在使用 value!.length 时,我们通过 ! 告诉编译器 value 不会是 null,因此可以直接访问其属性 length。
类的 interface 接口
implements 关键字
interface Country {
name: string;
capital: string;
}
// 或者
type Country = {
name: string;
capital: string;
};
class MyCountry implements Country {
name = "";
capital = "";
}
interface或type都可以定义一个对象类型。
类MyCountry使用implements关键字,表示该类的实例对象满足这个外部类型。
可访问性修饰符
类的内部成员的外部可访问性,由三个可访问性修饰符(access modifiers)控制:
public、
private和protected。
public修饰符表示这是公开成员,外部可以自由访问
private修饰符表示私有成员,只能用在当前类的内部,类的实例和子类都不能使用该成员
protected修饰符表示该成员是保护成员,只能在类的内部使用该成员,实例无法使用该成员,但是子类内部可以使用。
静态成员
类的内部可以使用static关键字,定义静态成员。
静态成员是只能通过类本身使用的成员,不能通过实例对象使
泛型类
this 问题
泛型
函数返回值的类型与参数类型是相关的
function getFirst(arr) {
return arr[0];
}
上面示例中,函数getFirst()总是返回参数数组的第一个成员。参数数组是什么类型,返回值就是什么类型。
函数的类型声明只能写成下面这样。
function f(arr: any[]): any {
return arr[0];
}
为了解决这个问题,TypeScript 就引入了“泛型”(generics)。泛型的特点就是带有“类型参数”(type parameter)。
function getFirst<T>(arr: T[]): T {
return arr[0];
}
上面示例中,函数getFirst()的函数名后面尖括号的部分,就是类型参数,参数要放在一对尖括号(<>)里面。本例只有一个类型参数T,可以将其理解为类型声明需要的变量,需要在调用时传入具体的参数类型。
接口的泛型写法
interface Box<Type> {
contents: Type;
}
let box: Box<string>;
Enum类型
as 类型断言
TypeScript 提供了“类型断言”这样一种手段,允许开发者在代码中“断言”某个值的类型,告诉编译器此处的值是什么类型。TypeScript 一旦发现存在类型断言,就不再对该值进行类型推断,而是直接采用断言给出的类型。
type T = "a" | "b" | "c";
let foo = "a";
let bar: T = foo as T; // 正确
上面示例中,最后一行的foo as T表示告诉编译器,变量foo的类型断言为T,所以这一行不再需要类型推断了,编译器直接把foo的类型当作T,就不会报错了。
类型断言有两种语法。
// 语法一:<类型>值
value;
// 语法二:值 as 类型
value as Type;