interface 和 type 关键字
interface 和 type 两个关键字的含义和功能都非常的接近。这里我们罗列下这两个主要的区别:
interface:
- 同名的 interface 自动聚合,也可以跟同名的 class 自动聚合
- 只能表示 object、class、function 类型
- 给函数挂载属性
type:
- 不仅仅能够表示 object、class、function
- 不能重名(自然不存在同名聚合了),扩展已有的 type 需要创建新 type
- 支持复杂的类型操作
举例说明下上面罗列的几点:
Objects/Functions
都可以用来表示 Object 或者 Function ,只是语法上有些不同而已
typescript
interface Point {
x:number;
y:number;
}
interface SetPoint {
(x:number,y:number):void;
}
type Point = {
x:number;
y:number;
}
type SetPoint = (x:number,y:number) => void;
其他数据类型
与 interface 不同,type 还可以用来定义其他的类型,比如基本数据类型、元素、并集等
ini
type Name = string;
type PartialPointX = {x:number;};
type PartialPointY = {y:number;};
type PartialPoint = PartialPointX | PartialPointY;
type Data = [number,string,boolean];
extends
都可以被继承,但是语法上会有些不同。另外需要注意的是,「interface 和 type 彼此并不互斥」。
interface extends interface
typescript
interface PartialPointX {x:number;};
interface Point extends PartialPointX {y:number;};
type extends type
ini
type PartialPointX = {x:number;};
type Point = PartialPointX & {y:number;};
interface extends type
typescript
type PartialPointX = {x:number;};
interface Point extends PartialPointX {y:number;};
type extends interface
ini
interface ParticalPointX = {x:number;};
type Point = ParticalPointX & {y:number};
implements
一个类,可以以完全相同的形式去实现 interface 或者 type。但是,类和接口都被视为静态的 ,因此,他们不能实现/继承 联合类型的 type
ini
interface Point {
x: number;
y: number;
}
class SomePoint implements Point {
x: 1;
y: 2;
}
type Point2 = {
x: number;
y: number;
};
class SomePoint2 implements Point2 {
x: 1;
y: 2;
}
type PartialPoint = { x: number; } | { y: number; };
// FIXME: 不能实现一个联合类型
class SomePartialPoint implements PartialPoint {
x: 1;
y: 2;
}
声明合并
和 type 不同,interface 可以被重复定义,并且会被自动聚合
ini
interface Point {x:number;};
interface Point {y:number;};
const point:Pint = {x:1,y:2};
only interface can
在实际开发中,有的时候也会遇到 interface 能够表达,但是 type 做不到的情况:「给函数挂载属性」
ini
interface FuncWithAttachment {
(param: string): boolean;
someProperty: number;
}
const testFunc: FuncWithAttachment = function(param: string) {
return param.indexOf("Neal") > -1;
};
const result = testFunc("Nealyang"); // 有类型提醒
testFunc.someProperty = 4; // 有输入提醒
字面量类型
既可以表示值,还可以表示类型,即所谓的字面量类型。TypeScript 支持 3 种字面量类型:字符串字面量类型、数字字面量类型、布尔字面量类型。对应的字符串字面量、数字字面量、布尔字面量分别拥有与其值一样的字面量类型:
ini
let str: 'hello world' = 'hello world';
let num: 996 = 996;
let bool: true = true;
基本使用:
字符串字面量
定义单个的字符串字面量类型并没有太大用处,它的应用场景是可以把多个字面量类型组合成一个联合类型,用来描述拥有明确成员的实用的集合:
lua
type Direction = "north" | "east" | "south" | "west";
function getDirectionFirstLetter(direction: Direction) {
return direction.substr(0, 1);
}
getDirectionFirstLetter("test"); // error 类型"test"的参数不能赋给类型"Direction"的参数
getDirectionFirstLetter("east");
-
数字字面量
数字字面量类型和字符串字面量类型差不多,都是指定类型为具体的值:
ini
type Age = 18;
interface Info {
name: string;
age: Age;
}
const info: Info = {
name: "TS",
age: 28 // error 不能将类型"28"分配给类型"18"
};
-
布尔字面量
由于布尔值只有true和false两种,所以以下两种类型意思一样的:
typescript
let value: true | false
let value: boolean
字面量类型的拓宽
在ES6中提出了两个新的声明变量的关键字:let和const,那当他们定义的变量的值相同时,变量的类型是一样的吗?
ini
const str = "hello world";
const num = 996;
const bool = false;
这里const定义了三个不能变的常量,在不写类型注解的情况下,TypeScript 会推断出它的类型为赋值字面量的类型。这样就不能再改变变量的值。
再来看使用let定义变量的例子:
ini
let str = "hello world";
let num = 996;
let bool = false;
这里没有写注解的变量的类型就变成了赋值字面量类型的父类型,比如str的类型是字符串字面量类型"hello world"的父类型string,num的类型是数字字面量类型996的父类型number,bool的类型是布尔字面量类型false的父类型boolean。这样就意味着,我们可以给这三个变量分别赋值string、number、boolean类型的值:
ini
str = "hello TypeScript";
num = 666;
bool = true;
这种将字面量类型转换为其父类型的设计就是字面量类型的拓宽。 通过 let 或 var 定义的变量、函数形参、对象的非只读属性,如果指定了初始值且未显式添加类型注解,那么它们推断出来的类型就是指定的初始值字面量类型拓宽后的类型,这就是字面量类型拓宽。
下面通过一个例子来理解一下字面量类型拓宽:
ini
let str = 'hello'; // 类型是 string
let strFun = (str = 'hello') => str; // 类型是 (str?: string) => string;
const specifiedStr = 'hello'; // 类型是 'this is string'
let str2 = specifiedStr; // 类型是 'string'
let strFun2 = (str = specifiedStr) => str; // 类型是 (str?: string) => string;
第一段代码中通过let定义了字符串str,是一个形参,并且没有显式的声明其类型,属于是类型拓宽,所以变量和形参推断出类型为string。
第二段代码中通过const定义了字符串specifiedStr,这个字符串是常量,不能进行修改,所以specifiedStr的类型为hello字面量类型,后面的str2遍历和strFun2函数形参被赋值了字面量类型的常量,并且没有显式的声明其类型,所以变量、形参的类型都被拓宽了,并没有被指定为它对应的字面量类型。这也是符合我们预期的。
联合类型
联合类型可以理解为多个类型的并集 。联合类型用来表示变量、参数的类型不是某个单一的类型,而可能是多种不同的类型的组合, 使用|
作为标记。
基本使用:
go
type Command = string[] | string
类型缩减
如果定义的联合类型的包含数字类型和数字字面量类型这种情况,会有什么效果呢?实际上,由于数字类型是数字字面量类型的父类型,所以最后会缩减为数字类型。同样string和boolean在这种情况下也会发生类型缩减。
ini
type UnionNum = 1 | number; // 类型是number
type UniomStr = "string" | string; // 类型是string
type UnionBool = false | boolean; // 类型是boolean
在这种情况下,TypeScript会对类型进行缩减,将字面量类型去掉,保留原始类型。
除此之外,当联合类型的成员是接口类型,并满足其中一个接口的属性是另一个接口属性的子集,这个属性也会进行类型缩减:
go
type UnionInterface = {
age: "18"
} | {
age: "18" | "25",
[key: string]: string;
}
由于 "18"
是 "18" | "25"
的子集,所以age属性的类型会变成 "18" | "25"
。
交叉类型
交叉类型,我们可以理解为合并。其实就是**「将多个类型合并为一个类型」,可以理解为多个类型的交集。**
可以使用 &
操作符来声明交叉类型:
ini
type Overlapping = string & number;
如果我们仅仅把基础类型、字面量类型、函数类型等原子类型合并成交叉类型,是没有任何意义的,因为不会有变量同时满足这些类型,那这个类型实际上就等于never类型。
使用场景:
合并接口类型
将多个接口类型合并成为一个类型是交叉类型的一个常见的使用场景。这样就能相当于实现了接口的继承,也就是所谓的合并接口类型:
ini
type Person = {
name: string;
age: number;
} & {
height: number;
weight: number;
} & {
id: number;
};
const person: Person = {
name: "zhangsan",
age: 18,
height: 180,
weight: 60,
id: 123456
};
这里我们通过交叉类型使Person同时拥有了三个接口类型中的5个属性。
- 同一个属性定义了不同的类型
两个接口中都拥有age属性,并且类型分别是number和string,那么在合并后,age的类型就是string & number,就是一个 never 类型
css
type Person = {
name: string;
age: number;
} & {
age: string;
height: number;
weight: number;
};
const person: Person = {
name: "zhangsan",
age: 18, // Type 'number' is not assignable to type 'never'.ts(2322)
height: 180,
weight: 60,
};
- 同名属性的类型兼容
如果同名属性的类型兼容,比如一个是 number,另一个是 number 的子类型、数字字面量类型,合并后 age 属性的类型就是两者中的子类型:
css
type Person = {
name: string;
age: number;
} & {
age: 18;
height: number;
weight: number;
};
const person: Person = {
name: "zhangsan",
age: 20, // Type '20' is not assignable to type '18'.ts(2322)
height: 180,
weight: 60,
}
-
合并联合类型
交叉类型另外一个常见的使用场景就是合并联合类型。可以合并多个联合类型为一个交叉类型,这个交叉类型需要同时满足不同的联合类型限制,也就是提取了所有联合类型的相同类型成员:
ini
type A = "blue" | "red" | 996;
type B = 996 | 666;
type C = A & B;
const c: C = 996;
- 如果多个联合类型中没有相同的类型成员,那么交叉出来的类型就是never类型:
ini
type A = "blue" | "red";
type B = 996 | 666;
type C = A & B;
const c: C = 996; // Type 'number' is not assignable to type 'never'.ts(2322)
注意:
ini
T & never = never
extends
extends 即为扩展、继承。在 ts 中,「extends 关键字既可以来扩展已有的类型,也可以对类型进行条件限定」。在扩展已有类型时,不可以进行类型冲突的覆盖操作。例如,基类型中键a为string,在扩展出的类型中无法将其改为number。
ini
type num = {
num: number;
};
interface IStrNum extends num {
str: string;
}
在 ts 中,我们还可以通过条件类型进行一些三目操作:T extends U ? X : Y
typescript
type IsEqualType<A , B> = A extends B ? (B extends A ? true : false) : false;
type NumberEqualsToString = IsEqualType<number,string>; // false
type NumberEqualsToNumber = IsEqualType<number,number>; // true
keyof
「keyof 是索引类型操作符」 。用于获取一个"常量"的类型,这里的"常量"是指任何可以在编译期确定的东西,例如const、function、class等。它是从 「实际运行代码」 通向 「类型系统」 的单行道。理论上,任何运行时的符号名想要为类型系统所用,都要加上 typeof。
在使用class时,class名表示实例类型,typeof class表示 class本身类型。是的,这个关键字和 js 的 typeof 关键字重名了 。
假设 T 是一个类型,那么keyof T产生的类型就是 T 的属性名称字符串字面量类型构成的联合类型(联合类型比较简单,和交叉类型对立相似,这里就不做介绍了)。
「注意!上述的 T 是数据类型,并非数据本身」。
ini
interface IQZQD {
cnName: string;
age: number;
author: string;
}
type ant = keyof IQZQD; //'cnName' | 'age' | 'author'
注意,如果 T 是带有字符串索引的类型,那么keyof T是 string或者number类型。
索引签名参数类型必须为 "string" 或 "number"
typescript
interface Map<T> {
[key: string]: T;
}
//T[U]是索引访问操作符;U是一个属性名称。
let keys: keyof Map<number>; //string | number
let value: Map<number>['antzone'];//number
in
「in 是遍历操作符」, 用于遍历类型。
ini
type Keys = "a" | "b"
type Obj = {
[p in Keys]: any
} // -> { a: any, b: any }
infer
infer
关键字在 TypeScript 中用于在类型推断中引入一个特定的类型变量 。这是 TypeScript 中的一个高级类型功能,常常与extends
一起使用,使得在条件类型的某些分支中可以捕获类型而不必显式地将其作为参数传递。
infer
允许在表达式中声明一个类型变量 ,并且在条件类型检查的过程中,"推断"出这个变量的类型。这使得类型操作更加灵活和强大,因为它可以根据类型的实际结构来动态地推断出类型信息。
r
type PramType<T> = T extends (param : infer p) => any ? p : any;
在上面的条件语句中,infer P 表示待推断的函数参数,如果T能赋值给(param : infer p) => any,则结果是(param: infer P) => any类型中的参数 P,否则为T.
使用场景举例
- 提取函数返回类型,入参类型
假设你有一个场景,需要根据传入的函数类型提取其返回值的类型。使用 infer
可以这样做:
typescript
type ReturnType<T> = T extends () => infer P ? P : T;
// 使用示例
type Func = () => number;
type MyReturnType = ReturnType<Func>; // number
这里,ReturnType
类型会检查 T
是否是一个函数,并尝试推断其返回类型。如果是,那么将其赋给 P
,并将 P
作为最终的类型结果。。
根据传入的函数类型提取其入参类型的类型。使用 infer
可以这样做:
typescript
type PramType<T> = T extends (param: infer p) => any ? p : T;
interface Pengxy {
name: 'Pengxy';
age: '25';
}
type Func = (user: Pengxy) => void;
type Param = PramType<Func>; // Param = Pengxy
type Test = PramType<string>; // string
这里,PramType
类型会检查 T
是否是一个函数,并尝试推断其入参类型。如果是,那么将其赋给 P
,并将 P
作为最终的类型结果。
- 提取 Promise 包裹的类型
当你想要获取一个 Promise 解析后的类型时,infer
同样非常有用:
typescript
type Unpacked<T> = T extends Promise<infer U> ? U : T;
// 使用示例
type AsyncData = Promise<string>;
type UnpackedData = Unpacked<AsyncData>; // string
这个 Unpacked
类型会检查 T
是否是一个 Promise,如果是,就使用 infer
推断出 Promise 解析后的类型,并将其返回。
总结
infer
关键字是 TypeScript 中一个非常强大的工具,允许开发者在不显式声明类型的情况下,从其他类型中"推断"出所需的类型信息。这在处理复杂的类型转换、提取或操作时非常有用,特别是当直接操作类型而不是值时。
泛型
泛型就是指定一个表示类型的变量 ,用它来代替某个实际的类型用于编程,而后再通过实际运行或推导的类型来对其进行替换,以达到一段使用泛型程序可以实际适应不同类型的目的。说白了,「泛型就是不预先确定的数据类型,具体的类型在使用的时候再确定的一种类型约束规范」。
泛型可以应用于 function、interface、type 或者 class 中。但是注意,「泛型不能应用于类的静态成员」(泛型参数是定义在类的实例层面上的,而不是静态层面上)
用法 :使用尖括号 < >
语法来定义一个泛型函数或类。
几个简单的例子,先感受下泛型
- 泛型类型
ini
let outputExplicit = identity<string>("myString");
let outputInferred = identity("myString"); // 类型推断
- 泛型接口和类
r
interface GenericIdentityFn<T> {
(arg: T): T;
}
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
泛型的好处:
- 函数和类可以轻松的支持多种类型,增强程序的扩展性
- 不必写多条函数重载,冗长的联合类型声明,增强代码的可读性
- 灵活控制类型之间的约束
工具泛型
所谓的工具泛型,其实就是泛型的一些语法糖的实现
Partial
Partial的作用就是将传入的属性变为可选。
然后再看 Partial 的实现:
ini
type Partial<T> = { [P in keyof T]?: T[P] };
翻译一下就是keyof T 拿到 T 所有属性名, 然后 in 进行遍历, 将值赋给 P, 最后 T[P] 取得相应属性的值,然后配合?
:改为可选。
用法 :Partial<T>
- T:传入的类型
举个例子:
typescript
interface User {
id: number;
name: string;
age: number;
email: string;
}
function updateUser(id: number, fieldsToUpdate: Partial<User>) {
// 逻辑来更新用户信息,使用 fieldsToUpdate 对象的属性
}
updateUser(1, { name: "John Doe" });
Required
Required 的作用是将传入的属性变为必选项, 源码如下:
ini
type Required<T> = { [P in keyof T]-?: T[P] };
拓展 : -?:用于移除属性的可选性。在 TypeScript 中,可以在映射类型的上下文中使用 +
或 -
前缀来分别添加或移除修饰符。在这种情况下,-?
表示如果属性是可选的(有 ?
标记),那么这个 ?
将被移除,使属性变为必选。
用法 :Required<T>
- T:传入的类型
举个例子:
php
interface Person {
name: string;
age?: number;
email?: string;
}
function createPersonRecord(person: Required<Person>) {
// 这里可以安全地假设person包含name、age和email属性
console.log(`Name: ${person.name}, Age: ${person.age}, Email: ${person.email}`);
}
// 正确的调用,因为所有属性都被提供了
createPersonRecord({ name: "Alice", age: 30, email: "[email protected]" });
// 错误的调用,因为age和email属性缺失
// createPersonRecord({ name: "Bob" }); // TypeScript 编译时将会报错
Readonly
将传入的属性变为只读选项, 源码如下
typescript
type Readonly<T> = { readonly [P in keyof T]: T[P] };
//使用readonly 关键字对属性进行标记
interface ReadOnlyTask {
readonly id: number;
readonly title: string;
completed: boolean; // 这个属性不是只读的,可以被修改
}
用法 :Readonly<T>
- T:传入的类型
举个例子:
ini
interface Task {
id: number;
title: string;
completed: boolean;
}
function freezeTask(task: Task): Readonly<Task> {
return Object.freeze(task);
}
const myTask: Task = { id: 1, title: "Learn TypeScript", completed: false };
const frozenTask = freezeTask(myTask);
// TypeScript 编译错误:Cannot assign to 'title' because it is a read-only property.
frozenTask.title = "Try to modify";
Readonly
类型工具
Readonly<T>
是一个泛型工具类型,可以将某个类型T
的所有属性标记为只读。- 它是在类型级别上使用的,适用于将现有的类型整体转换为只读版本。
- 使用
Readonly
类型工具时,你需要将整个对象类型作为泛型参数传递给Readonly
,这将使得该对象类型的所有属性都变为只读。
readonly
关键字
readonly
是一个关键字,可以在接口或类型别名中直接用于属性前,使得该属性为只读。- 它是在属性级别上使用的,允许你为对象的某些属性指定只读属性,而不是整个对象的所有属性。
- 使用
readonly
关键字时,你需要在每个想要标记为只读的属性前加上readonly
。
Record
用于创建一个对象类型,其属性键(key)来自于 K
,每个键的值的类型为 T
。这个类型非常适合用来表示对象字典或映射,其中所有的属性值类型都是相同的。该类型可以将 K 中所有的属性的值转化为 T 类型.
用法 :Record<K, T>
:
K
:代表了对象中的键的类型。T
:代表了对象中的值的类型。
举个例子:
css
interface Person {
name: string;
age?: number;
email?: string;
}
type T11 = Record<'a' | 'b' | 'c', Person>; // -> { a: Person; b: Person; c: Person; }
Pick
它允许你从一个类型中选取一个或者多个属性来构造一个新的类型。
用法 :Pick<T, K>
T
:属性值的类型。K
:代表了传入类型中选取的键的类型。
举个例子
ini
interface User {
id: number;
name: string;
age: number;
email: string;
}
type UserContactInfo = Pick<User, 'name' | 'email'>;
Exclude
Exclude 将某个类型中属于另一个的类型排除掉。
用法:Exclude<T, U>
T
:属性值的类型。U
:准备排除的类型。
r
type Exclude<T, U> = T extends U ? never : T;
以上语句的意思就是 如果 T 能赋值给 U 类型的话,那么就会返回 never 类型,否则返回 T,最终结果是将 T 中的某些属于 U 的类型移除掉
举个例子:
bash
type T00 = Exclude<'a' | 'b' | 'c' | 'd', 'a' | 'c' | 'f'>; // -> 'b' | 'd'
可以看到 T 是 'a' | 'b' | 'c' | 'd'
,然后 U 是 'a' | 'c' | 'f'
,返回的新类型就可以将 U 中的类型给移除掉,也就是 'b' | 'd'
了。
Extract
Extract 的作用是提取出T 包含在 U 中的元素,换种更加贴近语义的说法就是从 T 中提取出 U,源码如下:
r
type Extract<T, U> = T extends U ? T : never;
用法 :Extract<T, U>
T
:属性值的类型。U
:准备包含的类型。
例子:
bash
type T01 = Extract<'a' | 'b' | 'c' | 'd', 'a' | 'c' | 'f'>; // -> 'a' | 'c'
可以看到 T 是 'a' | 'b' | 'c' | 'd'
,然后 U 是 'a' | 'c' | 'f'
,返回的新类型就可以将 TU共用的类型提出来,就是'a' | 'c'
Omit
Pick 和 Exclude 进行组合, 实现忽略对象某些属性功能
用法 :Omit<T, K>
T
:属性值的类型。K
:准备忽略的键。
例子:
typescript
// 使用
type Foo = Omit<{name: string, age: number}, 'name'> // -> { age: number }
更多工具泛型
其实常用的工具泛型大概就是我上面介绍的几种。更多的工具泛型,可以通过查看 lib.es5.d.ts里面查看。
类型断言
Typescript 允许我们覆盖它的推断 ,然后根据我们自定义的类型去分析它 。这种机制,我们称之为 「类型断言」,使用 as
关键字
ini
const Pengxy = {};
Pengxy.enName = 'ppp'; // Error: 'enName' 属性不存在于 '{}'
Pengxy.cnName = '小玉'; // Error: 'cnName' 属性不存在于 '{}'
interface Pengxy {
enName: string;
cnName: string;
}
const Pengxy = {} as Pengxy; // const Pengxy = <Pengxy>{};
Pengxy.enName = 'ppp';
Pengxy.cnName = '小玉';
类型断言比较简单,其实就是"纠正"ts对类型的判断,当然,是不是纠正就看你自己的了。
需要注意一下两点即可:
- 推荐类型断言的预发使用 as关键字,而不是<> ,防止歧义
- 类型断言并非类型转换,类型断言发生在编译阶段。类型转换发生在运行时
函数重载
函数重载允许你为同一个函数提供多个函数类型定义 ,这样,你可以有多个函数签名,但是实现上只有一个。这使得在调用函数时,可以根据传入参数的不同类型或数量,执行不同的代码逻辑。这是一个非常有用的特性,因为它允许函数根据输入参数的不同而表现不同,同时也保持了代码的清晰和类型安全。
函数重载的声明方式是,首先声明所有的重载签名,然后提供一个实现签名。实现签名本身是不直接可见的,它的类型应当是所有重载签名的一个兼容类型,但是实现签名不会作为重载的一部分对外暴露。这是因为实现签名可能会有更宽泛的类型,以容纳所有的重载情况。
举个例子:
typescript
// 函数重载签名
function greet(name: string): string;
function greet(age: number): string;
// 实现签名
function greet(input: string | number): string {
if (typeof input === "string") {
return `Hello, ${input}`;
} else {
return `Your age is ${input}`;
}
}
// 使用重载
console.log(greet("Alice")); // 输出: Hello, Alice
console.log(greet(42)); // 输出: Your age is 42
在这个例子中,greet
函数被重载为两种形式:一种接受一个字符串参数,另一种接受一个数字参数。实现部分通过检查输入参数的类型来决定执行哪段代码。
注意
- 实现签名不可直接调用:实现函数签名(最后的实现)不会被当作重载的一部分。它的参数类型通常比较宽泛,是为了覆盖所有的重载情况。
- 参数类型和返回类型:函数重载允许你基于不同的参数类型和数量来改变函数的返回类型。
- 重载顺序:当有多个重载匹配时,TypeScript会从上到下匹配重载列表中的第一个匹配项。因此,你应该把最具体的(或最特殊的)重载签名放在最前面。
- 类型检查:TypeScript的类型检查器会检查重载签名,但不会检查实现签名。因此,你需要确保实现逻辑能够正确处理所有的重载情况。