TypeScript 基础详解

269 阅读11分钟

应用场景: TS 解决大型项目中,架构以及代码共建等复杂维护场景。

尝试通过一张图来描述 TypeScript 类型系统 的全景。

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 & {}

- {}包含了除了nullundefined所有的类型
- {}跟unknown的区别,unknown可以包含nullundefined

- 类似于{},所有拥有Object原型的值都能赋给Object作为类型的变量。

- object跟{}有一点不同,它不包含原始类型。

用于一些复合对象的情况,选object
复合对象跟原始类型皆可的情况,选{}
如果要包含nullundefined的情况,用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中剔除nullundefined
  • 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 的世界里不断探索和进步!🌟