一、简介

TypeScript是JavaScript的一个超集,在标准JavaScript的基础上添加了静态类型系统。它有

1、特性

  • 静态类型检查

在编写代码时可以为变量、函数参数、返回值等显式地声明数据类型(如stringnumberboolean以及自定义接口interface或类class等)。

使用静态类型检查可以在编译阶段就能检查出类型不匹配的错误,减少运行时错误,提高代码的健壮性,增强代码可读性。

  • 面向对象编程增强

TypeScript原生支持并扩展了ES6的类(class)、接口(interface)、继承(extends)、访问修饰符(publicprivateprotected)等面向对象概念。

2、工作原理

  • 使用npm install -g typescript安装TypeScript

  • 编写TypeScript文件,以.ts命名

  • 使用TypeScript编译器(tsc)将.ts文件转换成.js文件

  • 运行编译生成后的*.js文件

3、Hello World

  • hello.ts
const message:string = "Hello World!";
console.log(message);
  • 编译
tsc hello.ts

生成hello.js

var message = "Hello World!";
console.log(message);
  • 运行

运行编译生成后的hello.js:

node hello.js

输出:

Hello World!

4、配置

tsconfig.json是每个TypeScript项目的核心文件,它告诉TypeScript编译器如何处理你的代码,包含哪些文件,以及启用或禁用哪些功能等。可以通过tsc --init生成tsconfig.json文件。

主要的配置项如下:

  • compilerOptions

控制TypeScript如何编译代码。

  • include

配置用于编译的文件或文件夹。

  • exclude

配置需要排除的文件或文件夹。

  • files

明确列出需要包含的文件。

  • extends

继承另一个配置文件中的配置。

例如:

{
  "compilerOptions": {
    "target": "es2020",
    "module": "esnext",
    "strict": true,
    "baseUrl": ".",
    "paths": {
      "@app/*": ["src/app/*"]
    },
    "outDir": "dist",
    "esModuleInterop": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

二、语法

1、简单类型

  • Boolean
let isActive: boolean = true;
let hasPermission = false;
  • Number

表示整数和浮点数。

let decimal: number = 6;
//16进制
let hex: number = 0xf00d;
//2进制
let binary: number = 0b1010;
//8进制
let octal: number = 0o744;
let float: number = 3.14;
  • String
let color: string = "blue";
let fullName: string = 'John Doe';
let age: number = 30;
let sentence: string = `Hello, my name is ${fullName} and I'll be ${age + 1} next year.`;
console.log(sentence);

输出:

Hello, my name is John Doe and I'll be 31 next year.
  • BigInt

使用n作为后缀,表示大于2的53次方减1的整数。

const bigNumber: bigint = 9007199254740991n;
const hugeNumber = BigInt(9007199254740991); 
  • Symbol

通常用于创建唯一的属性键和常量。

const uniqueKey: symbol = Symbol('description');
const obj = {
  [uniqueKey]: 'This is a unique property'
};
console.log(obj[uniqueKey]);

使用tsc hello.ts --target es2016编译后运行,输出:

This is a unique property

2、特殊类型

  • any

any类型是TypeScript中最灵活的类型,本质上是告诉编译器跳过某个特定变量的类型检查。但应谨慎使用,因为它绕过了TypeScript的类型安全特性。

let u = true;
u = "string";
Math.round(u);

编译时报错:

error TS2322: Type 'string' is not assignable to type 'boolean'.
u = "string";

error TS2345: Argument of type 'boolean' is not assignable to parameter of type 'number'.
Math.round(u);

将变量设置为any类型会禁用类型检查:

let v: any = true;
v = "string";
Math.round(v);

可以成功编译。

  • unknown

unknown是类型安全的any,意思是“这可能是任何东西,所以你必须在使用前进行某种检查”。

任何值都可以赋给unknown:

let userInput: unknown;
userInput = 42;
userInput = true;
userInput = [1, 2, 3];
userInput = "Hello, World!";

使用unknown时必须先进行类型检查,如果直接使用:

console.log(userInput.length);

编译时会提示:

error TS2339: Property 'length' does not exist on type 'unknown'

需要增加类型检查:

if (typeof userInput === "string") {
	console.log(userInput.length);
}
  • never

never表示那些永远不会出现的值类型,例如:下面的函数总是抛出错误永不返回:

function throwError(message: string): never {
	throw new Error(message);
}

3、类型推断

TypeScript可以根据变量的初始值自动确定(推断)其类型:

let username = "alice";
let score = 100;
//推断类型为布尔数组
let flags = [true, false, true];
//推断返回值类型为数值
function add(a: number, b: number) {
	return a + b;
}
  • 对象字面量推断
const user = {
	name: "Alice",
	age: 30,
	isAdmin: true
};
//推断属性是否存在
console.log(user.name);
console.log(user.email);//Error

编译时会出现错误:

error TS2339: Property 'email' does not exist on type '{ name: string; age: number; isAdmin: boolean; }'.
  • 无法推断类型
// Type is 'any'
let something;  
something = 'hello';
something = 42;

无法确定正确的类型时,TypeScript会退回到any类型,从而禁用类型检查。

4、显式类型

使用显式类型可以明确声明变量的类型,通常用于:函数参数与返回值类型、对象字面量以及当初始值可能不是最终类型时。

  • 基本类型
// String
const greeting: string = "Hello, TypeScript!";

// Number
const userCount: number = 42;

// Boolean
const isLoading: boolean = true;

// Array of numbers
const scores: number[] = [100, 95, 98];
  • 类型不匹配

显式类型不匹配:

let username: string = "alice";
username = 42;//Error: Type 'number' is not assignable to type 'string'

隐式类型不匹配:

let score = 100;  
score = "high";//Error: Type 'string' is not assignable to type 'number'
  • 函数
function greet(name: string): string {
	return `Hello, ${name}!`;
}
//调用函数时会确保参数类型正确
greet("Alice");
greet(42); //Error

编译时会出现错误:

error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.

5、数组

const names: string[] = [];
names.push("Dylan");
//类型检查,编译会报错
names.push(3);
  • readonly

使用readonly可以阻止数组被修改:

const names: readonly string[] = ["Dylan"];
names.push("Jack");

编译时报错:

error TS2339: Property 'push' does not exist on type 'readonly string[]'.

6、元组

元组是一个带有预定义长度和每个索引类型的数组,它允许数组中的每个元素都是已知的值类型。

let ourTuple: [number, boolean, string];

ourTuple = [5, false, 'Coding God was here'];
  • readonly

也可以通过readonly设置为只读:

const ourReadonlyTuple: readonly [number, boolean, string] = [5, true, 'The Real Coding God'];
ourReadonlyTuple.push('Coding God took a day off');

编译时会出错:

error TS2339: Property 'push' does not exist on type 'readonly [number, boolean, string]'.
  • 命名元组与结构化

可以为每个索引命名:

const graph: [x: number, y: number] = [55.2, 41.3];
const [x,y] = graph;
console.log(x);//55.2
console.log(y);//41.3
console.log(graph[0]);//55.2

7、对象类型

  • 示例
const car: { type: string, model: string, year: number } = {
	type: "Toyota",
	model: "Corolla",
	year: 2009
};
  • 类型推断
const car = {
	type: "Toyota",
};
car.type = "Ford";
car.type = 2;//Error

编译时报错:

error TS2322: Type 'number' is not assignable to type 'string'.
  • 可选属性

下面例子中mileage为可选属性:

const car: { type: string, mileage?: number } = {
	type: "Toyota"
};
car.mileage = 2000;

如果不设置可选,则在编译时会报错:

const car: { type: string, mileage: number } = { 
	type: "Toyota",
};
car.mileage = 2000;
hello.ts:1:7 - error TS2741: Property 'mileage' is missing in type '{ type: string; }' but required in type '{ type: string; mileage: number; }'.
  • 索引签名

索引签名可用于没有定义属性列表的对象:

const nameAgeMap: { [index: string]: number } = {};
nameAgeMap.Jack = 25;
nameAgeMap.Mark = "Fifty";//Error

指定了索引类型和值类型,上面示例在编译时报错:

 error TS2322: Type 'string' is not assignable to type 'number'.

8、枚举

枚举有两种类型:字符串(string)和数值(numeric)。

默认情况下,枚举会将第一个值初始化为0,并在每个新增值上加1:

enum CardinalDirections {
	North,
	East,
	South,
	West
}
console.log(CardinalDirections.North);
console.log(CardinalDirections.South);

输出:

0
2
  • 初始化

以设置第一个数值枚举的值,后面的值依次递增:

enum CardinalDirections {
	North = 10,
	East,
	South,
	West
}
console.log(CardinalDirections.North);
console.log(CardinalDirections.South);

输出:

10
12
  • 完全初始化

可以为每个枚举值分配独特的数字值:

enum StatusCodes {
	NotFound = 404,
	Success = 200,
	Accepted = 202,
	BadRequest = 400
}
console.log(StatusCodes.NotFound);
console.log(StatusCodes.Success);

输出:

404
200
  • 字符串枚举
enum CardinalDirections {
	North = 'N',
	East = "E",
	South = "S",
	West = "W"
};
console.log(CardinalDirections.North);
console.log(CardinalDirections.West);

输出:

N
W

9、类型别名

允许用自定义名称(别名)定义类型:

//定义类型别名
type Year = number
type CarType = string
type CarModel = string
type Car = {
	year: Year,
	type: CarType,
	model: CarModel
}

//使用别名定义变量类型
const carYear: Year = 2001
const carType: CarType = "Toyota"
const carModel: CarModel = "Corolla"
const car: Car = {
	year: carYear,
	type: carType,
	model: carModel
};

10、接口

interface Rectangle {
	height: number,
	width: number
}

const rectangle: Rectangle = {
	height: 20,
	width: 10
};
  • 接口合并
interface Animal {
	name: string;
}

interface Animal {
	age: number;
}

const dog: Animal = {
	name: "Fido",
	age: 5
};

console.log(dog.name);
console.log(dog.age);

输出:

Fido
5
  • 接口继承
interface Rectangle {
	height: number,
	width: number
}

interface ColoredRectangle extends Rectangle {
	color: string
}

const cr: ColoredRectangle = {
	height: 20,
	width: 10,
	color: "red"
};

console.log(cr.height);
console.log(cr.width);
console.log(cr.color);

输出:

20
10
red

11、类

TypeScript为JavaScript类添加了类型和可见性修饰符。

  • 成员类型

类的成员(属性和方法)通过类型注释进行类型化,类似于变量:

class Person {
	name: string;
}

const person = new Person();
person.name = "Jane";
  • 可见性
class Person {
	private name: string;

	public constructor(name: string) {
		this.name = name;
	}

	public getName(): string {
		return this.name;
	}
}

const person = new Person("Jane");
console.log(person.getName()); //Jane
  • 参数属性

TypeScript在参数中添加可见性修饰符:

class Person {
	//name为私有变量
	public constructor(private name: string) {}

	public getName(): string {
		return this.name;
	}
}

const person = new Person("Jane");
console.log(person.getName());//Jane
  • 只读

使用readonly可以防止类成员被修改:

class Person {
	private readonly name: string;

	public constructor(name: string) {
		//name属性赋值后不允许被修改
		this.name = name;
	}

	public getName(): string {
		return this.name;
	}
}
  • 实现接口
interface Shape {
	getArea(): number;
}

class Rectangle implements Shape {
	public constructor(protected readonly width: number, protected readonly height: number) {
	
	}

	public getArea() : number {
		return this.width * this.height;
	}
}
  • 继承类
class Square extends Rectangle {
	public constructor(width: number) {
		super(width, width);
	}
}
  • 覆盖

继承时可以使用相同名称的成员替换父类的成员:

class Square extends Rectangle {
	public constructor(width: number) {
		super(width, width);
	}

	public override getArea(): number {
		return this.width * this.width;
	}
}
  • 抽象类

可以使用abstract将类定义为抽象类,类中未实现的成员也使用abstract关键词修饰;抽象类无法直接实例化。

abstract class Polygon {
	public abstract getArea(): number;

	public toString(): string {
		return `Polygon[area=${this.getArea()}]`;
	}
}

class Rectangle extends Polygon {
	public constructor(protected readonly width: number, protected readonly height: number) {
		super();
	}

	public getArea(): number {
		return this.width * this.height;
	}
}

12、或类型

当一个值是多个类型时,可以使用联合(或)类型|

例如,下面的例子中参数是stringnumber类型:

function printStatusCode(code: string | number) {
	console.log(`My status code is ${code}.`)
}
printStatusCode(404);
printStatusCode('404');

输出:

My status code is 404.
My status code is 404.
  • 类型错误

使用|类型时,需要避免类型错误:

function printStatusCode(code: string | number) {
	console.log(`My status code is ${code.toUpperCase()}.`);
}

编译时报错:

error TS2339: Property 'toUpperCase' does not exist on type 'string | number'.
Property 'toUpperCase' does not exist on type 'number'.

13、函数

  • 返回值类型

可以明确定义函数的返回值类型:

function getTime(): number {
	return new Date().getTime();
}
  • 返回空类型
function printHello(): void {
	console.log('Hello!');
}
  • 参数

函数参数的类型与变量声明的语法类似:

function multiply(a: number, b: number) {
	return a * b;
}
  • 可选参数
function add(a: number, b: number, c?: number) {
	return a + b + (c || 0);
}
  • 默认参数
function pow(value: number, exponent: number = 10) {
	return value ** exponent;
}
  • 命名参数
function divide({ dividend, divisor }: { dividend: number, divisor: number }) {
	return dividend / divisor;
}
  • 剩余参数

剩余参数可以将一个不定数量的参数表示为一个数组,它必须是函数参数列表中的最后一个参数。

function add(a: number, b: number, ...rest: number[]) {
	return a + b + rest.reduce((p, c) => p + c, 0);
}

14、类型断言

有时处理类型时需要覆盖变量的类型,可以使用as关键字改变变量的类型:

let x: unknown = 'hello';
console.log((x as string).length);//5

但并不会改变变量值数据的类型:

let x: unknown = 4;
console.log((x as string).length);

由于值是数值类型,因此输出undefined

也可以用<>,和as效果一样:

let x: unknown = 'hello';
console.log((<string>x).length);

为了避免TypeScript在覆盖类型时可能抛出的类型错误,可以先将类型改为unknown,然后再改为目标类型:

let x = 5;
console.log(((x as unknown) as string).length);

输出:undefined

15、泛型

  • 函数

带有泛型的函数可以创建更通用的函数:

function createPair<S, T>(v1: S, v2: T): [S, T] {
	return [v1, v2];
}
console.log(createPair<string, number>('hello', 42)); 

输出:

[ 'hello', 42 ]

泛型可以被赋予默认值,如果没有指定或推断其他值,则使用默认值:

class NamedValue<T = string> {
	private _value: T | undefined;

	constructor(private name: string) {}

	public setValue(value: T) {
		this._value = value;
	}

	public getValue(): T | undefined {
		return this._value;
	}

	public toString(): string {
		return `${this.name}: ${this._value}`;
	}
}

let value = new NamedValue('myKey');
value.setValue('myValue');
console.log(value.toString());

输出:

myKey: myValue
  • 继承
<S extends Shape | Animal>

16、keyof

  • 提取键类型

keyof可以从对象类型中提取键类型:

interface Person {
	name: string;
	age: number;
}

function printPersonProperty(person: Person, property: keyof Person) {
	console.log(`Printing person property ${property}: "${person[property]}"`);
}

let person = {
	name: "Max",
	age: 27
};
printPersonProperty(person, "name");

输出:

Printing person property name: "Max"
  • 提取索引类型

keyof也可以与索引签名一起使用提取索引类型:

type StringMap = { [key: string]: unknown };

function createStringPair(property: keyof StringMap, value: string): StringMap {
	return { [property]: value };
}

console.log(createStringPair('myKey', 'myValue'));

输出:

{ myKey: 'myValue' }

17、类型/属性检查

  • typeof

typeof可以在运行时检查原始值的类型。

function formatValue(value: string | number): string {
	if (typeof value === 'string') {
		return value.trim().toUpperCase();
	} else {
		return value.toFixed(2);
	}
}

console.log(formatValue('  hello  ')); 
console.log(formatValue(56.1234));

输出:

HELLO
56.12
  • instanceof

instanceof可以检查对象是否是特定类或构造函数的实例。

class Bird {
	fly() {
		console.log("Flying...");
	}
}

class Fish {
	swim() {
		console.log("Swimming...");
	}
}

function move(animal: Bird | Fish) {
	if (animal instanceof Bird) {
		animal.fly();
	} else {
		animal.swim();
	}
}

move(new Bird());
move(new Fish());

输出:

Flying...
Swimming...
  • in

in可以检查对象上是否存在属性。

interface Dog {
	bark(): void;
}

interface Cat {
	meow(): void;
}

function makeSound(animal: Dog | Cat) {
	if ("bark" in animal) {
		animal.bark();
	} else {
		animal.meow();
	}
}

const myDog: Dog = {
    bark: () => {
        console.log("Woof! Woof!");
    }
};

const myCat: Cat = {
    meow: () => {
        console.log("Meow! Meow!");
    }
};

makeSound(myDog);
makeSound(myCat);

输出:

Woof! Woof!
Meow! Meow!

18、工具类型

  • Partial

Partial将对象中的所有属性改为可选。

interface Point {
	x: number;
	y: number;
}
//x和y可选
let pointPart: Partial<Point> = {}; 
pointPart.x = 10;
  • Required

Required可以将对象中的所有属性改为必须:

interface Car {
	make: string;
	model: string;
	mileage?: number;
}

let myCar: Required<Car> = {
	make: 'Ford',
	model: 'Focus',
	mileage: 12000 // `Required`
};
  • Record

可以通过Record定义具有特定键类型和值类型的对象:

const nameAgeMap: Record<string, number> = {
	'Alice': 21,
	'Bob': 25
};
  • Omit

Omit可以移除对象类型的键。

interface Person {
	name: string;
	age: number;
	location?: string;
}

const bob: Omit<Person, 'age' | 'location'> = {
	name: 'Bob'
};

上面bob只能定义name,其他两个属性已被移除。

  • Pick

Pick可以移除对象类型中除指定键外的其他键。

interface Person {
	name: string;
	age: number;
	location?: string;
}

const bob: Pick<Person, 'name'> = {
	name: 'Bob'
};

上面通过Pick只保留了name属性。

  • Exclude

Exclude从并集中移除类型。

type Primitive = string | number | boolean
const value: Exclude<Primitive, string> = true;
const num : Exclude<Primitive, string> = 123;

上面例子中Exclude<Primitive, string>表示在Primitive类型中移除string,因此只能定义为boolean或number类型。

  • ReturnType

ReturnType可以提取函数的返回类型。

type PointGenerator = () => { x: number; y: number; };
const point: ReturnType<PointGenerator> = {
	x: 10,
	y: 20
};
  • Parameters

提取函数的参数类型,值为数组形式。

type PointPrinter = (p: { x: number; y: number; }) => void;
const point: Parameters<PointPrinter>[0] = {
	x: 10,
	y: 20
};
  • Readonly

Readonly可以创建一个新类型,其中所有属性都是只读的,一旦赋值就无法修改。

interface Person {
	name: string;
	age: number;
}
const person: Readonly<Person> = {
	name: "Dylan",
	age: 35,
};
person.name = 'Israel'; 

会出现编译错误:

error TS2540: Cannot assign to 'name' because it is a read-only property.
参考资料:

TypeScript Getting Started