:
类型补全ts-node
npm i ts-node typescript -g
tsconfig.json
npx --package typescript tsc --init
# 如果已经全局安装 TypeScript
tsc --init
index.ts
console.log('Hello TypeScript')
ts-node index.ts
-P, --project
指定 tsconfig 文件位置,默认 ./tsconfig.json
-T, --transpileOnly
只编译,不检查类型--swc
在 transpileOnly
基础上使用 swc
进行文件编译,进一步提升执行速度--emit
执行并且生成 JS 文件,输出到 .ts-node
文件夹下(需要与 --compilerHost
一起使用)const name: string = 'Cell';
const age: number = 18;
const isMale: boolean = false;
const undef: undefined = undefined;
const nul: null = null;
const obj: object = {};
const bigint: bigint = BigInt(1);
const symbol: symbol = Symbol();
null
有值,但是个空值undefined
没有值在 TypeScript 中,null
和 undefined
类型是有具体意义的类型。在没有开启 strictNullChecks
检查时,会被视为其他类型的子类型,如 string
类型会被认为包含了 null
和 undefined
。
const tmp1: null = null;
const tmp2: undefined = undefined;
const tmp3: string = null; // 在关闭 strictNullChecks 检查时,不会报错
const tmp4: string = undefined;
JavaScript 中 void
操作符会执行后面跟着的表达式并返回一个 undefined
。
void function iife() {
console.log('Invoke Immediately')
}(); // undefined
// 相当于
void((function iife(){
console.log('Invoke Immediately')
})());
TypeScript 中的原始类型标注 void
,用于描述一个内部没有 return
语句 或 没有显示 return
一个值的函数的返回值类型。
function fn1 () {}
function fn2 () {
return;
}
function fn3 () {
return undefined;
}
上面 fn1()
和 fn2()
的返回值类型都会被隐式推导为 void
,只有显式返回了 undefined
值的 fn3()
其返回值类型才被推导为 undefined
。但是,在实际执行过程中,fn1()
和 fn2()
的返回值都是 undefined
。
虽然 fn3()
返回值类型会被推导为 undefined
,但仍然可以使用 void
类型进行标注,因为在类型层面 fn1()
、fn2()
、fn3()
都表示“没有返回一个有意义的值”。
const arr1: number[] = [1, 2, 3];
const arr2: Array<number> = [1, 2, 3];
const arr3: string[string, string, string] = ['a', 'b', 'c'];
const arr4: [string, number] = ['a', 1];
// 可选成员
const arr5: [string, number?] = ['a'];
// 可选成员的长度属性类型
type TupleLength = typeof arr5.length; // 1 | 2
具名元组
const arr6: [name: string, age: number] = ['Cell', 18];
const arr7: [name: string, age: number, male?: boolean] = ['Cell', 18, true];
相对于数组,使用元组能帮助进一步提升数据结构的严谨性,包括基于位置的类型标注,避免出现越界访问等。
TypeScript 中需要特殊的类型标注来描述对象类型——interface
,其代表了对象对外提供的接口结构。
interface IDescription {
name: string;
age: number;
male: boolean;
}
const p1: IDescription = {
name: 'Cell',
age: 18,
male: true
};
对对象的描述:
obj.prop
属性访问赋值的形式interface IDescription {
name: string;
age: number;
male?: boolean;
func?: Function;
}
const p2: IDescription = {
name: 'Cell',
age: 18,
male: true,
// func 也可以不实现
}
// 可选函数类型属性只限制不能调用,不会影响赋值
p2.func = () => {};
只读属性
interface IDescription {
readonly id: number;
name: string;
age: number;
}
const p3: IDescription = {
id: 1,
name: 'Cell',
age: 18
}
p3.id = 2; // Error: Cannot assign to 'id' because it is a read-only property.
push
、pop
等方法 ReadonlyArray
而不是 Array
虽然 type
也可以代替 interface
描述对象,但更推荐用 interface
来描述对象、类的结构,而类型别名用来将一个函数签名、一组联合类型、一个工具类型等抽离成一个完整独立的类型。
Object
Object
以及 Function
,所有的原始类型与对象类型最终都指向 Object
,在 TypeScript 中表现为 Object
包含了所有类型const tmp1: Object = undefined;
const tmp2: Object = null;
const tmp3: Object = void 0;
const tmp4: Object = 'Cell';
const tmp5: Object = 2022;
const tmp6: Object = true;
const tmp7: Object = { name: 'Cell' };
const tmp8: Object = [1, 2, 3];
const tmp9: Object = () => {};
Object
类似的还有 Boolean
、Number
、String
、Symbol
,这些装箱类型同样包含了一些超出预期的类型// String 同样包括 undefined、null、void 及其拆箱类型 string
const tmp1: String = undefined;
const tmp2: String = null;
const tmp3: String = void 0;
const tmp4: String = 'Cell';
任何情况下,都不应该使用这些装箱类型
object
object
的引入就是为了解决 Object
类型的错误使用,它代表所有非原始类型的类型,即数组、对象和函数类型const tmp1: object = undefined; // Error: Type 'undefined' is not assignable to type 'object'.
const tmp2: object = null; // Error: Type 'null' is not assignable to type 'object'.
const tmp3: object = void 0; // Error: Type 'void' is not assignable to type 'object'.
const tmp4: object = 'Cell'; // Error: Type 'string' is not assignable to type 'object'.
const tmp5: object = 2022; // Error: Type 'number' is not assignable to type 'object'.
const tmp6: object = true; // Error: Type 'boolean' is not assignable to type 'object'.
const tmp7: object = { name: 'Cell' };
const tmp8: object = [1, 2, 3];
const tmp9: object = () => {};
{}
{}
对象字面量类型(对应字符串字面量类型那种){}
作为类型签名,一个内部无属性定义的空对象,类似于 Object
,接受任何非 null
和 undefined
的值const tmp1: {} = undefined; // Error: Type 'undefined' is not assignable to type '{}'.
const tmp2: {} = null; // Error: Type 'null' is not assignable to type '{}'.
const tmp3: {} = void 0; // Error: Type 'void' is not assignable to type '{}'.
const tmp4: {} = 'Cell';
const tmp5: {} = 2022;
const tmp6: {} = true;
const tmp7: {} = { name: 'Cell' };
const tmp8: {} = [1, 2, 3];
const tmp9: {} = () => {};
const tmp: {} = { name: 'Cell' };
tmp.name = 'Cellinlab'; // Error: Property 'name' does not exist on type '{}'.
tmp.age = 18; // Error: Property 'age' does not exist on type '{}'.
Object
及类似的装箱类型object
Record<string, unknown>
或 Record<string, any>
代表对象unknown[]
或 any[]
代表数组(...args: any[]) => any
代表函数{}
{}
意味任何非 null / undefined
的值,使用它和使用 any
一样恶劣Symbol
在 JavaScript 中代表一个唯一的值类型,类似于字符串类型,可以作为对象的属性名,并用于避免错误修改 对象或 class
内部属性的情况。
在 TypeScript 中,symbol
类型并不具有这一特性,多个具有 symbol
类型的对象,它们的 symbol
类型指的都是 TypeScript 中的同一个类型。
为了实现“独一无二”特性,TypeScript 中支持了 unique symbol
类型声明,它是 symbol
类型的子类型,每一个 unique symbol
类型都是独一无二的。
const sym1: unique symbol = Symbol();
const sym2: unique symbol = sym1; // Error: Type 'typeof sym1' is not assignable to type 'typeof sym2'.
在 JavaScript 中,可以用 Symbol.for
方法来复用已创建的 Symbol
,如 Symbol.for('Cell')
会首先查找全局是否已经有使用 Cell
作为 key
的 Symbol
注册,如果有则返回该 Symbol
,否则创建一个新的 Symbol
并注册到全局。
在 TypeScript 中,要引用已创建的 unique symbol
类型,需要使用类型查询操作符 typeof
,如 typeof sym1
。
declare const uniqueSymbolFoo: unique symbol;
const uniqueSymbolBaz: typeof uniqueSymbolFoo = uniqueSymbolFoo;
interface Res {
code: 10000 | 10001 | 50000;
status: 'success' | 'error';
data: any;
}
字面量类型,代表比原始类型更精确的类型,同时原始类型的子类型。
字面量类型主要包括字符串字面量类型、数字字面量类型、布尔字面量类型 和 对象字面量类型。
const tmp1: 'Cell' = 'Cell';
const tmp2: 2022 = 2022;
const tmp3: true = true;
const tmp4: { name: 'Cell' } = { name: 'Cell' };
原始类型的值可以包括任意的同类型值,而字面量类型要求是值级别的字面量一致。
const tmp1: 'Cell' = 'Cellinlab'; // Error: Type '"Cellinlab"' is not assignable to type '"Cell"'.
const tmp2: 2022 = 2021; // Error: Type '2021' is not assignable to type '2022'.
const tmp3: true = false; // Error: Type 'false' is not assignable to type 'true'.
const tmp4: string = 'Cell';
const tmp5: number = 2022;
const tmp6: boolean = true;
单独使用字面量类型比较少见,因为单个字面量类型并没有什么实际意义。通常和联合类型(|
)一起使用,表达一组字面量类型:
interface Tmp {
bool: true | false;
num: 1 | 2 | 3;
str: 'Cell' | 'Cellinlab';
}
联合类型,代表一组类型的可用集合,只要最终赋值的类型属于联合类型的一员,就可以通过类型检查。
联合类型对其成员并没有任何限制,除了对同一类型字面量的联合,还可以将各种类型混合到一起:
interface Tmp {
mixed: 'Cell' | 2022 | true | {} | [1, 2, 3] | (() => {}) | (1 | 2 | 3);
}
联合类型使用是需要注意:
()
包裹起来(() => {})
是一个合法的函数类型联合类型常用场景之一是通过多个对象类型的联合,来实现手动的互斥属性,即这一属性如果有 字段1 那就没有字段2:
interface Tmp {
user:
| {
vip: true;
expire: number;
}
| {
vip: false;
promotion: string;
};
}
declare var tmp: Tmp;
if (tmp.user.vip) {
console.log(tmp.user.expire);
} else {
console.log(tmp.user.promotion);
}
也可以通过类型别名来复用一组字面量联合类型
type Code = 10000 | 10001 | 50000;
type Status = 'success' | 'error';
对象字面量类型就是一个对象类型的值,即这个对象的值全都为字面量值:
interface Tmp {
obj: {
name: 'Cell';
age: 18;
};
}
const tmp: Tmp = {
obj: {
name: 'Cell',
age: 18,
},
};
无论是原始类型还是对象类型的字面量类型,其本质都是类型而不是值。在编译时同样会被移除,同时也是被存储在内存中的类型空间而非值空间。
如果说字面量类型是对原始类型的进一步扩展,那么某些方面枚举类型就是对对象类型的进一步扩展。
enum PageUrl {
Home_Page_Url = '/',
Setting_Page_Url = '/setting',
About_Page_Url = '/about',
}
const home = PageUrl.Home_Page_Url;
枚举在提供更好的类型提示之外,还将这些常量真正地约束在一个命名空间下。
如果没有声明枚举的值,它会默认使用数字枚举,且默认值从 0 开始:
enum Items {
A,
B,
C,
}
console.log(Items.A); // 0
console.log(Items.B); // 1
console.log(Items.C); // 2
如果只为某个成员指定了枚举值,其之前的成员仍从 0 开始,之后的成员会从指定值递增:
enum Items {
A,
B = 10,
C,
}
console.log(Items.A); // 0
console.log(Items.B); // 10
console.log(Items.C); // 11
在数字型枚举中,可以使用延迟求值的枚举值,如函数:
const returnNum = () => 2021 + 1;
enum Items {
A,
B = returnNum(),
}
console.log(Items.A); // 0
console.log(Items.B); // 2022
如果使用了延迟求值,那么没有使用延迟求值的枚举成员必须放在使用常量枚举值声明的成员之后,或者放在第一位。
TypeScript 中可以同时使用字符串枚举值和数字枚举值:
enum Mixed {
Num = 1,
Str = 'str',
}
枚举和对象的重要差异在于,对象是单向映射的,只能从键映射到键值,而枚举是双向映射的,可以从枚举成员映射到枚举值,也可以从枚举值映射到枚举成员。
enum Items {
A,
B,
C,
}
console.log(Items.A); // 0
console.log(Items[0]); // A
// 原理
// "use strict";
// var Items;
// (function (Items) {
// Items[Items["A"] = 0] = "A";
// Items[Items["B"] = 1] = "B";
// Items[Items["C"] = 2] = "C";
// })(Items || (Items = {}));
注意,仅有值为数字的枚举成员才能进行双向枚举,字符串成员仍然只会进行单次映射。
enum Items {
A,
B = 'b',
C = 'c',
}
// "use strict";
// var Items;
// (function (Items) {
// Items[Items["A"] = 0] = "A";
// Items["B"] = "b";
// Items["C"] = "c";
// })(Items || (Items = {}));
常量枚举和枚举类似,只是声明多了一个 const
:
const enum Items {
A,
B,
C,
}
console.log(Items.A); // 0
// console.log(Items[0]); // Error A const enum member can only be accessed using a string literal.
// "use strict";
// console.log(0 /* Items.A */); // 0
常量枚举只能通过枚举成员访问枚举值,同时,其编译产物中并不会存在一个额外的辅助对象,对枚举成员的访问会被直接内联替换为枚举的值。
函数的类型描述函数入参类型和函数返回值类型。
// 函数声明
function foo(name: string): number {
return name.length;
}
// 函数表达式
const foo2 = function (name: string): number {
return name.length;
};
// 类型标注
// (name: string) => number 表示函数的类型签名
const foo3: (name: string) => number = function (name: string): number {
return name.length;
};
// 箭头函数的类型标注
const foo4: (name: string) => number = (name: string): number => {
return name.length;
};
// 箭头函数的类型推断
const foo5 = (name: string): number => {
return name.length;
};
为了提高可读性,一般要么直接在函数中进行参数和返回值的类型声明,要么使用类型别名将函数声明抽离出来。
type FuncFoo = (name: string) => number;
const foo: FuncFoo = (name) => {
return name.length;
};
如果只是为了描述函数的类型结构,也可以使用 interface
进行函数声明:
interface FuncFooStruct {
(name: string): number;
}
用来描述函数的 interface
被称为 Callable Interface,interface
用来描述一个类型结构,而函数类型本质上也是一个结构固定的类型。
在 TypeScript 中,一个没有返回值(即没有调用 return
语句)的函数,其返回值类型应该被标记为 void
而不是 undefined
,尽管它的实际值就是 undefined
。
function foo(): void {
console.log('foo');
}
function bar(): void {
return;
}
在 TypeScript 中,undefined
类型是一个实际的、有意义的类型值,而 void
才代表空的、没有意义的类型值。
void
更强调没有返回,如果返回但是没有返回实际的值,推荐用 undefined
:
function bar(): undefined {
return;
}
function foo1(name: string, age?: number): number {
const inputAge = age || 18;
return name.length + inputAge;
}
// 直接为可选参数指定默认值
// 既然有默认值,当然可以不传,所以不用强调可选
function foo2(name: string, age: number = 18): number {
return name.length + age;
}
可选参数必须位于必选参数之后。
rest
参数实际上是一个数组,使用数组类型标注即可:
function foo(arg1: string, ...rest: any[]) {}
rest
参数也可以用元组类型进行标注:
function foo(arg1: string, ...rest: [number, boolean]) {}
foo('a', 1, true);
要实现与入参关联的返回值类型,可以使用 TypeScript 提供的函数重载签名:
function func(foo: number, bar: true): string;
function func(foo: number, bar?: false): number;
function func(foo: number, bar?: boolean): string | number {
if (bar) {
return String(foo);
} else {
return foo * 10;
}
}
console.log(func(2022)); // 20220
console.log(func(2022, false)); // 20220
console.log(func(2022, true)); // '2022'
function func
的不同意义:
function func(foo: number, bar: true): string;
,重载签名一,传入 bar
的值为 true
时,返回值类型为 string
;function func(foo: number, bar?: false): number;
,重载签名二,bar
不传值或传入 bar
的值为 false
时,返回值类型为 number
;function func(foo: number, bar?: boolean): string | number;
,函数的实现签名,包含重载签名的所有可能情况基于重载签名,实现了将入参类型和返回值类型的可能情况进行关联,获得了更精确的类型标注能力。
拥有多个重载声明的函数在被调用时,是按照重载的声明顺序往下查找的。
TypeScript 中的重载更像伪重载,只有一个具体的实现,其重载体现在方法调用的签名上而不是具体实现细节上。在像 C++ 等语言中,重载体现在多个名称一样,但是入参不同的函数实现上。
async function asyncFunc(): Promise<void> {}
function* genFunc(): Iterable<void>{}
async function* asyncGenFunc(): AsyncIterable<void>{}
// "use strict";
// async function asyncFunc() { }
// function* genFunc() { }
// async function* asyncGenFunc() { }
类的主要结构有构造函数、属性、方法和访问符。属性的类型标注类似于变量,构造函数、方法、存取器的类型标注类似于函数。
class Foo {
prop: string;
constructor(inputProp: string) {
this.prop = inputProp;
}
print(addon: string): void {
console.log(`${this.prop} ${addon}`);
}
get propA(): string {
return `${this.prop}A`;
}
set propA(value: string) {
this.prop = `${value}A`;
}
}
注意,setter
方法不允许进行返回值的类型标注,因为其返回值并不会被消费,它更多地关注过程。
类也可以通过类声明和类表达式的方法创建:
const Foo = class {
prop: string;
constructor(inputProp: string) {
this.prop = inputProp;
}
print(addon: string): void {
console.log(`${this.prop} ${addon}`);
}
}
在 TypeScript 中可以为 Class
成员添加修饰符,修饰符有:public
、private
、protected
、readonly
。readonly
属于操作性修饰符,其他的都是访问性修饰符。
class Foo {
private prop: string;
constructor(inputProp: string) {
this.prop = inputProp;
}
protected print(addon: string): void {
console.log(`${this.prop} ${addon}`);
}
public get propA(): string {
return `${this.prop}A`;
}
public set propA(value: string) {
this.prop = `${value}A`;
}
}
通常不会为构造函数添加修饰符,而是让其保持默认的 public
。
各修饰符的含义:
public
此类成员在类、类的实例、子类中都可以访问;private
此类成员只能在类的内部访问;protected
此类成员只能在类的内部和子类中访问;不显式使用访问性修饰符,默认会被标记为 public
。
为了简单,可以在构造函数中对参数应用访问性修饰符。参数会被直接作为类的成员(即实例的属性),不需要再手动添加属性和赋值。
class Foo {
constructor(public arg1: string, private arg2: boolean) {}
}
const foo = new Foo('a', true);
console.log(foo.arg1); // 'a'
class Foo {
static staticHandler() {}
public innerHandler() {}
}
在类的内部静态成员无法通过 this
来访问,需要通过 Foo.staticHandler()
的方式来访问。
"use strict";
var Foo = /** @class */ (function () {
function Foo() {
}
Foo.staticHandler = function () { };
Foo.prototype.innerHandler = function () { };
return Foo;
}());
静态成员直接被挂载在函数体上,而实例成员被挂载在原型上。静态成员不会被实例继承,始终属于当前定义的这个类(及其子类)。原型对象上的实例成员会沿着原型链进行传递,能被继承。
// 基类
class Base {}
// 派生类
class Derived extends Base {}
基类中哪些成员可以被派生类访问,由其访问性修饰符决定。派生类可以访问使用 public
或 protected
修饰符的基类成员。除了访问外,派生类可以覆盖基类中的方法,但仍然可以通过 super
来调用基类的方法。
class Base {
print() {}
}
class Derived extends Base {
print() {
super.print();
}
}
为了检查被覆盖的基类方法在基类中确实存在,可以使用 override
关键字:
class Base {
print() {}
}
class Derived extends Base {
override print() {
super.print();
}
}
抽象类 是对类结构与方法的抽象,抽象类描述一个类中有哪些成员(属性,方法等),抽象方法描述这一个方法在实际实现中的结构。
abstract class AbsFoo {
abstract absProp: string;
abstract get absGetter(): string;
abstract absMethod(name: string): string;
}
抽象类中的成员需要使用 abstract
关键字才能被视为抽象类成员。
class Foo implements AbsFoo {
absProp: string;
get absGetter() {
return this.absProp;
}
absMethod(name: string) {
return `${name} ${this.absProp}`;
}
}
必须完全实现抽象类中的每一个成员。在 TypeScript 中无法声明静态的抽象成员。
对于抽象类,其本质是描述类的结构,因此也可以用 interface
来声明类的结构。
interface FooStruct {
absProp: string;
get absGetter(): string;
absMethod(name: string): string;
}
class Foo implements FooStruct {
absProp: string;
get absGetter() {
return this.absProp;
}
absMethod(name: string) {
return `${name} ${this.absProp}`;
}
}
类的构造函数被标记为私有,只允许在类内部访问,无法实例化。此时,可以使用私有构造函数来组织其被错误的实例化,如在创建 Utils
类时,其内部都是静态成员。
class Utils {
public static identifier = 'Cell';
private constructor() {}
public static getIdentifier() {
return Utils.identifier;
}
}
或者在一个类希望把实例化逻辑通过方法来实现,而不是通过 new
实现,可以使用私有构造函数。
class Foo {
private constructor() {}
public static create() {
return new Foo();
}
}
const foo = Foo.create();
为了能够表示“任意类型”,TypeScript 提供了一个内置类型 any
,用来表示任意类型。
log(message?: any, ...optionalParams?: any[]): void;
除了显式标记一个变量或参数为 any
,在某些情况下一些变量或参数会被隐式推导为 any
类型,如:
let foo;
function func(foo, bar){} // foo, bar 都会被推导为 any 类型
any
类型的变量几乎无所不能,它可以在声明后再次接受任意类型的值,同时可以被赋值给任意其他类型的变量:
let anyVal: any = 'Cell';
anyVal = 123;
anyVal = true;
anyVal = null;
anyVal = () => {};
let strVal: string = anyVal;
let numVal: number = anyVal;
可以在 any
类型变量上任意地进行操作,包括赋值、访问、方法调用等,此时可以认为类型推导与检查时完全被禁用的:
let anyVal: any = null;
anyVal.foo.bar();
anyVal();
anyVal = 'Cell';
any
类型的主要意义,是为了表示一个无拘无束的“任意类型”,它能兼容所有类型,也能被所有类型兼容。
any
的本质是类型系统中的顶级类型。
any
类型的万能性会导致其被经常滥用,需要注意:
any
,考虑使用类型断言代替any
, 考虑去将这里的类型去断言为需要的最简类型unknown
类型unknown
类型的变量可以再次赋值为任意其他类型,但注意只能赋值给 any
或 unknown
类型的变量:
let unknownVal: unknown = 'Cell';
unknownVal = 123;
unknownVal = true;
unknownVal = null;
unknownVal = () => {};
let anyVal: any = unknownVal;
let unknownVal2: unknown = unknownVal;
let strVal: string = unknownVal; // Error
unknown
和 any
的主要差异体现在赋值给别的变量时,any
把所有类型都兼容,而 unknown
在期待一个确定的值。
any
放弃了所有的类型检查,而 unknown
没有:
let unknownVal: unknown;
unknownVal.foo(); // Error
要对 unknown
类型进行属性访问,需要进行类型断言,即许诺它会是一个确定的类型:
let unknownVal: unknown;
(unknownVal as { foo: () => {}}).foo();
never
是一个“什么都没有”的类型,不携带任何的类型信息。
never
类型被称为 Bottom Type,是整个类型系统层级中最底层的类型。
和 null
、undefined
一样,是所有类型的子类型,但只有 never
类型的变量可以赋值给另一个 never
类型的变量。
通常不会显式声明一个 never
类型,它主要被类型检查所使用。
类型断言可以显式告知类型检查程序当前变量的类型。是一个将变量的已有类型更改为新指定的类型的操作。
let unknownVar: unknown;
(unknownVar as { foo: () => {} }).foo();
类型断言正确使用方式是,在 TypeScript 类型分析不正确或不符合预期时,将其断言为此处的正确类型:
interface IFoo {
name: string;
}
declare const obj: {
foo: IFoo;
};
const {
foo = {} as IFoo,
} = obj;
在原类型与断言类型之间差异过大时,需要先断言到一个通用的类型,any
或 unknown
,再进行第二次断言:
const str: string = 'Cell';
(str as unknown as { handler: () => {} }).handler();
// 尖括号语法
(<{ handler: () => {} }>(<unknown>str)).handler();
非空断言是类型断言的简化,标记前面的一个声明一定是非空的,即剔除 null
和 undefined
类型:
declare const foo: {
func?: () => ({
prop?: number | null
})
}
foo.func!().prop!;