# 类型分类
聊聊 TS
中的类型分类吧
# 原始类型
JavaScript 中以下类型被视为原始类型:
string
、boolean
、number
、bigint
、symbol
、null
和undefined
。
// 注意 | |
// 非严格模式下 null 和 undefined 是所有类型的子类型,就是说你可以把 null 和 undefined 赋值给其他类型。 | |
// 虽然 number 和 bigint 都表示数字,但是这两个类型不兼容 | |
let str: string = "SakuraSnow"; | |
let num: number = 16; | |
let bool: boolean = true; | |
let u: undefined = undefined; | |
let n: null = null; | |
let big: bigint = 100n; | |
let sym: symbol = Symbol("snow"); |
# 对象类型
# 对象
// 定义一个 type | |
type User = { | |
name: string, | |
age: number | |
} | |
// 使用这个 type | |
let user: User = { | |
name: "Sakura", | |
age: 16 | |
} |
注意, object
和 Object
和 空对象{}
是不同的
- 小
object
代表的是所有非原始类型,也就是说我们不能把number
、string
、boolean
、symbol
等 原始类型赋值给object
。在严格模式下,null
和undefined
类型也不能赋给object
。 - 大
Object
代表所有拥有 toString,hasOwnProperty 方法的类型,所以所有原始类型、非原始类型都可以赋给Object
。同样,在严格模式下,null
和undefined
类型也不能赋给Object
。 {}空对象
类型和大 Object
一样,也是表示原始类型和非原始类型的集合,并且在严格模式下,null
和undefined
也不能赋给{}
// 小 object | |
let lowerCaseObject: object; | |
lowerCaseObject = 1; // ts(2322) | |
lowerCaseObject = 'a'; // ts(2322) | |
lowerCaseObject = true; // ts(2322) | |
lowerCaseObject = null; // ts(2322) | |
lowerCaseObject = undefined; // ts(2322) | |
lowerCaseObject = {}; // ok | |
// 大 object | |
let upperCaseObject: Object; | |
upperCaseObject = 1; // ok | |
upperCaseObject = 'a'; // ok | |
upperCaseObject = true; // ok | |
upperCaseObject = null; // ts(2322) | |
upperCaseObject = undefined; // ts(2322) | |
upperCaseObject = {}; // ok | |
// 空对象 | |
let ObjectLiteral: {}; | |
ObjectLiteral = 1; // ok | |
ObjectLiteral = 'a'; // ok | |
ObjectLiteral = true; // ok | |
ObjectLiteral = null; // ts(2322) | |
ObjectLiteral = undefined; // ts(2322) | |
ObjectLiteral = {}; // ok。 |
综上结论:{}、大 Object 是比小 object 更宽泛的类型(least specific),{} 和大 Object 可以互相代替,用来表示原始类型(null、undefined 除外)和非原始类型;而小 object 则表示非原始类型。
从上面示例可以看到, 大 Object
包含原始类型, 小 object
仅包含非原始类型,所以 大 Object
似乎是 小 object
的父类型。实际上, 大 Object
不仅是 小 object
的父类型,同时也是 小 object
的子类型。
type isLowerCaseObjectExtendsUpperCaseObject = object extends Object ? true : false; // true | |
type isUpperCaseObjectExtendsLowerCaseObject = Object extends object ? true : false; // true | |
upperCaseObject = lowerCaseObject; // ok | |
lowerCaseObject = upperCaseObject; // ok |
为什么这么设置呢,我也在想,等我想到再补充
# 数组
// 两种定义方式 | |
let arr: string[] = ["1","2"]; | |
let arr: Array<string> = ["1","2"]; | |
let arr: (number | string)[] = ["1", 2]; |
# 函数
# 函数定义
// 函数声明 | |
function sum(x: number, y: number): number { | |
return x + y; | |
} | |
// 函数表达式 | |
let sum: (x: number, y: number) => number = function (x: number, y: number): number { | |
return x + y; | |
}; | |
// 使用接口定义函数 | |
interface SumFunc { | |
(x: number, y: number): number; | |
} |
# 函数参数
// 可选参数 | |
// 注意:可选参数后面不允许再出现必需参数 | |
function buildName(firstName: string, lastName?: string) { | |
return lastName ? `${firstName} ${lastName}` : firstName; | |
} | |
buildName('Sakura', 'Snow'); | |
buildName('Sakura'); |
// 参数默认值 | |
function buildName(firstName: string, lastName: string = "Snow") { | |
return `${firstName} ${lastName}`; | |
} |
// 剩余参数 | |
function push<T>(array: Array<T>, ...items: Array<T>): Array<T> { | |
items.forEach((item: T) => array.push(item)); | |
return array; | |
} | |
let a: Array<number> = []; | |
push<number>(a, 1, 2, 3); |
# 函数重载
函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力
// 提供函数类型的定义 | |
function add(x: number, y: number): number; | |
function add(x: string, y: string): string; | |
// 实现函数 | |
function add(x, y) { | |
return x + y; | |
} | |
// 不报错 | |
let num: number = add(1,2); | |
let str: string = add("Sakura", "Snow"); | |
// 报错 | |
let count: number = add("Sakura", "Snow"); |
# 元组
众所周知,数组一般由同种类型的值组成,但有时我们需要在单个变量中存储不同类型的值,这时候我们就可以使用元组。在 JavaScript 中是没有元组的,元组是 TypeScript 中特有的类型,其工作方式类似于数组。
元组最重要的特性是可以限制数组元素的个数和类型,它特别适合用来实现多值返回。
// 普通元组 | |
type Info = [string, number]; | |
let info: Info = ["Sakura", 16]; // ok | |
let info: Info = ["Sakura"]; // Error | |
// 带可选参数的元组 | |
type Tuple = [string, number?]; | |
let tuple: Tuple = ["Sakura", 16]; // ok | |
let tuple: Tuple = ["Sakura"]; // ok | |
let tuple: Tuple = [16]; // Error | |
type Point = [number, number?, number?]; | |
const x: Point = [10]; // 一维坐标点 | |
const xy: Point = [10, 20]; // 二维坐标点 | |
const xyz: Point = [10, 20, 10]; // 三维坐标点 | |
// 带剩余元素的元组 | |
// 元组类型里最后一个元素可以是剩余元素,形式为 ...X,这里 X 是数组类型。剩余元素代表元组类型是开放的,可以有零个或多个额外的元素。 例如,[number, ...string []] 表示带有一个 number 元素和任意数量 string 类型元素的元组类型。 | |
type RestTupleType = [number, ...string[]]; | |
let restTuple: RestTupleType = [666, "Semlinker", "Kakuqo", "Lolo"]; |
# 其他类型
# void
void
表示没有任何类型,和其他类型是平等关系,不能直接赋值
let a: void; | |
let b: number = a; // Error |
你只能为它赋予 null
和 undefined
(在 strictNullChecks
未指定为 true 时)。声明一个 void
类型的变量没有什么大用,我们一般也只有在函数没有返回值时去声明。
// 值得注意的是,方法没有返回值将得到 undefined,但是我们需要定义成 void 类型,而不是 undefined 类型。 | |
function fun(): void { | |
console.log("this is TypeScript"); | |
} |
# never
相比于 void
表示没有, never
表示无法到达
never
一般用在下面三种情况
- 永远抛出错误的函数
- 死循环的函数
- 无法到达的分支
// Function returning never must not have a reachable end point | |
function error(message: string): never { | |
throw new Error(message); | |
} | |
// Inferred return type is never | |
function fail() { | |
return error("Something failed"); | |
} | |
// Function returning never must not have a reachable end point | |
function infiniteLoop(): never { | |
while (true) {} | |
} | |
// 可以利用 never 类型的特性来实现全面性检查 | |
function foo(data : number|string) { | |
if (typeof data === "number") { | |
// ... | |
} else if (typeof data === "string") { | |
// ... | |
} else { | |
let rua = data; | |
// Property 'name' does not exist on type 'never'. | |
console.log(rua.name) | |
} | |
} |
never
类型同 null
和 undefined
一样,也是任何类型的子类型,也可以赋值给任何类型。
但是没有类型是 never
的子类型或可以赋值给 never
类型(除了 never
本身之外),即使 any
也不可以赋值给 never
# any
表示一个变量的值可以是任何类型,并且去掉类型检查。基本等价于变回 js
了
在许多场景下,这太宽松了。使用
any
类型,可以很容易地编写类型正确但在运行时有问题的代码。如果我们使用any
类型,就无法使用 TypeScript 提供的大量的保护机制。请记住,any 是魔鬼!
尽量不要用 any。
function foo() : any { | |
return null; | |
} | |
//ts 不会报错 | |
// 只有运行时才会报错 | |
console.log(foo().data); |
# unknown
表示一个变量的类型是未知的,每次使用前都要进行类型检查
function foo(): string | number { | |
if (Math.random() < 0.5) { | |
return 0; | |
} else { | |
return "" | |
} | |
} | |
let data: unknown = foo(); | |
let num = 10; | |
// 如果不缩小类型,就无法对 unknown 类型执行任何操作 | |
// 这种机制起到了很强的预防性,更安全,这就要求我们必须缩小类型,我们可以使用 typeof、类型断言等方式来缩小未知范围 | |
if (typeof data === "number") { | |
let sum = num + data; | |
console.log(sum); | |
} |
unknown
与any
一样,所有类型都可以分配给unknown
。
但是任何类型的值可以赋值给
any
,同时any
类型的值也可以赋值给任何类型。unknown
任何类型的值都可以赋值给它,但它只能赋值给unknown
和any
# 包装类型
Number、String、Boolean、Symbol 被称为包装类型,当然这是我自己起的名字
原始类型 number
、 string
、 boolean
、 symbol
混淆的首字母大写的 Number
、 String
、 Boolean
、 Symbol
类型,后者是相应原始类型的 包装对象
从类型兼容性上看,原始类型兼容对应的对象类型,反过来对象类型不兼容对应的原始类型。
let num: number; | |
let Num: Number; | |
Num = num; // ok | |
num = Num; //ts (2322) 报错 |
# 字面量类型
在 TypeScript 中,字面量不仅可以表示值,还可以表示类型,即所谓的字面量类型。
目前,TypeScript 支持 3 种字面量类型:字符串字面量类型、数字字面量类型、布尔字面量类型,对应的字符串字面量、数字字面量、布尔字面量分别拥有与其值一样的字面量类型,具体示例如下。
// 分别为字符串,数字,布尔字面量类型 | |
let specifiedStr: 'this is string' = 'this is string'; // 类型是 'this is string' | |
let specifiedNum: 1 = 1; // 类型是 1 | |
let specifiedBoolean: true = true; // 类型就是 true | |
// 子类型 | |
let str: string = 'any string'; | |
specifiedStr = str; //ts (2322) 类型 '"string"' 不能赋值给类型 'this is string' | |
str = specifiedStr; // ok |
当然单单使用一个字面量类型没啥意义,它真正的应用场景是可以把多个字面量类型组合成一个联合类型
// 一个元素只能为 1 2 3 的数组 | |
let arr: Array<1 | 2 | 3> = [1, 2, 3] |
# 联合类型
联合类型表示取值可以为多种类型中的一种,使用 |
分隔每个类型。你可以把它当成是求交集
// 表明 myFavoriteNumber 为 string 或者 number | |
let myFavoriteNumber | |
myFavoriteNumber: string | number; | |
myFavoriteNumber = 'seven'; // OK | |
myFavoriteNumber = 7; // OK | |
// 与 undefined 和 null 联用 | |
const sayHello = (name: string | undefined) => { | |
/* ... */ | |
}; | |
// 和字面量一起使用 | |
let num: 1 | 2 = 1; | |
type EventNames = 'click' | 'scroll' | 'mousemove'; | |
// 联合类型只能 取到公共的方法或者属性 | |
let n!: number | string; |
# 交叉类型
交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性,使用 &
定义交叉类型。
注意,如果我们仅仅把原始类型、字面量类型、函数类型等原子类型合并成交叉类型,是没有任何用处的,因为任何类型都不能满足同时属于多种原子类型,比如既是 string 类型又是 number 类型。因此,下面的代码中,类型别名 Useless 的类型是 never。
// never | |
type Useless = string & number; |
交叉类型真正的用武之地就是将多个接口类型合并成一个类型,从而实现等同接口继承的效果,也就是所谓的合并接口类型,代码如下所示:
// 合并 | |
type IntersectionType = { id: number; name: string; } & { age: number }; | |
const mixed: IntersectionType = { | |
id: 1, | |
name: 'name', | |
age: 18 | |
} | |
// 注意 & 时会对同名属性进行合并 | |
type Person = { | |
name: string, | |
age: number, | |
opt: { | |
walk: () => void; | |
} | |
} | |
type Animal = { | |
name: number, | |
age: number | string, | |
opt: { | |
eat: (food: string) => void; | |
} | |
} | |
type Sth = Person & Animal; | |
let o : Sth = { | |
//name 的类型变为 never 导致 Sth 类型无效 | |
name: null, | |
// 类型为 number | |
age: 16, | |
// 类型为 {eat, walk} | |
opt: { | |
walk() {}, | |
eat() {}, | |
} | |
} |
在上述示例中,我们通过交叉类型,使得 IntersectionType
同时拥有了 id、name、age 所有属性,这里我们可以试着将合并接口类型理解为求并集。
# 枚举类型
枚举
通常用来约束某个变量的取值范围
解决了使用 字面量
进行类型约束的问题
- 在类型约束位置会产生重复代码
- 逻辑含义和真实的值产生了混淆,会导致当修改真实值的时候,产生大量修改
- 字面量类型不会进入到编译结果
// 有重复代码 | |
// 定义一个 gender 变量,并且约束为男或者女 | |
let gender : "男" | "女"; | |
//gender 可以赋值为男或者女 | |
gender = "女"; | |
gender = "男"; | |
// 根据性别查询函数 | |
function searchUsers(g:"男" | "女") {} | |
// 修改真实值的时候比较难改 | |
type Gender = "帅哥" | "美女"; | |
// 定义一个 gender 变量,并且约束为男或者女 | |
let gender : Gender; | |
//gender 可以赋值为男或者女 | |
gender = "女"; | |
gender = "男"; | |
// 根据性别查询函数 | |
function searchUsers(g:Gender) {} |
这时候就可以使用枚举来优化
// 编译后的结果是一个对象 | |
enum Gender { | |
male = "美女", | |
female = "帅哥" | |
} | |
let gender : Gender; | |
gender = Gender.male; | |
gender = Gender.female; | |
function searchUser(g: Gender) {} | |
searchUser(gender); |
# 类型推断
类型推断:在很多情况下,TypeScript 会根据上下文环境自动推断出变量的类型
初始化值的变量
、 有默认值的函数参数
、 函数返回的类型
都可以根据上下文推断出来。
// 初始化值的变量 | |
let str = 'this is string'; // 自动标记为 string 类型 | |
let num = 1; // 自动标记为 number 类型 | |
let bool = true; // 自动标记为 boolean 类型 | |
// 有默认值的函数参数 | |
// 根据参数的类型,推断出返回值的类型也是 number | |
function add(a: number, b: number) { | |
return a + b; | |
} | |
const count = add(1, 1); // 推断出 count 的类型也是 number | |
// 推断参数 b 的类型是数字或者 undefined,返回值的类型也是数字 | |
function add(a: number, b = 1) { | |
return a + b; | |
} | |
const count1 = add(1); | |
const count2 = add(1, '1'); // ts(2345) Argument of type "1" is not assignable to parameter of type 'number | undefined |
如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any
类型而完全不被类型检查:
let myFavoriteNumber; | |
myFavoriteNumber = 'seven'; | |
myFavoriteNumber = 7; |
# 类型断言
所以有时候你会遇到这样的情况,你会比
TypeScript
更了解某个值的详细信息。通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。
类型断言不是类型转换,它不会真的影响到变量的类型。
有时会碰到我们比 TypeScript
更清楚实际类型的情况,比如下面的例子:
const arrayNumber: number[] = [1, 2, 3, 4]; | |
const greaterThan2: number = arrayNumber.find(num => num > 2); // 提示 ts (2322) |
其中, greaterThan2
一定是一个数字,因为 arrayNumber
中明显有大于 2 的成员,但静态类型对运行时的逻辑无能为力。
在 TypeScript
看来, greaterThan2
的类型既可能是数字,也可能是 undefined
,所以上面的示例中提示了一个 ts(2322)
错误,此时我们不能把类型 undefined
分配给类型 number
。
所以,这时候就需要类型断言了
# 强制类型断言
强制指定操作对象的类型
let n!: number | string; | |
// 尖括号 | |
let num: number = <number>n; | |
//as 语法 | |
let str: string = n as string; |
# 非空断言
使用 !
断言操作对象是非 null
和非 undefined
let mayNullOrUndefinedOrString: null | undefined | string; | |
mayNullOrUndefinedOrString!.toString(); // ok | |
mayNullOrUndefinedOrString.toString(); // Object is possibly 'null' or 'undefined'. |
# 确认赋值断言
允许在实例属性和变量声明后面放置一个 !
号,从而告诉 TypeScript 该属性会被明确地赋值。
// 未使用断言 | |
let x: number; | |
initialize(); | |
// Variable 'x' is used before being assigned.(2454) | |
console.log(2 * x); // Error | |
function initialize() { | |
x = 10; | |
} | |
// 使用断言 | |
let x!: number; | |
initialize(); | |
console.log(2 * x); // Ok | |
function initialize() { | |
x = 10; | |
} |
# 双重断言
普通的断言是这样的
- 联合类型可以被断言为其中一个类型
- 父类可以被断言为子类
那,如果两个完全不兼容的断言,就要使用双重断言了,因为
- 任何类型都可以被断言为 any
- any 可以被断言为任何类型
// Conversion of type 'string | number' to type 'boolean' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. | |
let bool: boolean = <boolean>n; | |
// 双重断言 | |
let bool: boolean = n as any as boolean; |
# 类型扩宽
所有通过
let
或var
定义的变量、函数的形参、对象的非只读属性,如果满足指定了初始值且未显式添加类型注解的条件,那么它们推断出来的类型就是指定的初始值字面量类型拓宽后的类型,这就是字面量类型拓宽 (Type Widening)。
//let 变量,在缺省显式类型注解时,类型转换为了赋值字面量类型的父类型 | |
let str = 'this is string'; // 类型是 string | |
let strFun = (str = 'this is string') => str; // 类型是 (str?: string) => string; | |
// 通过 let、var 定义的变量如果满足未显式声明类型注解且被赋予了 null 或 undefined 值,则推断出这些变量的类型是 any | |
let x = null; // 类型拓宽成 any | |
let y = undefined; // 类型拓宽成 any | |
// 将 const 定义为一个不可变更的常量,在缺省类型注解的情况下,TypeScript 推断出它的类型直接由赋值字面量的类型决定 | |
const specifiedStr = 'this is string'; // 类型是 'this is string' | |
let str2 = specifiedStr; // 类型是'string' | |
let strFun2 = (str = specifiedStr) => str; // 类型是 (str?: string) => string; |
# 类型缩小
在
TypeScript
中,我们可以通过某些操作将变量的类型由一个较为宽泛的集合缩小到相对较小、较明确的集合,这就是 "Type Narrowing"。
function func(anything: string | number) { | |
if (typeof anything === 'string') { | |
return anything; // 类型是 string | |
} else { | |
return anything; // 类型是 number | |
} | |
} |
你可以使用下面几个方式进行类型缩小
- 类型守卫:
typeof
- 类型判断:
===
- 控制流语句:
if
,三目运算符,switch
分支
# 类型别名
类型别名用来给一个类型起个新名字
注意:类型别名,诚如其名,即我们仅仅是给类型取了一个新的名字,并不是创建了一个新的类型。
type Args = Array<string> | Array<number>; | |
function log(...args: Args) { | |
console.log(...args); | |
} |
# 接口
在 TypeScript 中,我们使用接口(Interfaces)来定义对象的类型。
# 什么是接口
在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(classes)去实现(implement)。
TypeScript 中的接口是一个非常灵活的概念,除了可用于 ++「 对类的一部分行为进行抽象」以外,也常用于对「对象的形状(Shape)」++ 进行描述。
# 简单的示例
interface Person { | |
name: string; | |
age: number; | |
} | |
// 赋值的时候,变量的形状必须和接口的形状保持一致 | |
// 多一个属性或者少一个属性都是不可以的 | |
let tom: Person = { | |
name: 'Tom', | |
age: 25, | |
// Object literal may only specify known properties, and 'gender' does not exist in type 'Person'. | |
gender: 'male' | |
}; |
# 可选 & 只读属性
interface Person { | |
// 全部属性都是只读的 不允许修改 | |
// 其中 gender 是可选的变量 | |
readonly name: string; | |
readonly age: number; | |
readonly gender ?: boolean; | |
} | |
let tom: Person = { | |
name: 'Sakura', | |
age: 16, | |
}; | |
// Attempt to assign to const or readonly variable | |
tom.name = "Snow" |
# 任意属性
有时候我们希望一个接口中除了包含必选和可选属性之外,还允许有其他的任意属性,这时我们可以使用 索引签名 的形式来满足上述要求。
interface Person { | |
readonly name: string; | |
readonly age: number; | |
readonly gender ?: boolean; | |
// 任意属性 | |
[propName: string]: any; | |
} | |
let tom: Person = { | |
name: 'Sakura', | |
age: 16, | |
data: {} | |
}; |
需要注意的是,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集
也就是下面的代码是不允许的
interface Person { | |
readonly name: string; | |
// Property 'age' of type 'number' is not assignable to string index type 'string'. | |
readonly age: number; | |
readonly gender ?: boolean; | |
// 任意属性 | |
[propName: string]: string; | |
} |
# 接口和类型别名的区别
实际上,在大多数的情况下使用接口类型和类型别名的效果等价,但是在某些特定的场景下这两者还是存在很大区别。
TypeScript 的核心原则之一是对值所具有的结构进行类型检查。 而接口的作用就是为这些类型命名和为你的代码或第三方代码定义数据模型。
type (类型别名) 会给一个类型起个新名字。 type 有时和 interface 很像,但是可以作用于原始值(基本类型),联合类型,元组以及其它任何你需要手写的类型。起别名不会新建一个类型 - 它创建了一个新 名字来引用那个类型。给基本类型起别名通常没什么用,尽管可以做为文档的一种形式使用。
# 语法不同
两者都可以用来描述对象或函数的类型,但是语法不同。
// interface | |
interface Point { | |
x: number; | |
y: number; | |
} | |
interface SetPoint { | |
(x: number, y: number): void; | |
} | |
// type | |
type Point = { | |
x: number; | |
y: number; | |
}; | |
type SetPoint = (x: number, y: number) => void; |
# 接口可以自动合并
与类型别名不同,接口可以定义多次,会被自动合并为单个接口。
interface Point { x: number; } | |
interface Point { y: number; } | |
const point: Point = { x: 1, y: 2 }; |
利用这点可以对系统数据结构进行扩展
interface Array<T> { | |
remove(index : number) : Array<T> | |
} |
# 接口可以在声明函数类型时同时声明一些属性
// 无法再声明其他的属性 | |
type Type = (num : number) => number; | |
interface IType { | |
(num : number) : void; | |
_cache : Array<number> | |
} | |
const foo : IType = function () { | |
} | |
foo._cache = [] |
我也很少用,也就在刷 leetcode
时写斐波那契数列时用过
# type 可以声明一些 interface 无法表示的类型,比如 union 和 tuple
// union | |
type PartialPoint = PartialPointX | PartialPointY; | |
// tuple | |
type Data = [number, string]; |
# 扩展
// 接口扩展接口 | |
interface PointX { | |
x: number | |
} | |
interface Point extends PointX { | |
y: number | |
} | |
// 类型别名扩展类型别名 | |
type PointX = { | |
x: number | |
} | |
type Point = PointX & { | |
y: number | |
} | |
// 接口扩展类型别名 | |
type PointX = { | |
x: number | |
} | |
interface Point extends PointX { | |
y: number | |
} | |
// 类型别名扩展接口 | |
interface PointX { | |
x: number | |
} | |
type Point = PointX & { | |
y: number | |
} |
# 类
传统的面向对象语言都是基于类的,而 JavaScript 是基于原型的。在 ES6 中拥有了 class 关键字,虽然它的本质依旧是构造函数,但是能够让开发者更舒服的使用 class 了。 TypeScript 作为 JavaScript 的超集,自然也是支持 class 全部特性的,并且还可以对类的属性、方法等进行静态类型检测。
# 基本概念
# 基本使用
interface PointInterface { | |
x: number; | |
y: number; | |
} | |
class Point implements PointInterface { | |
x: number; | |
y: number; | |
constructor(x: number, y: number) { | |
this.x = x; | |
this.y = y; | |
} | |
// 注意,方法是直接设置在 Point.prototype 上的 | |
getPosition() { | |
return `(${this.x}, ${this.y})`; | |
} | |
} | |
const point = new Point(1, 2); | |
point.getPosition() // (1, 2) |
# 继承
class Animal { | |
name: string; | |
constructor(name: string) { | |
this.name = name; | |
} | |
eat() { | |
console.log("eat"); | |
} | |
} | |
// 使用 extend 继承 | |
class Cat extends Animal { | |
age: number; | |
constructor(name: string, age: number) { | |
super(name); | |
this.age = age; | |
} | |
play() { | |
console.log("play"); | |
} | |
} |
如上, Cat
继承 Animal
,那 Animal
被称为父类(超类), Cat
被称为子类(派生类)。此时 Cat
的实例继承了基类 Animal
的属性和方法。
需要注意,派生类如果包含一个构造函数 constructor
,则必须在构造函数中调用 super()
方法,这是 TypeScript
强制执行的一条重要规则。否则就会报错: Constructors for derived classes must contain a 'super' call.
那这个 super()
有什么作用呢?其实这里的 super
函数会调用基类的构造函数,用于数据初始化。
# 类的修饰符
在 ES6 标准类的定义中,默认情况下,定义在实例的属性和方法会在创建实例后添加到实例上;
而如果是定义在类里没有定义在 this
上的方法,实例可以继承这个方法;而如果使用 static
修饰符定义的属性和方法,是静态属性和静态方法,实例是没法访问和继承到的。
传统面向对象语言通常都有访问修饰符,可以通过修饰符来控制可访问性。 TypeScript
中有三类访问修饰符:
public
:修饰的是在任何地方可见、公有的属性或方法;private
:修饰的是仅在同一类中可见、私有的属性或方法;protected
:修饰的是仅在类自身及子类中可见、受保护的属性或方法。
此外,在类中可以使用 readonly
关键字将属性设置为只读
class Stack<T> { | |
// 私有属性 | |
private readonly arr: Array<T> = []; | |
// 静态属性 | |
public static readonly Name = "Stack"; | |
public static isStack(o: any): o is Stack<any> { | |
return o instanceof Stack; | |
} | |
// 访问器属性 | |
get length() { | |
return this.arr.length; | |
} | |
// 方法 | |
public push(...args: Array<T>) { | |
this.arr.push(...args); | |
} | |
public pop(): T { | |
let v = this.arr[this.length - 1]; | |
this.arr.length--; | |
return v; | |
} | |
public isEmpty() { | |
return !this.length; | |
} | |
protected forEach(fun: Function) { | |
let cache = this.arr.splice(0, this.arr.length); | |
cache.forEach((value) => { | |
fun(value); | |
}) | |
} | |
} | |
let stack = new Stack<number>(); | |
stack.push(1); | |
stack.push(2); | |
console.log(stack.pop()); | |
console.log(stack.pop()); | |
console.log(stack.length); | |
console.log(Stack.isStack(stack)); | |
console.log(Stack.Name); |
# 抽象类
抽象类是一种不能被实例化的类,它的目的就是用来继承的,抽象类里面可以有抽象的成员,就是自己不实现,等着子类去实现。
- 抽象类和抽象成员,都是用
abstract
修饰 - 抽象类中还是可以有具体实现的,这样子类如果不实现,可以继承抽象类中的实现。
abstract class Animal { | |
abstract makeSound(): void; | |
move(): void { | |
console.log('roaming the earch...'); | |
} | |
} |
# 泛型
# 泛型的定义
软件工程中,我们不仅要创建一致的定义良好的 API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。
在像 C# 和 Java 这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。
设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:类的实例成员、类的方法、函数参数和函数返回值。
举个例子
function identity <T, U>(value: T, message: U) : T { | |
console.log(message); | |
return value; | |
} | |
console.log(identity<Number, string>(68, "Semlinker")); |
其中 T
代表 Type,在定义泛型时通常用作第一个类型变量名称。但实际上 T
可以用任何有效名称代替。除了 T
之外,以下是常见泛型变量代表的意思:
- K(Key):表示对象中的键类型;
- V(Value):表示对象中的值类型;
- E(Element):表示元素类型。
# 泛型的使用
# 在接口中使用
interface Identities<V, M> { | |
value: V, | |
message: M | |
} | |
function identity<T, U> (value: T, message: U): Identities<T, U> { | |
console.log(value + ": " + typeof (value)); | |
console.log(message + ": " + typeof (message)); | |
let res: Identities<T, U> = { | |
value, | |
message | |
}; | |
return res; | |
} | |
//ts 自动推断类型了 | |
console.log(identity(16, "Sakura")); | |
// 写全是这样的 | |
console.log(identity<number, string>(16, "Sakura")) |
# 在类中使用
直接拿 Array
举例
interface Array<T> { | |
/** | |
* Gets or sets the length of the array. This is a number one higher than the highest element defined in an array. | |
*/ | |
length: number; | |
/** | |
* Removes the last element from an array and returns it. | |
*/ | |
pop(): T | undefined; | |
/** | |
* Appends new elements to an array, and returns the new length of the array. | |
* @param items New elements of the Array. | |
*/ | |
push(...items: T[]): number; | |
/** | |
* Calls a defined callback function on each element of an array, and returns an array that contains the results. | |
* @param callbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array. | |
* @param thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value. | |
*/ | |
map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[]; | |
} | |
interface ArrayConstructor { | |
new(arrayLength?: number): any[]; | |
<T>(arrayLength: number): T[]; | |
isArray(arg: any): arg is any[]; | |
readonly prototype: any[]; | |
} | |
declare var Array: ArrayConstructor; |
用法
// 大伙都很熟悉 | |
let arr : Array<number> = new Array<number>(); | |
arr.push(1); |
# 泛型约束
泛型的一个应用场景是确保属性存在,有时候,我们希望类型变量对应的类型上存在某些属性。这时,除非我们显式地将特定属性定义为类型变量,否则编译器不会知道它们的存在。
interface Length { | |
length: number; | |
} | |
// T extends Length 用于告诉编译器,我们支持已经实现 Length 接口的任何类型。 | |
function identity<T extends Length>(arg: T): T { | |
console.log(arg.length); // 可以获取 length 属性 | |
return arg; | |
} |
泛型约束的另一个常见的使用场景就是检查对象上的键是否存在。不过在看具体示例之前,我们得来了解一下 keyof
操作符, keyof
操作符是在 TypeScript 2.1 版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型。
interface Person { | |
name: string; | |
age: number; | |
location: string; | |
} | |
type K1 = keyof Person; // "name" | "age" | "location" | |
type K2 = keyof Person[]; // number | "length" | "push" | "concat" | ... | |
type K3 = keyof { [x: string]: Person }; // string | number |
通过 keyof
操作符,我们就可以获取指定类型的所有键,之后我们就可以结合前面介绍的 extends
约束,即限制输入的属性名包含在 keyof
返回的联合类型中。
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { | |
return obj[key]; | |
} |
# 泛型参数默认类型
一看就懂,和函数一样的
interface User<T = string> { | |
name: T; | |
} | |
const strA: User = {name: "Sakura"}; | |
const numB: User<number> = {name: 101}; |
# 高级语法 / 用法
# typeof
typeof
的主要用途是在类型上下文中获取变量或者属性的类型
interface Person { | |
name: string; | |
age: number; | |
} | |
const sakura: Person = { name: "Sakura", age: 16 }; | |
type SakuraType = typeof sakura; // type SakuraType = Person |
# keyof
keyof
操作符是在 TypeScript 2.1 版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型。
为了同时支持两种索引类型,就得要求数字索引的返回值必须是字符串索引返回值的子类。其中的原因就是当使用数值索引时,JavaScript 在执行索引操作时,会先把数值索引先转换为字符串索引。所以 keyof { [x: string]: Person }
的结果会返回 string | number
。
interface Person { | |
name: string; | |
age: number; | |
} | |
type K1 = keyof Person; // "name" | "age" | |
type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join" | |
type K3 = keyof { [x: string]: Person }; // string | number | |
interface StringArray { | |
// 字符串索引 -> keyof StringArray => string | number | |
[index: string]: string; | |
} | |
interface StringArray1 { | |
// 数字索引 -> keyof StringArray1 => number | |
[index: number]: string; | |
} | |
//keyof 也支持基本数据类型 | |
let K1: keyof boolean; // let K1: "valueOf" | |
let K2: keyof number; // let K2: "toString" | "toFixed" | "toExponential" | ... | |
let K3: keyof symbol; // let K1: "valueOf" |
# in
in
用来遍历联合类型,主要用于数组和对象的构建
type Keys = "a" | "b" | "c" | |
type Obj = { | |
[p in Keys]: any | |
} // -> { a: any, b: any, c: any } |
# infer
infer
用于提取那个位置的类型值
function getSchool(name: string, age: number, address: string) { | |
return {name, age, address} | |
} | |
// T 是 getSchool 的类型 。 T extends ((...args: any []) => infer R) | |
type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : any | |
type MyReturnType = ReturnType<typeof getSchool>; // 这个类型很好用 | |
type Parameters<T extends (...args: any[]) => any> = T extends (...args: infer P) => any ? P : never | |
type MyParamaters = Parameters<typeof getSchool> |
以上代码中 infer R
就是声明一个变量来承载传入函数签名的返回值类型,简单说就是用它取到函数返回值的类型方便之后使用。
# 索引类型
// T [K] 表示对象 T 的属性 K 所表示的类型,在上述例子中,T [K][] 表示变量 T 取属性 K 的值的数组 | |
// 通过 [] 索引类型访问操作符,我们就能得到某个索引的类型 | |
class Person { | |
name: string; | |
age: number; | |
} | |
type MyType = Person['name']; //Person 中 name 的类型为 string, 所以 type MyType = string |
用法
interface Person { | |
name: string; | |
age: number; | |
} | |
const person: Person = { | |
name: 'sakura', | |
age: 16 | |
} | |
function getValues<T, K extends keyof T>(person: T, keys: K[]): T[K][] { | |
return keys.map(key => person[key]); | |
} | |
getValues(person, ['name']) // ['sakura'] | |
getValues(person, ['gender']) // 报错: Type "gender" is not assignable to type "name" | "age". |
# 映射类型
根据旧的类型创建出新的类型,我们称之为映射类型
常见的有
# Partial
Partial<T>
将类型的属性变成可选
type Partial<T> = { | |
[P in keyof T]?: T[P]; | |
}; | |
// Initial type: {name?: string, age?: number} | |
type PartialPerson = Partial<Person>; |
# Required
Required 将类型的属性变成必选
type Required<T> = { | |
[P in keyof T]-?: T[P] | |
}; |
# Readonly
Readonly<T>
的作用是将某个类型所有属性变为只读属性,也就意味着这些属性不能被重新赋值。
type Readonly<T> = { | |
readonly [P in keyof T]: T[P]; | |
}; |
# Pick
Pick 从某个类型中挑出一些属性出来
type Pick<T, K extends keyof T> = { | |
[P in K]: T[P]; | |
}; |
# Record
Record
的作用是将K
中所有的属性的值转化为T
类型。
type Record<K extends keyof any, T> = { | |
[P in K]: T; | |
}; |
# ReturnType
用来得到一个函数的返回值类型
理解为:如果 T
继承了 extends (...args: any[]) => any
类型,则返回类型 R
,否则返回 any
。其中 R
是什么呢? R
被定义在 extends (...args: any[]) => infer R
中,即 R 是从传入参数类型中推导出来的。
type ReturnType<T extends (...args: any[]) => any> = | |
T extends (...args: any[]) => infer R ? R : any; |
# Parameters
Parameters<T>
的作用是用于获得函数的参数类型组成的元组类型。
type Parameters<T extends (...args: any) => any> = | |
T extends (...args: infer P) => any ? P : never; |
# Exclude
Exclude
的作用是将某个类型中属于另一个的类型移除掉。
// 因为 extends 是分配式的,所以这么写是 ok 的 | |
type Exclude<T, U> = T extends U ? never : T; |
如果 T
能赋值给 U
类型的话,那么就会返回 never
类型,否则返回 T
类型。最终实现的效果就是将 T
中某些属于 U
的类型移除掉。
用法
type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c" | |
type T1 = Exclude<"a" | "b" | "c", "a" | "b">; // "c" | |
type T2 = Exclude<string | number | (() => void), Function>; // string | number |
# Extract
Extract<T, U>
的作用是从T
中提取出U
type Extract<T, U> = T extends U ? T : never; |
用法
type T0 = Extract<"a" | "b" | "c", "a" | "f">; // "a" | |
type T1 = Extract<string | number | (() => void), Function>; // () =>void |
# Omit
Omit<T, K extends keyof any>
的作用是使用T
类型中除了K
类型的所有属性,来构造一个新的类型。
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>; |
用法
interface Todo { | |
title: string; | |
description: string; | |
completed: boolean; | |
} | |
type TodoPreview = Omit<Todo, "description">; | |
const todo: TodoPreview = { | |
title: "Clean room", | |
completed: false, | |
}; |
# NonNullable
NonNullable<T>
的作用是用来过滤类型中的null
及undefined
类型。
type NonNullable<T> = T extends null | undefined ? never : T; |
用法
type T0 = NonNullable<string | number | undefined>; // string | number | |
type T1 = NonNullable<string[] | null | undefined>; // string[] |
# 其他
# tsconfig.json 的配置
{ | |
"compilerOptions": { | |
/* 基本选项 */ | |
"target": "es5", // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT' | |
"module": "commonjs", // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015' | |
"lib": [], // 指定要包含在编译中的库文件 | |
"allowJs": true, // 允许编译 javascript 文件 | |
"checkJs": true, // 报告 javascript 文件中的错误 | |
"jsx": "preserve", // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react' | |
"declaration": true, // 生成相应的 '.d.ts' 文件 | |
"sourceMap": true, // 生成相应的 '.map' 文件 | |
"outFile": "./", // 将输出文件合并为一个文件 | |
"outDir": "./", // 指定输出目录 | |
"rootDir": "./", // 用来控制输出目录结构 --outDir. | |
"removeComments": true, // 删除编译后的所有的注释 | |
"noEmit": true, // 不生成输出文件 | |
"importHelpers": true, // 从 tslib 导入辅助工具函数 | |
"isolatedModules": true, // 将每个文件做为单独的模块 (与 'ts.transpileModule' 类似). | |
/* 严格的类型检查选项 */ | |
"strict": true, // 启用所有严格类型检查选项 | |
"noImplicitAny": true, // 在表达式和声明上有隐含的 any 类型时报错 | |
"strictNullChecks": true, // 启用严格的 null 检查 | |
"noImplicitThis": true, // 当 this 表达式值为 any 类型的时候,生成一个错误 | |
"alwaysStrict": true, // 以严格模式检查每个模块,并在每个文件里加入 'use strict' | |
/* 额外的检查 */ | |
"noUnusedLocals": true, // 有未使用的变量时,抛出错误 | |
"noUnusedParameters": true, // 有未使用的参数时,抛出错误 | |
"noImplicitReturns": true, // 并不是所有函数里的代码都有返回值时,抛出错误 | |
"noFallthroughCasesInSwitch": true, // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿) | |
/* 模块解析选项 */ | |
"moduleResolution": "node", // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6) | |
"baseUrl": "./", // 用于解析非相对模块名称的基目录 | |
"paths": {}, // 模块名到基于 baseUrl 的路径映射的列表 | |
"rootDirs": [], // 根文件夹列表,其组合内容表示项目运行时的结构内容 | |
"typeRoots": [], // 包含类型声明的文件列表 | |
"types": [], // 需要包含的类型声明文件名列表 | |
"allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入。 | |
/* Source Map Options */ | |
"sourceRoot": "./", // 指定调试器应该找到 TypeScript 文件而不是源文件的位置 | |
"mapRoot": "./", // 指定调试器应该找到映射文件而不是生成文件的位置 | |
"inlineSourceMap": true, // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件 | |
"inlineSources": true, // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性 | |
/* 其他选项 */ | |
"experimentalDecorators": true, // 启用装饰器 | |
"emitDecoratorMetadata": true // 为装饰器提供元数据的支持 | |
} | |
} |
# 参考
https://juejin.cn/post/7050290769562697736
https://juejin.cn/post/6844904184894980104
https://juejin.cn/post/7000182870404759589
https://blog.csdn.net/lhjuejiang/article/details/119038312
https://juejin.cn/post/6999441997236797470
https://juejin.cn/post/6844904146877808653
https://juejin.cn/post/6844904146877808653