一、基本介绍
TypeScript 可以看成是 JavaScript 的超集(superset),即它继承了后者的全部语法,所有 JavaScript 脚本都可以当作 TypeScript 脚本(但是可能会报错),此外它再增加了一些自己的语法。
类型是人为添加的一种编程约束和用法提示。 主要就是为了提高代码质量,减少错误
动态类型和静态类型 语法上js属于动态类型,变量的类型是动态的,不具有很强的约束性,这对于提前发现代码错误,非常不利。ts属于静态类型语言,即变量的类型是静态的。
// 变量的类型是动态的。
let x = 1;
x = 'hello';
静态类型的优缺点:
优点:1.有利于代码静态分析 2.有利于发现错误 3.更好的 IDE 支持,做到语法提示和自动补全。4.有利于代码重构
缺点:1.丧失了动态类型的代码灵活性 2.增加了编程工作量 3.兼容性问题
二、基本用法
类型声明(冒号+类型)
类型推断(类型声明并不是必需的,如果没有,TypeScript 会自己推断类型)
JavaScript 的运行环境(浏览器和 Node.js)不认识 TypeScript 代码。所以,TypeScript 项目要想运行,必须先转为 JavaScript 代码。TypeScript 的类型检查只是编译时的类型检查,而不是运行时的类型检查。一旦代码编译为 JavaScript,运行时就不再检查类型了
三、类型介绍
1、any
any 类型表示没有任何限制,该类型的变量可以赋予任意类型的值。
变量类型一旦设为any
,TypeScript 实际上会关闭这个变量的类型检查。
let x:any = 'hello';
x(1)
x.foo = 100; // 不报错
变量x
的值是一个字符串,但是把它当作函数调用,或者当作对象读取任意属性,TypeScript 编译时都不报错。原因就是x
的类型是any
,TypeScript 不对其进行类型检查。
开发者没有指定类型、ts必须自己推断的类型,如果无法推断出类型,ts就会认为该变量的类型为any
污染问题:
any
类型除了关闭类型检查,还有一个很大的问题,就是它会“污染”其他变量。它可以赋值给其他任何类型的变量(因为没有类型检查),导致其他变量出错。
let x:any = 'hello';
let y:number;
y = x;
y * 123 // 不报错
y.toFixed() // 不报错
x值是字符串,类型为any,但是y被赋值为x并不会报错,y继续运算。TypeScript 也检查不出错误,问题就这样留到运行时才会暴露。污染其他具有正确类型的变量。
2、unknow
为了解决any类型“污染”其他变量的问题,TypeScript 3.0 引入了unknow类型。它与any
含义相同,表示类型不确定,可能是任意类型,但是它的使用有一些限制,不像any
那样自由,可以视为严格版的any
。
unknown
跟any
的相似之处,在于所有类型的值都可以分配给unknown
类型。
unknown
类型跟any
类型的不同之处在于,它不能直接使用。主要有以下几个限制。
1.不能直接赋值给其他类型的变量(除了any
类型和unknown
类型)
let v:unknown = 123;
let v1:boolean = v;
let v2:number = v; // 报错
2.不能直接调用unknown类型变量的方法和属性。let v1:unknown = { foo: 123 };
v1.foo // 报错
let v2:unknown = 'hello';
v2.trim() // 报错
let v3:unknown = (n = 0) => n + 1;
v3() // 报错
3.unknown
类型变量能够进行的运算是有限的,只能进行比较运算(运算符==
、===
、!=
、!==
、||
、&&
、?
)、取反运算(运算符!
)、typeof
运算符和instanceof
运算符这几种,其他运算都会报错。
怎么才能使用unknown
类型变量呢? “类型缩小” 将一个不确定的类型缩小为更明确的类型
let a:unknown = 1;
if (typeof a === 'number') {
let r = a + 10;
}
unknown
可以看作是更安全的any
。一般来说,凡是需要设为any
类型的地方,通常都应该优先考虑设为unknown
类型。
3、never
为了保持与集合论的对应关系, 引入“空类型”的概念,即该类型为空,不包含任何值。由于不存在任何属于“空类型”的值,所以该类型被称为never
,即不可能有这样的值。
never
类型的使用场景,主要是在一些类型运算之中,保证类型运算的完整性,不可能返回值的函数,返回值的类型就可以写成never
never
类型的一个重要特点是,可以赋值给任意其他类型。为什么never
类型可以赋值给任意其他类型呢?这也跟集合论有关,空集是任何集合的子集。任何类型都包含了never
类型,TypeScript 把这种情况称为“底层类型”(bottom type)
TypeScript 有两个“顶层类型” (any
和unknown
),但是“底层类型” 只有never
唯一一个。
4、基本类型
JavaScript语言(注意,不是 TypeScript)将值分成8种类型。
TypeScript继承了JavaScript 的类型设计,以上8种类型可以看作 TypeScript 的基本类型。
首字母大写的Number
、String
、Boolean
等在 JavaScript 语言中都是内置对象,而不是类型名称。
undefined 和 null 既可以作为值,也可以作为类型
bigint 与 number 类型不兼容。
如果没有声明类型的变量,被赋值为undefined
或null
,它们的类型会被推断为any
如果希望避免这种情况,则需要打开编译选项strictNullChecks
。
// 打开编译设置 strictNullChecks
let a = undefined; // undefined
const b = undefined; // undefined
let c = null; // null
const d = null; // null
包装对象
boolean、string、number、bigint、symbol 上面这五种原始类型的值,都有对应的包装对象(wrapper object)。所谓“包装对象”,指的是这些值在需要时,会自动产生的对象。
上面示例中,字符串hello
执行了charAt()
方法。但是,在 JavaScript 语言中,只有对象才有方法,原始类型的值本身没有方法。这行代码之所以可以运行,就是因为在调用方法时,字符串会自动转为包装对象,charAt()
方法其实是定义在包装对象上。
五种包装对象之中,symbol 类型和 bigint 类型无法直接获取它们的包装对象(即Symbol()
和BigInt()
不能作为构造函数使用),但是剩下三种可以。
Boolean()
String()
Number()
以上三个构造函数,执行后可以直接获取某个原始类型值的包装对象。
const s = new String('hello');
typeof s
s.charAt(1) // 'e'
注意,String()
只有当作构造函数使用时(即带有new
命令调用),才会返回包装对象。如果当作普通函数使用(不带有new
命令),返回就是一个普通字符串。其他两个构造函数Number()
和Boolean()
也是如此。
包装类型和字面量类型
包装对象的存在,导致每一个原始类型的值都有包装对象和字面量两种情况。
'hello' // 字面量
new String('hello') // 包装对象
为了区分这两种情况,TypeScript 对五种原始类型分别提供了大写和小写两种类型。
大写类型同时包含包装对象和字面量两种情况,小写类型只包含字面量,不包含包装对象。
const n1:Number = 1; // 正确
const n2:Number = new Number(1); // 正确
const n3:number = 1; // 正确
const n4:number = new Number(1); // 报错
上面示例中,Number
类型可以赋值为数字的字面量,也可以赋值为包装对象。但是,number
类型只能赋值为字面量,赋值为包装对象就会报错。
建议只使用小写类型,不使用大写类型。因为绝大部分使用原始类型的场合,都是使用字面量,不使用包装对象。
很多内置方法的参数,定义成小写类型,使用大写类型会报错。
const n1:number = 1;
const n2:Number = 1;
Math.abs(n1)
Math.abs(n2) // 报错
Symbol()
和BigInt()
这两个函数不能当作构造函数使用,所以没有办法直接获得 symbol 类型和 bigint 类型的包装对象,因此Symbol
和BigInt
这两个类型虽然存在,但是完全没有使用的理由。
Object类型和object类型
TypeScript的对象类型也有大写Object
和小写object
两种。
大写的Object
类型代表 JavaScript 语言里面的广义对象。所有可以转成对象的值,都是Object
类型,这囊括了几乎所有的值。除了undefined
和null
这两个值不能转为对象,其他任何值都可以赋值给Object
类型。
let obj:Object;
obj = true;
obj = 'hi';
obj = 1;
obj = { foo: 123 };
obj = [1, 2];
obj = (a:number) => a + 1;
obj = undefined;
obj = null; // 报错
空对象{}是Object类型的简写形式,所以使用Object时常常用空对象代替。小写的object类型代表JavaScript里面的狭义对象,即可以用字面量表示的对象,只包含对象、数组和函数,不包括原始类型的值。let obj:object;
obj = { foo: 123 };
obj = [1, 2];
obj = (a:number) => a + 1;
obj = true;
obj = 'hi'; // 报错
obj = 1; // 报错
undefined 和 null 的特殊性
undefined
和null
既是值,又是类型。以便跟 JavaScript 的行为保持一致,TypeScript 就允许了任何类型的变量都可以赋值为这两个值。
const obj:object = undefined;
obj.toString()
为了避免这种情况,及早发现错误,TypeScript提供了一个编译选项strictNullChecks。只要打开这个选项,undefined
和null
就不能赋值给其他类型的变量(除了any
类型和unknown
类型)。
5、值类型
TypeScript 规定,单个值也是一种类型,称为“值类型”。
let x:'hello';
x = 'hello'; // 正确
x = 'world'; // 报错
TypeScript 推断类型时,遇到const
命令声明的变量,如果代码里面没有注明类型,就会推断该变量是值类型。
因为const
命令声明的变量,一旦声明就不能改变,相当于常量。值类型就意味着不能赋为其他值。
// x 的类型是 "https"
const x = 'https';
// y 的类型是 string
const y:string = 'https';
注意,const
命令声明的变量,如果赋值为对象,并不会推断为值类型。
值类型可能会出现一些很奇怪的报错。
let x:5 = 5;
let y:number = 4 + 1;
x = y;
y = x; // 正确
等号左侧的类型是数值5
,等号右侧4+1
的类型,TypeScript推测为number
。由于5
是number
的子类型,number
是5
的父类型,父类型不能赋值给子类型,所以报错了。反过来是可以的,子类型可以赋值给父类型。
6、联合类型和交叉类型
联合类型A|B表示,任何一个类型只要属于A或B,就属于联合类型A|B。let x:string|number;
x = 123;
x = 'abc'; // 正确
交叉类型A&B
表示,任何一个类型必须同时属于A
和B
,才属于交叉类型A&B
,即交叉类型同时满足A
和B
的特征。
交叉类型的主要用途是表示对象的合成。
let obj: { foo: string } & { bar: string };
obj = {
foo: 'hello',
bar: 'world'
};
交叉类型常常用来为对象类型添加新属性。类型B是一个交叉类型,用来在A的基础上增加了属性bar。type A = { foo: number };
type B = A & { bar: number };
7、type命令 typeof运算符
type命令
type`命令用来定义一个类型的别名。
别名不允许重名。同一个别名Color
声明了两次,就报错了
type Color = 'red';
type Color = 'blue';
别名支持使用表达式,也可以在定义一个别名时,使用另一个别名,即别名允许嵌套。type World = "world";
type Greeting = `hello ${World}`;
type
命令属于类型相关的代码,编译成 JavaScript 的时候,会被全部删除。
typeof运算符
JavaScript 语言中,typeof 运算符是一个一元运算符,返回一个字符串,代表操作数的类型。
typeof undefined; // "undefined"
typeof true; // "boolean"
typeof 1337; // "number"
typeof "foo"; // "string"
typeof {}; // "object"
typeof parseInt; // "function"
typeof Symbol(); // "symbol"
typeof 127n // "bigint"
TypeScript将typeof运算符移植到了类型运算,它的操作数依然是一个值,但是返回的不是字符串,而是该值的 TypeScript 类型。const a = { x: 0 };
type T0 = typeof a;
type T1 = typeof a.x; // number
这种用法的typeof返回的是 TypeScript 类型,所以只能用在类型运算之中(即跟类型相关的代码之中),不能用在值运算。let a = 1;
let b:typeof a;
if (typeof a === 'number') {
b = a;
}
上面示例中,用到了两个typeof
,第一个是类型运算,第二个是值运算。它们是不一样的,不要混淆。
JavaScript 的 typeof 遵守 JavaScript 规则,TypeScript 的 typeof 遵守 TypeScript 规则。它们的一个重要区别在于,编译后,前者会保留,后者会被全部删除。上面示例中,编译后只保留了原始代码的第二个typeof,删除了第一个 typeof。
typeof 的参数只能是标识符,不能是需要运算的表达式。
上面示例会报错,原因是 typeof 的参数不能是一个值的运算式,而Date()
需要运算才知道结果。
typeof
命令的参数不能是类型。
type Age = number;
type MyAge = typeof Age;
TypeScript 的类型存在兼容关系,某些类型可以兼容其他类型。type T = number|string;
let a:number = 1;
let b:T = a;
上面示例中,变量a
和b
的类型是不一样的,但是变量a
赋值给变量b
并不会报错。这时,我们就认为,b
的类型兼容a
的类型。
TypeScript 为这种情况定义了一个专门术语。如果类型A
的值可以赋值给类型B
,那么类型A
就称为类型B
的子类型(subtype)。在上例中,类型number
就是类型number|string
的子类型。
TypeScript 的一个规则是,凡是可以使用父类型的地方,都可以使用子类型,但是反过来不行。
8、数组
TypeScript 数组有一个根本特征:所有成员的类型必须相同,但是成员数量是不确定的,可以是无限数量的成员,也可以是零成员。
数组的类型有两种写法。第一种写法是在数组成员的类型后面,加上一对方括号。
let arr:number[] = [1, 2, 3];
let arr:(number|string)[];
数组类型的第二种写法是使用 TypeScript 内置的 Array 接口。let arr:Array<number> = [1, 2, 3];
数组类型声明了以后,成员数量是不限制的,任意数量的成员都可以,也可以是空数组。这种规定的隐藏含义就是,数组的成员是可以动态变化的。let arr:number[] = [1, 2, 3];
arr[3] = 4;
arr.length = 2;
arr
上面示例中,数组增加成员或减少成员,都是可以的。
9、元组
元组(tuple)是 TypeScript 特有的数据类型,JavaScript 没有单独区分这种类型。它表示成员类型可以自由设置的数组,即数组的各个成员的类型可以不同。
数组的成员类型写在方括号外面(number[]
),元组的成员类型是写在方括号里面([number]
)
使用元组时,必须明确给出类型声明(上例的[number]
),不能省略,否则 TypeScript 会把一个值自动推断为数组。
元组成员的类型可以添加问号后缀(?
),表示该成员是可选的。
let a:[number, number?] = [1];
注意,问号只能用于元组的尾部成员,也就是说,所有可选成员必须在必选成员之后。type myTuple = [
number,
number,
number?,
string?
];
由于需要声明每个成员的类型,所以大多数情况下,元组的成员数量是有限的,从类型声明就可以明确知道,元组包含多少个成员,越界的成员会报错。let x:[string, string] = ['a', 'b'];
x[2] = 'c';
但是,使用扩展运算符(...),可以表示不限成员数量的元组。type NamedNums = [
string,
...number[]
];
const a:NamedNums = ['A', 1, 2];
const b:NamedNums = ['B', 1, 2, 3];
10、函数
函数的类型声明,需要在声明函数时,给出参数的类型和返回值的类型。
function hello( txt:string ):void {
console.log('hello ' + txt);
}
上面示例中,函数hello()
在声明时,需要给出参数txt
的类型(string),以及返回值的类型(void
),后者写在参数列表的圆括号后面。void
类型表示没有返回值
如果不指定参数类型(比如上例不写txt的类型),TypeScript 就会推断参数类型,如果缺乏足够信息,就会推断该参数的类型为any
返回值的类型通常可以不写,因为 TypeScript 自己会推断出来。
如果变量被赋值为一个函数,变量的类型有两种写法。
// 写法一
const hello = function (txt:string) {
console.log('hello ' + txt);
}
// 写法二
const hello:
(txt:string) => void
= function (txt) {
console.log('hello ' + txt);
};
上面示例中,变量hello
被赋值为一个函数,它的类型有两种写法。写法一是通过等号右边的函数类型,推断出变量hello
的类型;写法二则是使用箭头函数的形式,为变量hello
指定类型,参数的类型写在箭头左侧,返回值的类型写在箭头右侧。
写法二有两个地方需要注意。
首先,函数的参数要放在圆括号里面,不放会报错。
其次,类型里面的参数名(本例是txt
)是必须的。有的语言的函数类型可以不写参数名(比如 C 语言),但是 TypeScript 不行。如果写成(string) => void
,TypeScript 会理解成函数有一个名叫 string 的参数,并且这个string
参数的类型是any
。
type MyFunc = (string, number) => number;
11、泛型
有些时候,函数返回值的类型与参数类型是相关的。
function getFirst(arr) {
return arr[0];
}
上面示例中,函数getFirst()总是返回参数数组的第一个成员。参数数组是什么类型,返回值就是什么类型。function f(arr:any[]):any {
return arr[0];
}
上面的类型声明,就反映不出参数与返回值之间的类型关系。为了解决这个问题,TypeScript 就引入了“泛型”(generics)。泛型的特点就是带有“类型参数”(type parameter)。function getFirst<T>(arr:T[]):T {
return arr[0];
}
上面示例中,函数getFirst()
的函数名后面尖括号的部分<T>
,就是类型参数,参数要放在一对尖括号(<>
)里面。本例只有一个类型参数T
,可以将其理解为类型声明需要的变量,需要在调用时传入具体的参数类型。
上例的函数getFirst()
的参数类型是T[]
,返回值类型是T
,就清楚地表示了两者之间的关系。比如,输入的参数类型是number[]
,那么 T 的值就是number
,因此返回值类型也是number
。
函数调用时,需要提供类型参数
getFirst<number>([1, 2, 3])
//不过为了方便,函数调用时,往往省略不写类型参数的值,让 TypeScript 自己推断。
getFirst([1, 2, 3])
有些复杂的使用场景,TypeScript可能推断不出类型参数的值,这时就必须显式给出了。function comb<T>(arr1:T[], arr2:T[]):T[] {
return arr1.concat(arr2);
}
// 两个参数arr1、arr2和返回值都是同一个类型。如果不给出类型参数的值,下面的调用会报错。
comb([1, 2], ['a', 'b']) // 报错
// 但是,如果类型参数是一个联合类型,就不会报错。
comb<number|string>([1, 2], ['a', 'b']) // 正确
类型参数的名字,可以随便取,但是必须为合法的标识符。习惯上,类型参数的第一个字符往往采用大写字母。一般会使用T
(type 的第一个字母)作为类型参数的名字。如果有多个类型参数,则使用 T 后面的 U、V 等字母命名,各个参数之间使用逗号(“,”)分隔。
总之,泛型可以理解成一段类型逻辑,需要类型参数来表达。有了类型参数以后,可以在输入类型与输出类型之间,建立一一对应关系。
泛型主要用在四个场合:函数、接口、类和别名。
12、interface
interface是对象的模板,可以看作是一种类型约定,中文译为“接口”。interface Person {
firstName: string;
lastName: string;
age: number;
}
上面示例中,定义了一个接口Person
,它指定一个对象模板,拥有三个属性firstName
、lastName
和age
。任何实现这个接口的对象,都必须部署这三个属性,并且必须符合规定的类型。
interface 可以表示对象的各种语法,它的成员有5种形式。
(1)对象属性
interface Point {
x: number;
y: number;
}
//如果属性是可选的,就在属性名后面加一个问号。
interface Foo {
x?: string;
}
// 如果属性是只读的,需要加上readonly修饰符。
interface A {
readonly a: string;
}
interface A {
[prop: string]: number;
}
上面示例中,[prop: string]
就是属性的字符串索引,表示属性名只要是字符串,都符合类型要求。
一个接口中,最多只能定义一个字符串索引。字符串索引会约束该类型中所有名字为字符串的属性。
interface MyObj {
[prop: string]: number;
a: boolean;
}
interface A {
[prop: number]: string;
}
const obj:A = ['a', 'b', 'c'];
如果一个 interface 同时定义了字符串索引和数值索引,那么数值索引必须服从于字符串索引。interface A {
[prop: string]: number;
[prop: number]: string; // 报错
}
interface B {
[prop: string]: number;
[prop: number]: number; // 正确
}
上面示例中,数值索引的属性值类型与字符串索引不一致,就会报错。数值索引必须兼容字符串索引的类型声明。
(3)对象的方法
// 写法一
interface A {
f(x: boolean): string;
}
// 写法二
interface B {
f: (x: boolean) => string;
}
// 写法三
interface C {
f: { (x: boolean): string };
}
interface Add {
(x:number, y:number): number;
}
const myAdd:Add = (x,y) => x + y;
interface ErrorConstructor {
new (message?: string): Error;
}
interface 可以使用extends关键字,继承其他 interface。interface Shape {
name: string;
}
interface Circle extends Shape {
radius: number;
}
interface Box {
height: number;
width: number;
}
interface Box {
length: number;
}
//上面示例中,两个Box接口会合并成一个接口,同时有height、width和length三个属性。
interface与type的异同
很多对象类型即可以用 interface 表示,也可以用 type 表示。而且,两者往往可以换用,几乎所有的 interface 命令都可以改写为 type 命令。
interface 与 type 的区别有下面几点。
(1)type
能够表示非对象类型,而interface
只能表示对象类型(包括数组、函数等)。
(2)interface
可以继承其他类型,type
不支持继承。
继承的主要作用是添加属性,type
定义的对象类型如果想要添加属性,只能使用&
运算符,重新定义一个类型。
type Animal = {
name: string
}
type Bear = Animal & {
honey: boolean
}
继承时,type 和 interface 是可以换用的。interface 可以继承 type。
type Foo = { x: number; };
interface Bar extends Foo {
y: number;
}
interface Foo {
x: number;
}
type Bar = Foo & { y: number; };
(3)同名interface会自动合并,同名type则会报错。也就是说,TypeScript不允许使用type多次定义同一个类型。type A = { foo:number }; // 报错
type A = { bar:number }; // 报错
//type两次定义了类型A,导致两行都会报错。
interface A { foo:number };
interface A { bar:number };
const obj:A = {
foo: 1,
bar: 1
};
//interface把类型A的两个定义合并在一起。
这表明,inteface 是开放的,可以添加属性,type 是封闭的,不能添加属性,只能定义新的 type。
(4)interface
不能包含属性映射(mapping),type
可以
interface Point {
x: number;
y: number;
}
type PointCopy1 = {
[Key in keyof Point]: Point[Key];
};
// 报错
interface PointCopy2 {
[Key in keyof Point]: Point[Key];
};
(5)this
关键字只能用于interface
。
(6)type 可以扩展原始数据类型,interface 不行。
(7)interface
无法表达某些复杂类型(比如交叉类型和联合类型),但是type
可以。
type A = { };
type B = { };
type AorB = A | B;
type AorBwithName = AorB & {
name: string
};
上面示例中,类型AorB
是一个联合类型,AorBwithName
则是为AorB
添加一个属性。这两种运算,interface
都没法表达。
综上所述,如果有复杂的类型运算,那么没有其他选择只能使用type
;一般情况下,interface
灵活性比较高,便于扩充类型或自动合并,建议优先使用。
四、总结
上述部分主要总结了typescript的基本用法,涵盖了大部分使用场景。
阅读原文:原文链接
该文章在 2024/12/30 16:02:57 编辑过