之前 TS 笔记由于放在长文章后面阅读体验不太好,故单独抽离出来一篇文章。
基本类型 TS
有以下几种数据类型:
基础类型 :包括boolean
(布尔值)、number
(数字)、string
(字符串)、null
(空值)、undefined
(未定义值)、bigint
(大整型)和symbol
(符号)等。这些类型和 JavaScript
的基本类型基本一致,只是 TS
在编译时会检查变量的类型是否匹配。例如:
let isDone : boolean = false ; // 声明一个布尔类型的变量
let age : number = 18 ; // 声明一个数字类型的变量
let name : string = "Alice" ; // 声明一个字符串类型的变量
let x : null = null ; // 声明一个空值类型的变量
let y : undefined = undefined ; // 声明一个未定义值类型的变量
let a : bigint = 2172141653 n ; // 定义一个大整型变量
let z : symbol = Symbol ( "key" ); // 声明一个符号类型的变量
数组类型 :用来表示一组相同类型的数据。TS 有两种方式可以定义数组类型,一种是在元素类型后面加上[]
,另一种是使用泛型Array<元素类型>
。例如:
let arr1 : number [] = [ 1 , 2 , 3 ]; // 声明一个数字类型的数组
let arr2 : Array < number > = [ 4 , 5 , 6 ]; // 声明一个数字类型的数组,使用泛型
元组类型 :用来表示一个已知元素数量和类型的数组,各元素的类型不必相同,但是对应位置的类型需要相同。例如:
let tuple : [ string , number ]; // 声明一个元组类型的变量
tuple = [ "Bob" , 20 ]; // 赋值正确,字符串和数字类型分别对应
tuple = [ 20 , "Bob" ]; // 赋值错误,类型不匹配
枚举类型 :用来定义一组有名字的常数,可以方便地访问和使用。TS 支持数字枚举和字符串枚举,还可以使用const
关键字定义常量枚举,以提高性能。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 enum Color {
Red ,
Green ,
Blue ,
} // 声明一个数字枚举类型
let c : Color = Color . Blue ; // 赋值正确,c的值为2
console . log ( c ); // 输出2
enum Direction {
Up = "UP" ,
Down = "DOWN" ,
Left = "LEFT" ,
Right = "RIGHT" ,
} // 声明一个字符串枚举类型
let d : Direction = Direction . Left ; // 赋值正确,d的值为'LEFT'
console . log ( d ); // 输出'LEFT'
const enum Month {
Jan ,
Feb ,
Mar ,
} // 声明一个常量枚举类型
let m : Month = Month . Feb ; // 赋值正确,m的值为1
console . log ( m ); // 输出1
any 类型 :用来表示任意类型的数据,可以赋值给任何类型的变量,也可以接受任何类型的赋值。这样可以避免类型检查的错误,但是也会失去类型检查的好处。例如:
let a : any = 1 ; // 声明一个任意类型的变量
a = "hello" ; // 赋值正确,可以赋值为字符串类型
a = true ; // 赋值正确,可以赋值为布尔类型
void 类型 :用来表示没有任何类型,一般用于标识函数的返回值类型,表示该函数没有返回值。例如:
function sayHello (): void {
// 声明一个返回值为void类型的函数
console . log ( "Hello" );
}
never 类型 :用来表示永远不会出现的值的类型,例如抛出异常或无限循环的函数的返回值类型。例如:
function error ( message : string ): never {
// 声明一个返回值为never类型的函数
throw new Error ( message ); // 抛出异常
}
function loop (): never {
// 声明一个返回值为never类型的函数
while ( true ) {} // 无限循环
}
unknown 类型 :unknown
类型和any
类型有些相似,但是更加安全,因为它不允许对未经类型检查的值进行任何操作,除非使用类型断言或类型收缩来缩小范围。例如:
let u : unknown = "Hello" ; // 声明一个unknown类型的变量
u = 10 ; // 赋值正确,可以赋值为任何类型
console . log ( u + 1 ); // 错误,不能对unknown类型进行运算
console . log (( u as number ) + 1 ); // 正确,使用类型断言缩小范围
if ( typeof u === "number" ) {
// 正确,使用类型收缩缩小范围
console . log ( u + 1 );
}
查看 never 类型与 void 类型的区别
TS 中的 never 类型和 void 类型是两种特殊的类型,它们的用法和含义有一些区别:
never 类型 :用来表示永远不会出现的值的类型,例如抛出异常或无限循环的函数的返回值类型。never
类型是任何类型的子类型,也就是说never
类型的值可以赋值给任何类型的变量,但是没有类型的值可以赋值给never
类型的变量(除了never
本身)。这意味着never
类型可以用来进行详尽的类型检查,避免出现不可能的情况。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function error ( message : string ): never {
// 声明一个返回值为never类型的函数
throw new Error ( message ); // 抛出异常
}
function loop (): never {
// 声明一个返回值为never类型的函数
while ( true ) {} // 无限循环
}
function check ( x : string | number ) {
switch ( typeof x ) {
case "string" :
// do something
break ;
case "number" :
// do something
break ;
default :
const never : never = x ; // 错误,x的类型不可能是never
// do something
}
}
void 类型 :用来表示没有任何类型,一般用于标识函数的返回值类型,表示该函数没有返回值。void
类型的变量只能赋值为undefined
和null
(在严格模式下,只能赋值为undefined
)。void
类型的作用是避免不小心使用了空指针导致的错误,和 C 语言中的void
是类似的。例如:
function sayHello (): void {
// 声明一个返回值为void类型的函数
console . log ( "Hello" );
}
let x : void = undefined ; // 声明一个void类型的变量
x = null ; // 赋值正确,如果不是严格模式
x = 1 ; // 错误,不能赋值为其他类型
console . log ( x ); // 输出undefined或null
查看 any 类型与 unknown 类型的区别
TS
中any
类型与unknown
类型是两种特殊的类型,它们都可以接受任何类型的值,但它们之间有一些重要的区别。下面我将从以下几个方面来讲解这两种类型的特点和用法,以及它们的异同:
定义和赋值 :any
类型是TS
中最宽泛的类型,它表示任意类型的值,可以赋值给任何类型的变量,也可以接受任何类型的值。unknown
类型是 TS 3.0 中引入的一种新的类型,它表示未知类型的值,也可以赋值给任何类型的变量,也可以接受任何类型的值。例如:
let a : any ; // 定义一个any类型的变量a
let b : unknown ; // 定义一个unknown类型的变量b
a = 1 ; // 可以给a赋值为数字
a = "hello" ; // 可以给a赋值为字符串
b = 2 ; // 可以给b赋值为数字
b = "world" ; // 可以给b赋值为字符串
操作和访问 :any
类型的变量可以进行任何操作和访问,不会有类型检查的错误,但这也会导致一些潜在的问题,比如访问不存在的属性或方法,或者调用不合法的参数。unknown
类型的变量则不能进行任何操作和访问,除非进行类型断言或类型收缩,否则会有类型检查的错误,这样可以保证类型的安全性。例如:
let a : any ;
let b : unknown ;
a . foo (); // 可以调用任意的方法,不会报错,但可能运行时出错
a + 1 ; // 可以进行任意的运算,不会报错,但可能得到意外的结果
b . foo (); // 不能调用任意的方法,会报错:Object is of type 'unknown'
b + 1 ; // 不能进行任意的运算,会报错:Object is of type 'unknown'
赋值给其他类型 :any
类型的变量可以赋值给任何类型的变量,不会有类型检查的错误,但这也会导致一些潜在的问题,比如赋值给不兼容的类型,或者覆盖了原有的类型信息。unknown
类型的变量则只能赋值给any
类型或unknown
类型的变量,否则会有类型检查的错误,这样可以保证类型的一致性。例如:
let a : any ;
let b : unknown ;
let c : number ;
let d : string ;
c = a ; // 可以把any类型赋值给number类型,不会报错,但可能赋值不合法的值
d = a ; // 可以把any类型赋值给string类型,不会报错,但可能赋值不合法的值
c = b ; // 不能把unknown类型赋值给number类型,会报错:Type 'unknown' is not assignable to type 'number'
d = b ; // 不能把unknown类型赋值给string类型,会报错:Type 'unknown' is not assignable to type 'string'
类型断言 :类型断言是一种告诉编译器我们比它更了解类型的方式,它可以让我们强制把一个类型转换为另一个类型,但这也有一定的风险,比如断言不合法的类型,或者忽略了一些类型检查。any
类型的变量可以使用类型断言转换为任何类型,不会有类型检查的错误,但这也会导致一些潜在的问题,比如断言错误的类型,或者丢失了类型信息。unknown
类型的变量则可以使用类型断言转换为任何类型,但这需要我们明确地指定要转换的类型,这样可以保证类型的正确性。例如:
let a : any ;
let b : unknown ;
let c = a as number ; // 可以把any类型断言为number类型,不会报错,但可能断言错误的类型
let d = a as string ; // 可以把any类型断言为string类型,不会报错,但可能断言错误的类型
let e = b as number ; // 可以把unknown类型断言为number类型,不会报错,但需要明确指定类型
let f = b as string ; // 可以把unknown类型断言为string类型,不会报错,但需要明确指定类型
类型收缩 :类型收缩是一种让编译器自动推断出更具体的类型的方式,它可以让我们根据一些条件判断来缩小类型的范围,从而进行一些操作和访问。any
类型的变量不能使用类型收缩,因为它已经是最宽泛的类型,没有更具体的类型可以推断出来。unknown
类型的变量则可以使用类型收缩,通过一些类型保护的方法,比如typeof
,instanceof
,in
等,来推断出更具体的类型,从而进行一些操作和访问。例如:
let a : any ;
let b : unknown ;
if ( typeof a === "number" ) {
a . toFixed ( 2 ); // 不能使用类型收缩,会报错:Object is of type 'any'
}
if ( typeof b === "number" ) {
b . toFixed ( 2 ); // 可以使用类型收缩,不会报错,推断出b是number类型
}
查看 ts 中的元组 tuple
TS 中的元组是一种特殊的数组,它可以存储不同类型的元素,并且元素的个数和类型在定义时就已经确定了。元组的语法格式如下:
let tuple_name : [ type1 , type2 , ..., typeN ] = [ value1 , value2 , ..., valueN ];
例如,我们可以定义一个元组,包含一个字符串和一个数字:
let mytuple : [ string , number ] = [ "Hello" , 42 ];
元组的元素可以通过索引来访问,索引从 0 开始,例如:
console . log ( mytuple [ 0 ]); // 输出 "Hello"
console . log ( mytuple [ 1 ]); // 输出 42
元组的长度和类型都是固定的,所以不能越界访问或修改元素,也不能添加或删除超出范围的元素。否则,TS 编译器会报错。但是,我们可以对元组的元素进行更新操作,例如:
mytuple [ 0 ] = "World" ; // 更新第一个元素
console . log ( mytuple [ 0 ]); // 输出 "World"
元组还有一些与其相关的方法,主要有以下几种:
push()
:向元组的末尾添加一个新元素,返回新的长度。注意,这个方法会改变原来的元组,而且添加的元素必须是元组中已有类型的联合类型。pop()
:从元组的末尾移除一个元素,返回被移除的元素。注意,这个方法会改变原来的元组。concat()
:连接两个元组,返回一个新的元组。注意,这个方法不会改变原来的元组,而且连接后的元组的类型必须是两个元组的类型的联合类型。slice()
:从元组中截取一部分元素,返回一个新的元组。注意,这个方法不会改变原来的元组,而且截取后的元组的类型必须是原来元组的类型的联合类型。下面是一些使用这些方法的例子:
let mytuple : [ string , number ] = [ "Hello" , 42 ];
let yourtuple : [ string , number ] = [ "true" , 100 ];
mytuple . push ( "World" ); // 添加一个字符串元素
console . log ( mytuple ); // 输出 ["Hello", 42, "World"]
let last = mytuple . pop (); // 移除最后一个元素
console . log ( last ); // 输出 "World"
console . log ( mytuple ); // 输出 ["Hello", 42]
let newtuple = mytuple . concat ( yourtuple ); // 连接两个元组
console . log ( newtuple ); // 输出 ["Hello", 42, 'true', 100]
let subtuple = newtuple . slice ( 1 , 3 ); // 截取一部分元组
console . log ( subtuple ); // 输出 [42, 'true']
枚举类型 枚举类型是一种在 TypeScript
中定义一组带名字的常量的方式。枚举可以清晰地表达意图或创建一组有区别的用例。TypeScript
支持基于数字和基于字符串的枚举。
enum Direction {
Up ,
Down = 10 ,
Left ,
Right ,
}
console . log ( Direction . Up , Direction . Down , Direction . Left , Direction . Right ); // 0 10 11 12
数字枚举:每个成员都有一个数字值,可以是常量或计算出来的。如果没有初始化器,第一个成员的值为 0,后面的成员依次递增。例如:
enum Direction {
Up , // 0
Down , // 1
Left , // 2
Right , // 3
}
字符串枚举:每个成员都必须用字符串字面量或另一个字符串枚举成员初始化。字符串枚举没有自增长的行为,但可以提供一个运行时有意义的并且可读的值。例如:
enum Direction {
Up = "UP" ,
Down = "DOWN" ,
Left = "LEFT" ,
Right = "RIGHT" ,
}
console . log ( Direction [ "Right" ], Direction . Up ); // RIGHT UP
// 后续也需要设置字符串
enum Direction {
Up = "UP" ,
Down , // error TS1061: Enum member must have initializer
Left , // error TS1061: Enum member must have initializer
Right , // error TS1061: Enum member must have initializer
}
异构枚举:可以混合字符串和数字成员,但不建议这么做。例如:
enum BooleanLikeHeterogeneousEnum {
No = 0 ,
Yes = "YES" ,
}
常量枚举:使用const enum
关键字定义,只能使用常量枚举表达式初始化成员,不能包含计算或动态的值。常量枚举在编译阶段会被删除,不会生成任何代码。例如:
const enum Direction {
Up ,
Down ,
Left ,
Right ,
}
let directions = [
Direction . Up ,
Direction . Down ,
Direction . Left ,
Direction . Right ,
]; // 编译后变为 [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */]
联合枚举和枚举成员类型:当所有枚举成员都是字面量类型时(不带有初始值或者初始化为字符串或数字字面量),枚举成员本身就是类型,而枚举类型本身就是每个成员的联合类型。这样可以实现更精确的类型检查和约束。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 enum ShapeKind {
Circle ,
Square ,
}
interface Circle {
kind : ShapeKind . Circle ; // 只能是 ShapeKind.Circle 类型
radius : number ;
}
interface Square {
kind : ShapeKind . Square ; // 只能是 ShapeKind.Square 类型
sideLength : number ;
}
let c : Circle = {
kind : ShapeKind . Square , // Error! 类型不匹配
radius : 100 ,
};
运行时的枚举:枚举是在运行时真正存在的对象,可以作为参数传递给函数或从函数返回。例如:
enum E {
X ,
Y ,
Z ,
}
function f ( obj : { X : number }) {
return obj . X ;
}
// Works, since 'E' has a property named 'X' which is a number.
f ( E );
反向映射:数字枚举成员具有反向映射,可以根据枚举值得到对应的名字。例如:
enum Enum {
A ,
}
let a = Enum . A ;
let nameOfA = Enum [ a ]; // "A"
查看枚举的本质
一个枚举的案例如下
enum Direction {
Up ,
Down ,
Left ,
Right ,
}
console . log ( Direction . Up === 0 ); // true
console . log ( Direction [ 0 ], typeof Direction [ 0 ]); // Up string
编译后的js
代码
var Direction ;
( function ( Direction ) {
Direction [( Direction [ "Up" ] = 0 )] = "Up" ;
Direction [( Direction [ "Down" ] = 1 )] = "Down" ;
Direction [( Direction [ "Left" ] = 2 )] = "Left" ;
Direction [( Direction [ "Right" ] = 3 )] = "Right" ;
})( Direction || ( Direction = {}));
( function ( Direction ) {
Direction [( Direction [ "Center" ] = 1 )] = "Center" ;
})( Direction || ( Direction = {}));
console . log ( Direction . Up === 0 ); // true
console . log ( Direction [ 0 ], typeof Direction [ 0 ]); // Up string
接口 ts
中的接口是一种用来描述对象的形状(shape
)的语法,它可以规定对象的属性和方法,以及它们的类型。接口可以让我们在编写代码时进行类型检查,避免出现类型错误。接口也可以提高代码的可读性和可维护性,让我们更清楚地知道对象的结构和功能。
ts 中的接口有以下几个特点:
可选属性 :接口中的属性可以用?
标记为可选,表示这个属性可以存在也可以不存在。例如:
interface Person {
name : string ; // 必须属性
age ? : number ; // 可选属性
}
let p1 : Person = { name : "Alice" }; // 合法,age可以省略
let p2 : Person = { name : "Bob" , age : 18 }; // 合法,age可以存在
let p3 : Person = { name : "Charlie" , gender : "male" }; // 非法,gender不是接口定义的属性
只读属性 :接口中的属性可以用readonly
标记为只读,表示这个属性只能在对象创建时赋值,不能再修改。例如:
interface Point {
readonly x : number ; // 只读属性
readonly y : number ; // 只读属性
}
let p1 : Point = { x : 10 , y : 20 }; // 合法,创建时赋值
p1 . x = 30 ; // 非法,不能修改只读属性
函数类型 :接口中可以定义函数的类型,即参数列表和返回值类型。例如:
interface SearchFunc {
( source : string , subString : string ): boolean ; // 函数类型
}
let mySearch : SearchFunc ; // 定义一个变量符合函数类型
mySearch = function ( src , sub ) {
// 实现一个函数符合函数类型
let result = src . search ( sub );
return result > - 1 ;
};
索引类型 :接口中可以定义索引的类型,即通过[]
访问对象的类型。索引可以是数字或字符串。例如:
interface StringArray {
[ index : number ]: string ; // 索引类型
}
let myArray : StringArray ; // 定义一个变量符合索引类型
myArray = [ "Bob" , "Fred" ]; // 赋值一个数组符合索引类型
let myStr : string = myArray [ 0 ]; // 访问数组元素符合索引类型
类类型 :接口中可以定义类的类型,即类的构造函数和实例方法。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 interface ClockInterface {
currentTime : Date ; // 实例属性
setTime ( d : Date ): void ; // 实例方法
}
class Clock implements ClockInterface {
// 类实现接口
currentTime : Date ;
constructor ( h : number , m : number ) {
this . currentTime = new Date ();
this . currentTime . setHours ( h );
this . currentTime . setMinutes ( m );
}
setTime ( d : Date ) {
this . currentTime = d ;
}
}
继承接口 :接口之间可以相互继承,从而拥有父接口的属性和方法。一个接口也可以继承多个接口,实现多重继承。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 interface Shape {
color : string ; // 父接口属性
}
interface PenStroke {
penWidth : number ; // 父接口属性
}
interface Square extends Shape , PenStroke {
// 子接口继承两个父接口
sideLength : number ; // 子接口属性
}
let square = {} as Square ; // 定义一个变量符合子接口类型
square . color = "blue" ; // 赋值父接口Shape的属性
square . sideLength = 10 ; // 赋值子接口Square的属性
square . penWidth = 5.0 ; // 赋值父接口PenStroke的属性
类(class) TS
中的类的基本使用与TS
相同,只不过引入了类型。
class Car {
// 字段
engine : string ;
// 构造函数
constructor ( engine : string ) {
this . engine = engine ;
}
// 方法
disp (): void {
console . log ( "发动机为 : " + this . engine );
}
}
但TS
相对于JS
中的class
也添加了一些更高阶的类的特性。
访问修饰符 TS
引入访问修饰符,类似JAVA
。值得注意的是,访问修饰符的特性是TS
本身的规范,JS
并不能实现类似访问修饰符的特性。
public
:公开,可以自由的访问类程序里定义的成员。
private
:私有,只能在类的内部进行访问。
protected
:受保护,除了在该类的内部可以访问,还可以在子类中仍然可以访问。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 class Animal {
public name : string ; // 公开的,可以在任何地方访问
private age : number ; // 私有的,只能在类的内部访问
protected color : string ; // 受保护的,可以在类的内部和子类中访问
readonly species : string ; // 只读的,只能在声明时或构造函数中赋值
constructor ( name : string , age : number , color : string , species : string ) {
this . name = name ;
this . age = age ;
this . color = color ;
this . species = species ;
}
// 访问器,用来获取或设置私有或受保护的成员
get Age () {
return this . age ;
}
set Age ( value : number ) {
if ( value > 0 ) {
this . age = value ;
}
}
get Color () {
return this . color ;
}
set Color ( value : string ) {
this . color = value ;
}
}
class Cat extends Animal {
constructor ( name : string , age : number , color : string ) {
super ( name , age , color , "cat" ); // 调用父类的构造函数
}
// 重写父类的方法
get Color () {
return "The color of this cat is " + super . Color ; // 访问父类的受保护成员
}
}
let animal = new Animal ( "Tom" , 3 , "black" , "dog" );
console . log ( animal . name ); // 可以访问公开成员
// console.log(animal.age); // 错误,不能访问私有成员
console . log ( animal . Age ); // 可以通过访问器访问私有成员
// console.log(animal.color); // 错误,不能访问受保护成员
console . log ( animal . Color ); // 可以通过访问器访问受保护成员
console . log ( animal . species ); // 可以访问只读成员
// animal.species = "bird"; // 错误,不能修改只读成员
let cat = new Cat ( "Jerry" , 2 , "white" );
console . log ( cat . name ); // 可以访问公开成员
// console.log(cat.age); // 错误,不能访问私有成员
console . log ( cat . Age ); // 可以通过访问器访问私有成员
// console.log(cat.color); // 错误,不能访问受保护成员
console . log ( cat . Color ); // 可以通过访问器访问受保护成员,注意这里调用的是子类重写的方法
console . log ( cat . species ); // 可以访问只读成员
// cat.species = "mouse"; // 错误,不能修改只读成员
TS
还引入了属性访问修饰符readonly
。注意:readonly
只能用于修饰属性,可以配合class
的classfield
写法例如:
class Person {
public readonly name : string ; // 公有的只读属性
private readonly age : number ; // 私有的只读属性
protected readonly gender : string ; // 受保护的只读属性
constructor ( name : string , age : number , gender : string ) {
this . name = name ;
this . age = age ;
this . gender = gender ;
}
}
抽象类 TS
中的抽象类是一种特殊的类,它不能被直接实例化,只能作为其他类的基类来提供通用的属性和方法的定义。抽象类主要用于定义一组相关的类的共同结构和行为,以及强制子类实现特定的方法。抽象类用 abstract
关键字修饰,抽象类中的抽象方法也用 abstract
关键字修饰,抽象方法没有具体的实现,只有声明。抽象类可以有构造器,也可以有非抽象的属性和方法。抽象类的子类必须实现抽象类中的所有抽象方法,否则也会成为抽象类。 以下是一个 TS
中抽象类的代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 // 定义一个抽象类Animal,它有一个name属性,一个构造器,一个sayHello方法和一个抽象的eat方法
abstract class Animal {
name : string ;
constructor ( name : string ) {
this . name = name ;
}
sayHello (): void {
console . log ( `Hello, I am ${ this . name } ` );
}
abstract eat (): void ; // 抽象方法,没有实现
}
// 定义一个子类Dog,它继承了Animal类,它必须实现Animal类中的抽象方法eat
class Dog extends Animal {
constructor ( name : string ) {
super ( name ); // 调用父类的构造器
}
eat (): void {
// 实现抽象方法
console . log ( ` ${ this . name } is eating bones` );
}
}
// 定义一个子类Cat,它继承了Animal类,它必须实现Animal类中的抽象方法eat
class Cat extends Animal {
constructor ( name : string ) {
super ( name ); // 调用父类的构造器
}
eat (): void {
// 实现抽象方法
console . log ( ` ${ this . name } is eating fish` );
}
}
// 创建一个Dog对象和一个Cat对象
let dog = new Dog ( "Tommy" );
let cat = new Cat ( "Kitty" );
// 调用它们的方法
dog . sayHello (); // Hello, I am Tommy
dog . eat (); // Tommy is eating bones
cat . sayHello (); // Hello, I am Kitty
cat . eat (); // Kitty is eating fish
// 不能创建一个Animal对象,因为Animal是抽象类
let animal = new Animal ( "Jack" ); // 编译错误,不能实例化抽象类
函数
ts
类型定义
// 方式一,函数类型的对象字面量,可用于函数重载
type LongHand = {
( a : number ): number ;
};
// 方式二,函数类型的别名,只能定义一个函数的类型,而不能定义多个函数的类型,不可用于函数重载
type ShortHand = ( a : number ) => number ;
重载签名的作用是为了让 TS 编译器能够正确地推断函数的参数类型和返回类型,从而提供更好的类型检查和代码提示。如果没有重载签名,函数依旧能够发挥作用,但是 TS 编译器可能无法识别函数的参数类型和返回类型,导致类型错误或警告。例如,如果你在 TS 中使用以下的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // 没有重载签名的函数
function add ( x : any , y : any ): any {
// 函数体
if ( typeof x === "string" && typeof y === "string" ) {
// 字符串相拼接
return x + y ;
} else if ( typeof x === "number" && typeof y === "number" ) {
// 数字相加
return x + y ;
} else {
// 抛出错误
throw new Error ( "Invalid arguments" );
}
}
// 调用函数
let a = add ( "Hello" , "World" ); // a的类型是any
let b = add ( 1 , 2 ); // b的类型是any
let c = add ( "1" , 2 ); // c的类型是any,但是会抛出错误
你会发现,变量 a、b 和 c 的类型都是 any,这意味着 TS 编译器无法确定它们的具体类型,也就无法提供类型检查和代码提示。例如,如果你想对 a 进行字符串操作,或者对 b 进行数学运算,TS 编译器可能会提示你这样做是不安全的,因为它们可能不是你期望的类型。而如果你使用重载签名,TS 编译器就能够根据你传入的参数类型,推断出函数的返回类型,从而提供更好的类型检查和代码提示。例如,如果你在 TS 中使用以下的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // 有重载签名的函数
function add ( x : string , y : string ): string ;
function add ( x : number , y : number ): number ;
function add ( x : any , y : any ): any {
// 函数体
if ( typeof x === "string" && typeof y === "string" ) {
// 字符串相拼接
return x + y ;
} else if ( typeof x === "number" && typeof y === "number" ) {
// 数字相加
return x + y ;
} else {
// 抛出错误
throw new Error ( "Invalid arguments" );
}
}
// 调用函数
let a = add ( "Hello" , "World" ); // a的类型是string
let b = add ( 1 , 2 ); // b的类型是number
let c = add ( "1" , 2 ); // c的类型是any,但是会抛出错误
你会发现,变量 a 的类型是 string,变量 b 的类型是 number,这意味着 TS 编译器能够确定它们的具体类型,也就能够提供类型检查和代码提示。例如,如果你想对 a 进行字符串操作,或者对 b 进行数学运算,TS 编译器就不会提示你这样做是不安全的,因为它们是你期望的类型。而变量 c 的类型仍然是 any,因为你传入了不匹配的参数类型,这时候 TS 编译器会提示你函数的重载签名没有匹配到你的参数组合,也就提醒你可能会出现错误。
可选参数与默认参数
这个函数的签名和function multiply(x: number, y?: number)
是一样的。
泛型 泛型是一种编程范式,它允许在程序中定义形式类型参数,然后在泛型实例化时使用实际类型参数来替换形式类型参数。泛型可以提高代码的复用性和类型安全性,让程序更灵活和通用。
TS
中的泛型有以下几种应用场景:
泛型函数 :可以定义一个函数,它的参数和返回值的类型由一个类型变量来表示,这样就可以适用于不同的类型,而不需要重复编写相同逻辑的函数。例如:
function identity < T >( arg : T ): T {
return arg ;
}
这个函数可以接受任何类型的参数,并返回相同类型的值。我们可以在调用时指定泛型参数的实际类型,如identity<string>("hello")
,或者让 TS
自动推断类型,如identity(42)
。
泛型接口 :可以定义一个接口,它的属性或方法的类型由一个或多个类型变量来表示,这样就可以定义通用的数据结构或契约。例如:
interface MyArray < T > extends Array < T > {
first : T | undefined ;
last : T | undefined ;
}
这个接口继承了数组接口,并添加了两个属性,它们的类型都是泛型参数 T
。我们可以实现这个接口,并指定 T
的实际类型,如class StringArray implements MyArray<string>
。
泛型类 :可以定义一个类,它的属性或方法的类型由一个或多个类型变量来表示,这样就可以创建通用的类。例如:
class GetMin < T > {
arr : T [] = [];
add ( ele : T ) {
this . arr . push ( ele );
}
min (): T {
let min = this . arr [ 0 ];
this . arr . forEach ( function ( value ) {
if ( value < min ) {
min = value ;
}
});
return min ;
}
}
这个类可以创建一个存储任何类型元素的数组,并提供一个方法返回最小值。我们可以创建这个类的实例,并指定 T
的实际类型,如let gm1 = new GetMin<number>()
。
泛型约束 :可以使用 extends
关键字来限制泛型参数的范围,使之只能是某个类型或其子类型。这样就可以在泛型中使用一些特定的属性或方法。例如:
interface Point {
x : number ;
y : number ;
}
function toArray < T extends Point >( a : T , b : T ): T [] {
return [ a , b ];
}
这个函数只能接受 Point
或其子类型作为参数,并返回相同类型的数组。我们不能传入其他类型的参数,如toArray(1, 2)
会报错。
高级类型 当然可以,我会给你一些 TS 中高级类型的例子,你可以参考一下:
交叉类型(Intersection Types)的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // 定义两个接口
interface A {
name : string ;
age : number ;
}
interface B {
gender : "male" | "female" ;
hobby : string ;
}
// 使用交叉类型将两个接口合并为一个类型
type C = A & B ;
// 创建一个C类型的对象
let c : C = {
name : "Tom" ,
age : 20 ,
gender : "male" ,
hobby : "basketball" ,
};
联合类型(Union Types)的例子:
// 定义一个联合类型,表示一个值可以是number或string
type D = number | string ;
// 使用联合类型作为函数参数的类型
function print ( d : D ) {
// 使用typeof类型保护来判断d的具体类型
if ( typeof d === "number" ) {
console . log ( "The number is " + d );
} else if ( typeof d === "string" ) {
console . log ( "The string is " + d );
}
}
// 调用函数,传入不同类型的值
print ( 10 ); // The number is 10
print ( "Hello" ); // The string is Hello
字面量类型(Literal Types)的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // 定义一个字符串字面量类型,表示一个值只能是'hello'或'world'
type E = "hello" | "world" ;
// 定义一个数字字面量类型,表示一个值只能是1或2
type F = 1 | 2 ;
// 定义一个布尔字面量类型,表示一个值只能是true
type G = true ;
// 定义一个模板字面量类型,表示一个值只能是'Hello ${string}'
type H = `Hello ${ string } ` ;
// 使用字面量类型创建变量
let e : E = "hello" ; // OK
let f : F = 2 ; // OK
let g : G = true ; // OK
let h : H = `Hello world` ; // OK
索引类型(Indexed Types)的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 // 定义一个接口
interface I {
name : string ;
age : number ;
gender : "male" | "female" ;
}
// 使用索引类型查询操作符得到I的所有属性名的类型
type J = keyof I ; // J = 'name' | 'age' | 'gender'
// 使用索引访问操作符得到I的某个属性的类型
type K = I [ "name" ]; // K = string
// 使用泛型约束和索引类型实现一个获取对象属性值的函数
function getProp < T , K extends keyof T >( obj : T , key : K ): T [ K ] {
return obj [ key ];
}
// 创建一个I类型的对象
let i : I = {
name : "Alice" ,
age : 18 ,
gender : "female" ,
};
// 调用函数,传入对象和属性名
let name = getProp ( i , "name" ); // name的类型是string,值是'Alice'
let age = getProp ( i , "age" ); // age的类型是number,值是18
映射类型(Mapped Types)的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 // 定义一个接口
interface L {
name : string ;
age : number ;
gender : "male" | "female" ;
}
// 使用映射类型将L的所有属性变为可选的
type M = {
[ P in keyof L ] ? : L [ P ];
};
// 使用映射类型将L的所有属性变为只读的
type N = {
readonly [ P in keyof L ]: L [ P ];
};
// 使用内置的映射类型Pick从L中选择部分属性组成一个新的类型
type O = Pick < L , "name" | "gender" >;
// 使用映射类型创建变量
let m : M = {
name : "Bob" ,
}; // OK,只有name属性,其他属性可选
let n : N = {
name : "Bob" ,
age : 20 ,
gender : "male" ,
}; // OK,所有属性只读
n . name = "Tom" ; // Error,不能修改只读属性
let o : O = {
name : "Bob" ,
gender : "male" ,
}; // OK,只有name和gender属性,其他属性不存在
条件类型(Conditional Types):
上面的意思就是,如果 T 是 U 的子集,就是类型 X,否则为类型 Y
类型别名(Type Aliases)
类型别名是一种给一个类型起一个新的名字的方式,它可以让你更方便地引用这个类型,或者给这个类型添加一些语义。类型别名使用 type
关键字来定义,例如 type Name = string
表示给 string
类型起了一个别名叫 Name
,之后你就可以用 Name
来代替 string
了。
其次,你需要知道什么是泛型(Generics)。泛型是一种在定义类型时使用一个或多个类型参数的方式,它可以让你创建一些适用于任意类型的通用类型,而不是限定于某个具体的类型。泛型使用 <T>
这样的语法来表示类型参数,其中 T
是一个占位符,可以用任何合法的标识符来替换。例如 Array<T>
表示一个泛型数组类型,它可以存放任意类型的元素,例如 Array<number>
表示一个数字数组, Array<string>
表示一个字符串数组。
那么,类型别名可以是泛型吗?答案是肯定的,类型别名可以使用泛型来定义一些通用的类型,这样就可以让类型别名更灵活和复用。例如,你给出的这个类型别名:
type Container < T > = { value : T };
它就是一个泛型类型别名,它表示一个容器类型,它有一个属性叫 value
,这个属性的类型是由类型参数 T
决定的。这样,你就可以用这个类型别名来创建不同类型的容器,例如:
// 创建一个容器,它的value属性是一个数字
let numContainer : Container < number > = { value : 10 };
// 创建一个容器,它的value属性是一个字符串
let strContainer : Container < string > = { value : "Hello" };
// 创建一个容器,它的value属性是一个布尔值
let boolContainer : Container < boolean > = { value : true };
这些容器都是使用同一个类型别名 Container<T>
来定义的,只是类型参数 T
不同而已。这样,你就可以用一个类型别名来表示多种可能性,而不需要为每种情况都定义一个新的类型。
查看 keyof 与 infer 关键字的介绍
infer
是 TypeScript 2.8
中引入的一个关键字,它可以用在条件类型的 extends
子句中,用来推断某个类型变量的具体类型。infer
的作用是在真实分支中引用 此推断类型变量,从而获取待推断的类型。infer
可以用在多种场合,例如:
推断函数的参数和返回值类型
// 定义一个类型,用来获取函数的参数类型
type ParamType < T > = T extends (... args : infer P ) => any ? P : never ;
// 定义一个类型,用来获取函数的返回值类型
type ReturnType < T > = T extends (... args : any ) => infer R ? R : never ;
// 测试
type Fn = ( a : number , b : string ) => number ;
type P = ParamType < Fn >; // [number, string]
type R = ReturnType < Fn >; // number
推断对象的属性类型
// 定义一个类型,用来获取对象的属性类型
type PropType < T , K extends keyof T > = T [ K ] extends infer U ? U : never ;
// 测试
type Obj = { a : string ; b : number };
type A = PropType < Obj , "a" >; // string
type B = PropType < Obj , "b" >; // number
推断联合类型的成员类型
// 定义一个类型,用来获取联合类型的成员类型
type UnionType < T > = T extends infer U ? U : never ;
// 测试
type U = UnionType < string | number >; // string | number
keyof
是 TypeScript
中的一个关键字,它可以从一个对象类型中提取出它的所有属性名,组成一个字符串或数字的字面量联合类型。例如:
type Person = {
name : string ;
age : number ;
gender : string ;
};
type P = keyof Person ; // "name" | "age" | "gender"
keyof
的作用是可以让我们在类型层面上操作对象的属性,比如限制访问对象的某些属性,或者映射对象的属性类型。keyof
也可以和泛型、索引类型、映射类型等一起使用,实现更多的类型操作。下面是一些 keyof
的常见用法:
使用[]
访问对象属性的类型:
type Person = {
name : string ;
age : number ;
gender : string ;
};
type Name = Person [ "name" ]; // string
type Age = Person [ "age" ]; // number
type Gender = Person [ "gender" ]; // string
使用extends
限制泛型参数的属性:
type Person = {
name : string ;
age : number ;
gender : string ;
};
function getProperty < T , K extends keyof T >( obj : T , key : K ) {
return obj [ key ];
}
let p : Person = { name : "Alice" , age : 20 , gender : "female" };
let name = getProperty ( p , "name" ); // string
let age = getProperty ( p , "age" ); // number
let gender = getProperty ( p , "gender" ); // string
// let hobby = getProperty(p, "hobby"); // Error: Type '"hobby"' is not assignable to type '"name" | "age" | "gender"'
使用in
遍历对象的属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 type Person = {
name : string ;
age : number ;
gender : string ;
};
type Keys = keyof Person ; // "name" | "age" | "gender"
type PersonRecord = {
[ P in Keys ]: string ;
};
// 等价于
// type PersonRecord = {
// name: string;
// age: string;
// gender: string;
// };