应用场景: TS 解决大型项目中,架构以及代码共建等复杂维护场景。
尝试通过一张图来描述 TypeScript 类型系统 的全景。
类型系统
unknown & any & null & undefined & void & never
unknown
let unknownValue: unknown
unknownValue = 'string'
unknownValue = 1
unknownValue = true
const unKnownValue2: boolean = unknownValue // ERROR
const unKnownValue3: any = unknownValue // SUCCESS
let unKnownValue4: boolean
if (typeof unknownValue === 'boolean') {
unKnownValue4 = unknownValue // SUCCESS
}
any
let anyValue: any
anyValue = 100
anyValue = 'hello world!'
anyValue = true
anyValue = {}
anyValue = []
null & undefined
const nullValue: null = null
const undefinedValue: undefined = undefined
void
function foo(x: number, y: number): void {}
never
function errorFunc(msg:string): never {
throw new Error()
}
function errorFunc2(): never {
return errorFunc('error')
}
function infiniteLoop(): never {
while (true) {}
}
string & number & boolean & symbol & bigint & literal
// string & number & boolean & symbol & bigint
let strValue: string = 'hello world' // SUCCESS
strValue = 100 // ERROR
let numberValue: number = 100_000_000 // SUCCESS
numberValue = 100000000 // SUCCESS
numberValue = '100000000' // ERROR
let boolean: boolean = true
boolean = false
boolean = !strValue
let symbolValue: symbol = Symbol()
symbolValue = Symbol('x')
let uniqueSymbolValue: unique symbol = Symbol() // ERROR: 'unique symbol' type must be const
const uniqueSymbolValue2: unique symbol = Symbol() // SUCCESS
const symValue:unique symbol = Symbol("x")
const symValue2:unique symbol = symValue // ERROR
let bigintValue: bigint = 1n
bigintValue = 100 // ERROR: Type number is not assignable to type bigint
// literal
let literalValue : 'hello' | 'world' | '!' = 'hello'
object & Object & {}
- {}包含了除了null跟undefined所有的类型
- {}跟unknown的区别,unknown可以包含null跟undefined
- 类似于{},所有拥有Object原型的值都能赋给Object作为类型的变量。
- object跟{}有一点不同,它不包含原始类型。
用于一些复合对象的情况,选object
复合对象跟原始类型皆可的情况,选{}
如果要包含null跟undefined的情况,用unknown
参考文章: https://zhuanlan.zhihu.com/p/668791106
function foo(o: object) {}
class Names {}
foo({}) // SUCCESS
foo({ name: 'xxx' }) // SUCCESS
foo(() => {}) // SUCCESS
foo(RegExp('')) // SUCCESS
foo([]) // SUCCESS
foo(function () {}) // SUCCESS
foo(new Date()) // SUCCESS
foo(new Names())
foo('hello') // ERROR
foo(123) // ERROR
foo(true) // ERROR
foo(Symbol()) // ERROR
foo(1n) // ERROR
foo(null) // ERROR
foo(undefined) // ERROR
foo(new Proxy()) // ERROR
foo(new Promise()) // ERROR
function FOO(o: Object) {}
FOO({}) // SUCCESS
FOO({ name: 'xxx' }) // SUCCESS
FOO(() => {}) // SUCCESS
FOO(RegExp('')) // SUCCESS
FOO([]) // SUCCESS
FOO(function () {}) // SUCCESS
FOO(new Date()) // SUCCESS
FOO(new Names())
FOO('hello') // SUCCESS: 原始值的包装类型
FOO(123) // SUCCESS: 原始值的包装类型
FOO(true) // SUCCESS: 原始值的包装类型
FOO(Symbol()) // SUCCESS: 原始值的包装类型
FOO(1n) // SUCCESS: 原始值的包装类型
FOO(null) // ERROR
FOO(undefined) // ERROR
FOO(new Proxy()) // ERROR
FOO(new Promise()) // ERROR
function fkk(o2: {}) {}
fkk({})
fkk({ name: 'hello' })
fkk(Object.create(null))
fkk(Object.create({}))
fkk(Object.create({ name: 'hello' }))
fkk('123')
fkk(100)
fkk(null) // ERROR
fkk(undefined) // ERROR
const o2: {} = { name: '123' }
o2.key = 100 // ERROR: Property key does not exist on type {}
fkk(o2) // SUCCESS
array & tuple
// array
const strArr: string[] = ['1', '2', '3']
const numArr: number[] = [1, 2, 3, 4, 5]
const heterogeneousArr: (string | number | null)[] = [1, '2', null]
const symbol: Array<symbol> = [Symbol(), Symbol(), Symbol()]
type HA = string | number | {} | null | undefined
let heterogeneousArr2: Array<HA>
// tuple(元组): 限制数量和类型
const tupleArr: [string, number] = ['1', 2]
enum
// enum
// 1.number enum
// - 自动递增
enum SIZE {
SMALL, // 0
MIDDLE, // 1
BIG // 2
}
enum SIZE2 {
SMALL = 1, // 1
MIDDLE, // 2
BIG // 3
}
// 2.string enum
enum COLOR {
RED = 'RED',
BLUE = 'Blue',
YELLOW = 'YELLOW'
// OTHER // Enum member must have initializer
}
// 3.heterogeneous enum
enum HETEROGENEOUS {
SIZE,
COLOR = 'COLOR_VALUE'
}
// 4.反向映射
// - 不会为字符串枚举成员生成反向映射
const k = SIZE[1]
console.log(k) // MIDDLE
interface & type
- 概念定义: type 声明类型别名的关键字 & interface 接口用于描述对象。
- 表示范围: type 可以描述任何类型以及类型运算; interface只能描述对象类型。
- 声明合并: type 不可以重复声明; interface 存在声明合并。
- 导出方式: interface 可以直接 export; type需要先声明再 export。
- 继承实现: interface 通过 extedns, type 通过 &实现继承。 type 除联合类型可以 implements。
- 映射类型: type支持映射类型 type Keys = keyof T; interface 不支持
- 函数声明: type 可以直接声明函数类型; interface 描述函数签名。
- 类型别名可以通过 typeof 获取实例类型。
- 由于interfac可以进行声明合并,所以总有可能将新成员添加到同一个interface定义的类型上,也就是说
interface
定义的类型是不确定的;而type一旦声明类型,就无法更改它们。因此,索引签名是已知的。
官方推荐用 interface,其他无法满足需求的情况下用 type。
但联合类型 和 交叉类型 是很常用的,所以避免不了大量使用 type 的场景,一些复杂类型也需要通过组装后形成类型别名来使用。
所以,如果想保持代码统一,还是可选择使用 type。通过上面的对比,类型别名 其实可涵盖 interface 的大部分场景。
编写三方库时使用interface,其更加灵活自动的类型合并可应对未知的复杂使用场景。
联合类型( | ) 与 交叉类型 ( & )
联合类型( | ) 满足一个类型条件即可, 不存在属性冲突
interface Cat {
name: string
age: number
purr(): void
}
interface Dog {
name: string
age: number
bark(): void
}
let Animal: Cat | Dog
Animal = {
name: 'mimi',
age: 4,
purr() {}
}
Animal = {
name: 'lele',
age: 4,
bark() {},
}
交叉类型 ( & ) 合并属性
interface Cat {
name: string
age: number
purr(): void
}
interface Dog {
name: string
age: number
bark(): void
}
const Animal: Cat & Dog = {
name: 'mimi',
age: 4,
purr() {},
bark() {}
}
-
会存在属性冲突
比如 cat 的 age 为 number, dog 的 age 为 string
此时 使用交叉类型 age 会被识别为
nerver
, 因为不可能存在既是string又是number
interface Cat {
name: string
age: number
purr(): void
}
interface Dog {
name: string
age: string
bark(): void
}
const Animal: Cat & Dog = {
name: 'mimi',
age: 4, // Error: Type number is not assignable to type never
purr() {},
bark() {}
}
索引类型
索引类型查询操作符 keyof T
索引访问操作符 T[K]
function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
return names.map(n => o[n]);
}
interface Person {
name: string;
age: number;
}
let person: Person = {
name: 'Jarid',
age: 35
};
let strings: string[] = pluck(person, ['name']); // ok, string[]
keyof T // 数据 T中的所有索引
T[K] // 索引访问 比如 string | number
映射类型
type Kes = keyof T
type Readonly = {
readonly [P in keyof T]: T[P]
}
type Keys = 'option1' | 'option2';
type Flags = { [K in Keys]: boolean };
// 索引如果是 Keys 其中的一个则设置为 布尔类型
interface Map<T> {
[key: string]: T;
}
let keys: keyof Map<number>; // string
let value: Map<number>['foo']; // number
工具类型
TypeScript的标准库中的类型
type Readonly<T> = {
readonly [P in keyof T]: T[P];
}
type Partial<T> = {
[P in keyof T]?: T[P];
}
type Nullable<T> = { [P in keyof T]: T[P] | null }
type Partial<T> = { [P in keyof T]?: T[P] }
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
}
type Record<K extends string, T> = {
[P in K]: T;
}
Exclude<T, U>
-- 从T
中剔除可以赋值给U
的类型。Extract<T, U>
-- 提取T
中可以赋值给U
的类型。NonNullable<T>
-- 从T
中剔除null
和undefined
。ReturnType<T>
-- 获取函数返回值类型。InstanceType<T>
-- 获取构造函数类型的实例类型
函数
let myAdd = function(x: number, y: number): number { return x + y; };
声明函数签名
// 声明函数签名
type Func = {
description: string;
(arg: string): void;
}
构造签名
构造签名用于描述类的构造函数, 但构造函数不能通过构造签名声明,也不能实现构造签名。
构造签名的应用场景主要就是“工厂函数”,函数接收一个类,返回该类实例化的对象。
// 通过接口来描述构造签名
interface Constructor {
new (name: string, age: number): { name: string, age: number }
}
// 构造签名描述的构造函数所在的类
class Person {
constructor(public name: string, public age: number) {}
}
// 工厂函数
function factory(constructor: Constructor, name: string, age: number): Person {
return new constructor(name, age)
}
// 构造函数通过类名调用,所以将Person类传递过去
console.log(factory(Person, 'Danny', 21))
剩余参数
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
this
interface Deck {
suits: string[];
cards: number[];
createCardPicker(this: Deck): () => Card;
}
重载
传入不同参数而返回不同类型的数据
let suits = ["hearts", "spades", "clubs", "diamonds"];
function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): {suit: string; card: number; };
function pickCard(x): any {
// Check to see if we're working with an object/array
// if so, they gave us the deck and we'll pick the card
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
}
// Otherwise just let them pick the card
else if (typeof x == "number") {
let pickedSuit = Math.floor(x / 13);
return { suit: suits[pickedSuit], card: x % 13 };
}
}
let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);
let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);
泛型
- 泛型变量
- 泛型类型
- 泛型类
- 泛型约束
- 无法创建泛型枚举和泛型命名空间
// 泛型变量
function loggingIdentity<T>(arg: Array<T>): Array<T> {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
泛型类型
// 自定义泛型类型
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
泛型类
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
泛型约束
T extends Lengthwise: 至少包含Lengthwise的属性
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}
在泛型里使用类类型
function create<T>(c: {new(): T; }): T {
return new c();
}
断言 - 尖括号 & as & 非空断言操作符
let anyValue: any
<T>anyValue
anyValue as T
value! // 非空断言操作符
anyValue! // 类型推断排除 null、undefined 表示此处一定有值
类型守卫 & 类型区分
- 属性分类场景 - in 操作符
interface Dog {
bark: () => void;
}
interface Cat {
meow: () => void;
}
function makeSound(animal: Dog | Cat) {
if ('bark' in animal) {
animal.bark();
} else {
animal.meow();
}
}
const dog: Dog = { bark: () => console.log('Woof!') };
const cat: Cat = { meow: () => console.log('Meow!') };
makeSound(dog); // 输出: Woof!
makeSound(cat); // 输出: Meow!
- 类型分类场景 - typeof | instanceof 操作符
// 使用 typeof 进行基本类型分类
function formatValue(value: string | number) {
if (typeof value === 'string') {
return value.toUpperCase();
} else {
return value.toFixed(2);
}
}
console.log(formatValue('hello')); // 输出: HELLO
console.log(formatValue(123.456)); // 输出: 123.46
// 使用 instanceof 进行复杂类型分类
class Fish {
swim() {
console.log('Swimming');
}
}
class Bird {
fly() {
console.log('Flying');
}
}
function move(animal: Fish | Bird) {
if (animal instanceof Fish) {
animal.swim();
} else {
animal.fly();
}
}
const fish = new Fish();
const bird = new Bird();
move(fish); // 输出: Swimming
move(bird); // 输出: Flying
- 类型谓词 谓词 parameterName is Type
这种形式
interface Car {
drive: () => void;
}
interface Bike {
pedal: () => void;
}
function isCar(vehicle: Car | Bike): vehicle is Car {
return (vehicle as Car).drive !== undefined;
}
function operate(vehicle: Car | Bike) {
if (isCar(vehicle)) {
vehicle.drive();
} else {
vehicle.pedal();
}
}
const car: Car = { drive: () => console.log('Driving') };
const bike: Bike = { pedal: () => console.log('Pedaling') };
operate(car); // 输出: Driving
operate(bike); // 输出: Pedaling
类
基类 & 派生类(super) & 继承 & 实现
public & private & protected 修饰器
- public: 可以自由的访问程序里定义的成员, 默认 public
- private: 不能在声明它的类的外部访问
- protected:
protected
成员在派生类中仍然可以访问
继承 & 实现
- 继承 extends: 类从基类中继承了属性和方法。
- 实现 implements(interface)
静态属性 static
只有类本身可以调用。
abstract 抽象类
抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。 不同于接口,抽象类可以包含成员的实现细节。 abstract
关键字是用于定义抽象类和在抽象类内部定义抽象方法。
abstract class Animal {
abstract makeSound(): void;
move(): void {
console.log('roaming the earch...');
}
}
构造函数
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
class DigitalClock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("beep beep");
}
}
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
return new ctor(hour, minute);
}
let digital = createClock(DigitalClock, 12, 17);
使用类的例子
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter = new Greeter("world");
装饰器
tsconfig.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。 装饰器使用 @expression
这种形式,expression
求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。
function configurable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value;
};
}
class Point {
private _x: number;
private _y: number;
constructor(x: number, y: number) {
this._x = x;
this._y = y;
}
@configurable(false)
get x() { return this._x; }
@configurable(false)
get y() { return this._y; }
}
求值顺序
function f() {
console.log("f(): evaluated");
return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("f(): called");
}
}
function g() {
console.log("g(): evaluated");
return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("g(): called");
}
}
class C {
@f()
@g()
method() {}
}
// 洋葱模型
// f(): evaluated
// g(): evaluated
// g(): called
f(): called
声明合并
interface - 接口合并
最简单也最常见的声明合并类型是接口合并。 从根本上说,合并的机制是把双方的成员放到一个同名的接口里。
函数成员
对于函数成员,每个同名函数声明都会被当成这个函数的一个重载。 接口 A
与后来的接口 A
合并时,后面的接口具有更高的优先级。
非函数成员
接口的非函数的成员应该是唯一的。如果它们不是唯一的,那么它们必须是相同的类型。如果两个接口中同时声明了同名的非函数成员且它们的类型不同,则编译器会报错。
结语
通过这篇文章,相信大家已经对 TypeScript 的类型系统有了全面而深入的了解。从基础类型到高级类型,再到函数、泛型、类与装饰器,每个部分都展示了 TypeScript 强大的类型检查能力和灵活性。🎓 丰富的示例代码和详细的解释,帮助你在实际项目中更好地运用这些知识,编写出更加健壮和可维护的代码。💪
TypeScript 的类型系统不仅提升了代码的可靠性,也极大地提高了开发效率。希望你能在接下来的开发旅程中,充分发挥 TypeScript 的优势,创造出更多优秀的项目!🚀
感谢你的阅读,期待你在 TypeScript 的世界里不断探索和进步!🌟