TypeScript是一个JavaScript类型化的超集。为什么要使用TypeScript,引用官网的一段文字:
编程人员写出来的最多的错误就是类型错误:在应该使用某类值的地方使用了另一种类型的值。可能是由于一个简单的错别字,可能是对一个库暴露出的接口理解有误,可能是对代码运行时的行为有错误的假设,或者其他错误。TypeScript的目的是成为JavaScript项目的 静态(static) 类型检查(typechecker)工具,换句话说,在代码运行前(静态)TypeScript就会运行,并会确保项目中使用的类型是正确的。
其实TypeScript的作用不止类型检查,它还多了一些JavaScript目前没有(或还在草案阶段)的语法,比如枚举对象,抽象类、类的私有域等。
本文通过TypeScript官方手册学习使用TypeScript,全文主要包含官方手册内容 + 个人理解 + 练习代码。文章写得比较详细,篇幅较长,所以分为了以下五篇文章,一篇一篇耐心阅读和练习一定会有所收获的,如果发现什么错误的地方欢迎大家指出。
《TypeScript学习笔记-1-基础类型、字面量类型、类型声明》
《TypeScript学习笔记-2-联合类型&交叉类型、泛型、类型守卫、类型推断》
《TypeScript学习笔记-3-枚举、函数、类、装饰器》
《TypeScript学习笔记-5-tsc指令、TS配置、部分更新功能、通用类型》
准备工作
之前写了一个简单的脚手架,该脚手架创建的项目中已经安装了typescript并且使用tsconfig.json进行了基础配置,以及其他(比如babel等)项目配置,直接在项目中进行练习。(如果想先了解TypeScript的安装和配置,可以先看《TypeScript学习笔记-5-tsc指令、TS配置、部分更新功能、通用类型》)。
全局安装脚手架:
npm i @lana-rm/create-app -g
安装好之后,创建一个名为learn-typescript
的项目并运行:
create-app learn-typescript
cd learn-typescript
npm start
创建练习文件夹practice
,在文件夹中创建learnTypeScript.ts
和learnTypeScript01.ts
两个文件,在learnTypeScript.ts
对所有练习文件进行引入,在App.tsx
中使用learnTypeScript.ts
:import '@/practice/learnTypeScript';
引入文件。
这篇文章的练习就放在learnTypeScript01.ts
中。后续每篇文章的练习都会创建新的文件,到时候不再赘述这个步骤。
写例子的时候以a、b、c...作为变量名、x、y、z作为参数名,i、j、m、n作为索引名,A、B、C作为类名、m1、m2、m3作为成员名,这种起名法是不可取的,练习的时候方便直观才这样写,实际开发中一定要进行有意义的命名。
接下来就正式开始练习吧∠( ᐛ 」∠)_
基础类型
基础类型指的是一些最简单的数据单位的类型。在JavaScript中,一共有8种数据类型,7种原始类型和object
,函数function
和数组array
都包含在object
中。TypeScript在此基础上增加了枚举enum
数据类型。TypeScript也给出了一些在特定场景下的数据的类型,比如unknown
、never
、void
。
先来看看7种原始类型:
boolean
、number
、bigint
、string
、symbol
、null
、undefined
。
boolean
布尔类型的值是true或fasle。
const a: boolean = false;
使用: boolean
对a进行了注释,表明a是一个布尔值。
这样写会有一个eslint报错:
error Type boolean trivially inferred from a boolean literal, remove type annotation @typescript-eslint/no-inferrable-types
在编译器能简单推断出类型的地方使用类型注释就会出现这个报错。因为从一个布尔字面量能够简单的推断出a
是布尔类型,所以这里的类型注释是多余的,只会增加代码量。
仅在这几篇文章的练习中,为了更加直观的看到类型,在.eslintrc.js
中关掉这个检查:
rules: {
...
'@typescript-eslint/no-inferrable-types': 'off',
},
已经将a
注释为布尔类型的值,再将其他类型的值赋给a
会报错:
let a: boolean = false;
a = ''; // 报错:Type '""' is not assignable to type 'boolean'.
number
用来表示浮点数字。支持十进制、十六进制、二进制、八进制。
const b: number = 123; // 十进制
const b1: number = 0xabc; // 十六进制
const b2: number = 0b10; // 二进制
const b3: number = 0o567; // 八进制
bigint
bigint
可以表示任意大的整数。JavaScript中原本可以表示的最大的整数是2^53 - 1
,biginit
可以用来表示比2^53 - 1
大的整数。在数字字面量后面加一个n
表示bigint
或者使用BigInt
来创建bigint
。
const c: bigint = 123n;
const c1: bigint = 1234567891012345678910123n;
const c2: bigint = BigInt('1234567891012345678910123');
string
字符串表示文本数据,使用""
、''
、或``包裹。
使用``包裹的是模版字符串,在模版字符串中可以嵌套表达式。
const d: string = 'd';
const d1: string = "d1";
const d2: string = `组合${d}和${d1}`;
symbol
使用Symbol构造函数创建symbol类型的值,symbol值是匿名的、独一无二的、不可被修改的,它用来声明一个对象的属性或者一个类成员。
const e1: symbol = Symbol('e');
const e2: symbol = Symbol('e');
const e3: symbol = Symbol('e2');
// console.log(e1 === e2); // e1 === e2会永远返回false,e1和e2是不同的,虽然它们传给Symbol的都是'e'
// 用symbol类型的值声明对象属性
const e4 = {
[e1]: '1',
[e2]: '12',
[e3]: '123',
};
console.log(e4[e1], e4[e2]); // 打印结果:1 12
// 用symbol类型的值声明类成员
class E {
[e1](): string {
return 'e1';
}
}
const e = new E();
console.log(e[e1]()); // 打印结果:e1
symbol
属性需要和对象声明属性的语法([表达式]
)一起使用。
console.log(e4[e1], e4[e2]);
这句会报错Type 'symbol' cannot be used as an index type
(symbol类型不能作为索引类型使用)。这是TypeScript的bug,TypeScript代码贡献者给出的回答是使用unique symbol
类型来处理这个问题,unique symbol
表示明确地只和一个symbol
关联的类型。
const e1: unique symbol = Symbol('e');
const e2: unique symbol = Symbol('e');
null和undefined
null
类型只包含null
,undefined
类型只包含undefined
。
null
和undefined
在默认情况下是所有其他类型的子类型,所以可以赋给其他类型。比如:
let f: string = 'f';
f = null;
f = undefined;
像上面那样写是不会有类型报错的。
开启严格null检查后,null
和undefined
只能被赋值给它们各自的类型以及unknown
、any
(有一种特殊情况是undefined
可以赋给void
,void
一般用来表示没有返回值的函数的返回类型,因为在JavaScript中没有明确返回值的时候,函数会默认返回undefined
,所以这种特殊情况也说得通)。
在tsconfig.json
配置文件中加上以下配置开启严格null检查:
"compilerOptions": {
"strictNullChecks": true,
...
(如果用的是tsc指令:加上--strictNullChecks
)
开启strictNullChecks
后以上代码会给出错误提示:
let f: string = 'f';
f = null; // 报错:Type 'null' is not assignable to type 'string'.
f = undefined; // 报错:Type 'undefined' is not assignable to type 'string'.
object
在JavaScript中,除了上面的7种原始类型之外的类型都是object
类型。
let g: object = {
m1: '1'
};
// 这里只是为了说明object类型包含函数、数组、元组、枚举等类型,后者可以赋给前者
const g1 = (): string => { return '2'; }; // 函数
const g2 = [1, 2, 3]; // 数组
const g3: [string, number] = ['1', 1]; // 元组
enum G4 { // 枚举
M1,
M2,
M3
}
g = g1;
g = g2;
g = g3;
g = G4;
和JavaScript类似,TypeScript中的函数、数组、元组、枚举等有自身的类型,同时也包含在object
类型下,除此之外TypeScript还有一些特定场景的类型unknown
、never
、void
、any
。函数会在《TypeScript学习笔记-3-枚举、函数、类、装饰器》中提到,在这篇文章中不作说明。下面依次学习数组、元组、枚举、void
、never
、unknown
、any
类型。
数组
数组(array)类型。通过元素类型[]
和Array<元素类型>
两种方式来声明数组类型。
const h: number[] = [1, 2];
const h1: Array<number> = [1, 2];
使用ReadonlyArray<元素类型>
声明只读数组,只读数组剔除了修改数组的方法,只读数组不能被修改。
let h2: ReadonlyArray<string> = ['1', '2'];
h2[0] = '3'; // 报错:Index signature in type 'readonly string[]' only permits reading.
h2.push('3'); // 报错:Property 'push' does not exist on type 'readonly string[]'.
tuple
元组类型表示有固定元素数量、每个元素类型已知、不是所有元素的类型都一样的数组的类型。
const i: [string, number, number] = ['1', 2, 3];
声明元组类型后,赋给元素其他类型的值会报错、使用元素类型不包含的方法会报错,引用一个不存在的元素会报错。总之,元素的数量,每个元素的类型必需符合定义的类型。
const i: [string, number, number] = ['1', 2, 3];
i[0] = 1; // 报错:Type '1' is not assignable to type 'string'.
i[1].split(''); // 报错:Property 'split' does not exist on type 'number'.
i[10]; // 报错:Tuple type '[string, number, number]' of length '3' has no element at index '10'.
enum
枚举类型的作用是给常量更友好地命名。TypeScript支持数字枚举和字符串枚举。
enum J {
M1,
M2,
M3
}
console.log(J.M1, J.M2, J.M3); // 打印:0 1 2
数字枚举默认是从0开始的,往后依次加1。
可以给枚举成员赋值,往后的成员的数字会从赋的值开始,依次加1。
enum J {
M1,
M2 = 2,
M3
}
console.log(J.M1, J.M2, J.M3); // 打印:0 2 3
这里只简单的说明,在《TypeScript学习笔记-3-枚举、函数、类、装饰器》中可以查看更多枚举相关的知识。
void
void表示没有任何类型。它一般用于表示没有返回值的函数的返回类型。
开启strictNullChecks
之前,可以将null
和undefined
赋给void
类型,打开strictNullChecks
之后,只可以将undefined
赋值给void
类型。
function k (): void {
console.log('k');
}
const k1: void = undefined;
const k2: void = null; // 报错:Type 'null' is not assignable to type 'void'.
never
never
类型表示永远不会出现的值的类型。比如抛出错误或者永远不会返回的函数的返回类型,或者被类型推断为永远不为真的类型。
never
是所有类型的子类型,它可以赋值给所有类型。但是never
类型没有子类型,除了never
自身外,其他任何类型都不能赋给never
类型。
// 抛出错误的函数
function l (): never {
throw new Error('error');
}
// 永远不会返回的函数
function l1 (): never {
l1();
}
// 类型推断为never类型
const l3: never = function () { throw new Error('error'); }();
关于类型推断,在《TypeScript学习笔记-2-联合类型&交叉类型、泛型、类型守卫、类型推断》中能查看更多内容。
unknown
未知类型表示这个变量未来可能是任意类型,但是目前不知道会是什么。比如接口返回的数据。可以给unknown
类型赋任何类型的值。
let m: unknown;
m = '1';
m = {
m1: '2'
};
// 比较检查
if (m === '1') {
console.log(m);
}
// typeof检查
if (typeof m === 'string') {
console.log(m);
}
// 类型守卫
function m1 (x): void {
if ('m1' in x) {
console.log(x.m1);
}
}
m1(m);
这个例子中如果直接使用m.m1
会报错,
let m: unknown;
m = '1';
m = {
m1: '2'
};
console.log(m.m1); // 报错:Object is of type 'unknown'.
明明m
已经被赋值{ m1: '2'};
,但因为m
的类型为未知类型unknown
,它代表m
是未知的,不一定是一个包含m1
属性的对象,直接使用console.log(m.m1);
会报错,需要使用类型守卫确保在m
有属性m1
的时候,才使用m.m1
。
关于类型守卫,在《TypeScript学习笔记-2-联合类型&交叉类型、泛型、类型守卫、类型推断》中能查看更多内容。
any
any
类型相当于任意类型。使用any
类型相当于在这个地方关掉TypeScript类型检查。只有在必要的时候使用any
类型,比如JavaScript转换成TypeScript的时候一些不得不关掉类型检查的地方。
let n: any = 1;
n = '1';
n.join(', '); // 这个地方运行前不会给出TypeScript报错,代码运行时会报错Uncaught TypeError: n.join is not a function
字面量类型
字面量类型是更确切的一种类型。一共有三种字面量类型:字符串字面量类型、数字字面量类型、布尔字面量类型。
const o: 'a' = 'a';
const o1: 1 = 1;
const o2: false = false;
o
的类型是确切的字符串'a'
,o
只能被赋值'a'
。o1
的类型是确切的数字1
,o2
的类型是确切的布尔值false
。
类型断言
类型断言告诉TypeScript,这个地方的类型我(开发者)比你清楚,你不用管了按我定的来。类型断言有两种语法,as语法和尖括号语法。
const p: unknown = 'a,b,c';
p.split(','); // 报错:Object is of type 'unknown'.
unknown
类型的p
直接这样写会报错,可以使用类型断言:
as
语法:
(p as string).split(',');
或者尖括号语法:
(<string>m).split(',');
在使用TypeScript和JSX的时候,只有as
语法可以用,在APP.tsx中尝试使用(<string>m).split(',');
会报错:
Property 'string' does not exist on type 'JSX.IntrinsicElements'.
JSX element 'string' has no corresponding closing tag.
因为<string>
被当成HTML元素来处理了,但是有没有找到对应的元素。
类型声明
还没学好TypeScript就直接看代码的时候我是懵圈的,interface
、type
、declare
、class
都可以声明类型,它们看起来好像用法一样又好像又不一样,它们之间有什么区别,具体又该怎么使用呢ಠ_ಠ ?。
interfaces (接口)
接口是用来命名类型的。
// 命名一个包含数字类型的m1成员和m2成员的类型
interface Q {
m1: number;
m2: number;
}
// x的类型为Q,函数返回值的类型也为Q
function q (x: Q): Q {
const { m1, m2 } = x;
return {
m1: m1 + m2,
m2: m1 - m2,
};
}
const q1 = {
m1: 1,
m2: 2,
m3: 3,
};
q(q1);
函数q
的参数是一个对象,包含number
类型的m1
属性和m2
属性。传入的参数q1
中其实还包含另一个成员m3
,但是类型检查只会检查传入的参数是否包含{ m1: number; m2: number; }
中的属性和类型,只要包含就认为符合该类型,不会考虑多出来的m3
。
如果不使用interface
来命名类型,直接给参数和返回值注释类型,上面的那段代码的函数声明会是这样:
function q (
x: {
m1: number;
m2: number;
}
): {
m1: number;
m2: number;
}
{
const { m1, m2 } = x;
return {
m1: m1 + m2,
m2: m1 - m2,
};
}
可选属性
定义接口的时候,在属性名后面加一个?
表示这个属性是可选的。
interface Q {
m1: number;
m2: number;
m4?: string;
}
只读属性
在属性名前加一个readonly
表示只读属性。只读属性不能被修改。
interface Q {
readonly m1: number;
m2: number;
}
function q (x: Q): Q {
const { m1, m2 } = x;
x.m1 = 1; // 报错:Cannot assign to 'm1' because it is a read-only property.
return {
m1: m1 + m2,
m2: m1 - m2,
};
}
函数类型
接口有一个调用标志表示函数类型,就像只包含参数和返回值的函数声明。
interface Q1 {
(x: Q): Q;
}
const q: Q1 = function q (x) {
const { m1, m2 } = x;
return {
m1: m1 + m2,
m2: m1 - m2,
};
}
Q
是已经声明过的函数参数和返回值的类型,Q1
是函数的类型(包含参数类型和返回值类型)。等式const q: Q1 = function q (x) {...
右侧的参数类型和返回值类型不用再写了(function q (x: Q): Q {...
),因为TypeScript的上下文推断能通过左侧的Q1
类型,推断出右侧的函数的参数类型和返回值类型。
函数的参数名不需要和interface
中的参数名一样。比如Q1
也可以写成:
interface Q1 {
(y: Q): Q;
}
可索引类型
可索引类型包含索引标志的类型以及相应的索引值的类型。
interface Q2 {
[index: number]: boolean;
}
const q2: Q2 = [true, false];
console.log(q2[0]);
这里q2
是一个元素为字符串的数组,索引标志的类型是number
,相应的索引值的类型是boolean
。
索引标志的类型有number
和string
,要注意number
索引对应的值的类型必须是string
索引对应的值的类型的子类型,因为JavaScript在使用数字索引的时候,会先将数字转化字符串。
interface Q3 {
[index: number]: boolean;
[index: string]: boolean | object;
}
boolean | object
是联合类型,它表示boolean
或object
,是包含boolean
类型的。这里number
索引的类型是string
索引类型的子类型,所以没问题,但是当反过来的时候:
interface Q4 {
[index: number]: boolean | object; // 报错:Numeric index type 'boolean | object' is not assignable to string index type 'boolean'.
[index: string]: boolean;
}
字符串索引对应的类型一定要涵盖所有其他属性的类型,因为不论是 obj.property
还是 obj[property(数字属性)]
,都可以使用 obj["property"]
替代。
interface Q5 {
[index: number]: boolean;
[index: string]: boolean | object;
m1: void; // 报错:Property 'm1' of type 'void' is not assignable to string index type 'boolean | object'.
}
像这样写就没问题:
interface Q6 {
[index: number]: boolean;
[index: string]: boolean | object | void;
m1: void;
}
过度类型检查
在TypeScript中,对象字面量在赋值给其他变量或者作为参数传递的时候会被特殊处理以及接受过度类型检查。如果对象字面量中包含了目标类型中没有的属性的时候,就会报错。
interface Q7 {
m1?: number;
m2: string;
}
const q3: Q7 = {
m2: '2',
m3: '3',
// 报错:Type '{ m2: string; m3: string; }' is not assignable to type 'Q7'.
// Object literal may only specify known properties, and 'm3' does not exist in type 'Q7'.
};
按照“只要包含类型中的属性就认为符合该类型,多出来的属性不会考虑”的思想,这个例子中m1
是可选的,{ m2: '2', m3: '3', }
中包含string
类型的m2
,m3
是多出来的属性,可以不予考虑,但是由于过度类型检查,这里会报错。
有三种方式绕开过度类型检查:
1.使用类型断言:
interface Q7 {
m1?: number;
m2: string;
}
const q3: Q7 = {
m2: '2',
m3: '3',
} as Q7;
2.先将值赋给另一个变量:
interface Q7 {
m1?: number;
m2: string;
}
const q4 = {
m2: '2',
m3: '3',
};
const q3: Q7 = q4;
这种方式在对象至少包含一个通用属性的时候会有效,没有包含通用属性的时候会失效:
interface Q8 {
m1?: number;
m2?: string;
}
const q5 = {
m1: 1,
m3: '2',
};
const q6: Q8 = q5; // 不报错,因为有m1
const q7 = { m3: 2 };
const q8: Q8 = q7; // 报错:Type '{ m3: number; }' has no properties in common with type 'Q8'.
3.添加一个字符串索引标志:
interface Q9 {
m1?: string;
m2: number;
[index: string]: any;
}
const q9: Q9 = {
m2: 2,
m3: '3',
};
添加了 [index: string]: any;
后Q9
类型除了可选的string
类型的m2
属性,和必选的number
类型的m2
属性之外,还可以添加任意数量,任意类型的字符串索引的属性。
类类型
在TypeScript中,可以使类具有特定的类型。通过实现接口(implements an interface
)来描述类的公有(public)部分。
interface Q10 {
m1: string;
m2(x: number): number;
}
class Q11 implements Q10 {
m1 = '1';
m2(x: number): number {
return x;
}
}
使用接口Q10
描述类的公有部分的类型:包含一个string
类型的m1
成员和参数类型和返回值类型都是number
的函数类型的m2
成员。
使用类类型的时候,要记住类类型包含静态部分的类型和实例部分的类型。实现一个接口只会检查实例部分的类型,不会检查静态部分的类型。
interface Q10 {
m1: string;
m2(x: number): number;
new (x: string): void;
}
class Q11 implements Q10 {
m1 = '1';
m2(x: number): number {
return x;
}
constructor(x) {
this.m1 = x;
}
}
因为构造函数是类的静态部分,所以上述代码会报错:
Class 'Q11' incorrectly implements interface 'Q10'.
Type 'Q11' provides no match for the signature 'new (x: string): void'.
像下面这样单独定义构造函数的类型就没有问题:
interface Q10 {
m1: string;
m2(x: number): number;
}
interface Q12 {
new (x: string): void;
}
const Q11: Q12 = class Q11 implements Q10 {
m1 = '1';
m2(x: number): number {
return x;
}
constructor(x) {
this.m1 = x;
}
}
混合类型
因为JavaScript的灵活性,一个值可能会包含多个类型。比如一个即是函数(function
),又是对象(object
)的对象:
interface Q13 {
(x: number): number;
m1?: number;
m2?: boolean;
}
const q10: Q13 = (x) => { return x; };
q10.m1 = 1;
q10.m2 = false;
这里比较奇怪的是像这样写:const q10: Q13 = () => { return 1; };
也没有报错,按理说应该是必需得有一个参数的。
类型扩展
接口也可以像类那样互相扩展。通过扩展,接口能复制被扩展的接口的成员。
interface Q14 {
m1: number;
}
interface Q15 extends Q14 {
m2: string;
}
interface Q16 extends Q15 {
m3: boolean;
}
const q11: Q16 = {
m1: 1,
m2: '2',
m3: false,
};
接口扩展类
接口还可以从类扩展。一个接口从类类型扩展的时候,会继承类的成员,包括私有(private
)成员和保护(protected
)成员。
class Q17 {
private m1: number;
protected m2: number;
public m3: string;
}
interface Q18 extends Q17 {
m4: boolean;
}
// 类Q19扩展自类Q17 ,并实现接口Q18
class Q19 extends Q17 implements Q18 {
m4: boolean;
m5: string;
}
这个接口只能被它扩展的类(这里是Q17
)或者该类的子类(Q19
)实现。因为接口会继承私有成员和保护成员,不是继承自Q17
的类没有包含这些成员。
class Q20 implements Q18 {
m3: string;
m4: boolean;
}
Q20
不是Q17
的子类,这样写会报错:
Class 'Q20' incorrectly implements interface 'Q18'.
Type 'Q20' is missing the following properties from type 'Q18': m1, m2
注意这里的类是TypeScript中的类,不是JavaScript中的类。关于TypeScript中的类,会在《TypeScript学习笔记-3-枚举、函数、类、装饰器》中说明,这里只关注接口继承类。
type
使用type
给类型一个别名。类型别名没有创建一个新的类型,它只是创建了一个新名字来引用已有类型。
type R = number;
const r: R = 1; // R是类型number的别名
type R1 = {
m1: number;
m2: R1; // 类型别名可以在属性中使用自身
}
类型别名还可以是泛型:
type R2<T> = {
m1: T;
}
const r2: R2<number> = {
m1: 1,
}
从代码上很直观地可以看出,泛型就和函数类似。这里传入了类型number
作为m1
的类型。
在《TypeScript学习笔记-2-联合类型&交叉类型、泛型、类型守卫、类型推断》中会对泛型进行说明,这里只简单地了解即可。
interface 和 type
类型别名和接口相似,接口(interface
)有的功能类型(type
)别名基本都有。它们两主要的区别是,类型别名不能添加新的属性,但是接口总是可扩展的。
interface R3 {
m1: string;
}
interface R4 extends R3 {
m2: number;
}
const r3: R4 = {
m1: '1',
m2: 1,
};
type
本身不可以扩展,但是可以使用交叉类型扩展。
type R5 = {
m1: string;
};
type R6 = R5 & {
m2: number;
};
const r4: R6 = {
m1: '1',
m2: 1,
};
给已有接口添加一个新字段:
interface R3 {
m1: string;
}
interface R4 extends R3 {
m2: number;
}
interface R4 {
m3: number;
}
const r3: R4 = {
m1: '1',
m2: 1,
m3: 1,
};
以同样的方式给已有类型(type
)添加新字段会报错Duplicate identifier 'W5'.
:
type R5 = {
m1: string;
};
type R5 = { // 报错:Duplicate identifier 'R5'.
m3: string;
};
declare
declare
一般用来声明外部的代码的类型。比如在项目中用到了一个没有使用TypeScript的库,但是想要为这个库声明类型,就可以使用declare
声明全局的类型。
假如这个库的名字叫aLib
,在typings
文件夹下创建一个aLib.d.ts
文件。
声明包含属性的对象
假设这个库暴露出了一个aLib
变量,aLib
下包含一个参数是number
,返回值也是number
的函数。
可以这样声明类型:
declare namespace aLib {
function m1(x: number): number;
}
在App.tsx
中使用aLib.m3();
会报类型错误:
aLib.m3(); // Property 'm3' does not exist on type 'typeof aLib'.
重载函数
假设aLib
库暴露了一个传入number
类型会返回string
类型,传入string
类型会返回boolean
类型的函数aLibM2
。
可以这样声明类型:
declare function aLibM2(x: number): string;
declare function aLibM2(x: string): boolean;
在App.tsx
中像下面这样传入number
或者string
之外的其他类型:
aLibM2([]);
会报类型错误:
No overload matches this call.
Overload 1 of 2, '(x: number): string', gave the following error.
Argument of type 'never[]' is not assignable to parameter of type 'number'.
Overload 2 of 2, '(x: string): boolean', gave the following error.
Argument of type 'never[]' is not assignable to parameter of type 'string'.
可重用类型(接口Interfaces)
假设aLib
库暴露了一个参数为string
类型的m1
属性和number
类型的m2
属性组成的对象,没有返回值的对象。
可以这样声明类型:
interface ALibA {
m1: string;
m2: number;
}
declare function aLibM3(x: ALibA): void;
在App.tsx
中像下面这样使用:
aLibM3(null);
会报类型错误:Argument of type 'null' is not assignable to parameter of type 'ALibA'.
可重用类型(类型别名)
假设aLib
库暴露了一个参数为string
类型或number
类型,没有返回值的对象。
可以这样声明类型:
type ALibB = string | number;
declare function aLibM4(x: ALibB): void;
在App.tsx
中像下面这样使用:
aLibM4(false);
会报错:
Argument of type 'false' is not assignable to parameter of type 'ALibB'.
组织类型
1.可以使用命名空间组织类型:
declare namespace ALib {
interface ALibC {
m1: number;
}
interface ALibD {
m1: string;
}
}
在App.tsx
中像下面这样使用:
function a (x: ALib.ALibC) {
console.log('a');
}
2.在一个声明中使用嵌套的命名空间
declare namespace ALib.TypeA {
interface C {
m1: number;
}
interface D {
m1: string;
}
}
在App.tsx
中像下面这样使用:
function b (x: ALib.TypeA.C) {
console.log(x);
}
类
可以使用declare class
来描述一个类或者类似类的对象。类能包含属性、方法、和一个构造函数。
declare class ALibC {
constructor(x: number);
m1: number;
m2(x: number): number;
}
在App.tsx
中像下面这样使用:
const c = new ALibC(1);
c.m1;
全局变量
declare var aLibD1: number;
declare let aLibD2: number;
declare const aLibD3: number;
在App.tsx
中像下面这样使用:
aLibD1 = 1;
aLibD2 = 2;
aLibD3 = 3; // 报错:Cannot assign to 'aLibD3' because it is a constant.
全局函数
declare function aLibE (): void;
在App.tsx
中像下面这样使用:
aLibE(1); // 报错:Expected 0 arguments, but got 1.
类
类不是专门用来声明类型的,只是类声明的时候会创建代表类实例的类型。所以在一些和接口相同的地方可以作为接口来使用。比如:
class P {
m1: string;
m2: number;
}
const p1: P = {
m1: '1',
m2: 2,
}
在《TypeScript学习笔记-3-枚举、函数、类、装饰器》中会对类进行说明。
如何选择
内部代码声明类型使用interface
,遇到无法使用interface
描述的类型的时候使用type
,class
一般不专门作为类型声明来使用。
外部代码声明类型使用declare
。就比如想在文件A中使用一些变量,但是又不想在文件A中声明类型,也不想从别的模块中使用import
引入类型声明,那就在一个A.d.ts
文件中使用declare namespace A ...
进行相应的变量的类型声明。或者想给一个没有类型的第三方库声明类型的时候,也使用declare
。
问题
学了这一节的知识后,可以尝试为下面这段JavaScript代码声明类型,这种写法在我平时写代码的时候经常用到:
function s({ m1, m2 }) {
const arr = m1.split(', '); // 字符串对象有split方法
const str = m2.toFixed(2); // 数字对象有toFixed方法
console.log(arr, str);
}
我的解答:
方式一:
interface S {
m1: string;
m2: number;
}
function s({ m1, m2 }: S): void {
const arr = m1.split(', '); // 字符串对象有split方法
const str = m2.toFixed(2); // 数字对象有toFixed方法
console.log(arr, str);
}
const s1 = {
m1: 'a, b, c',
m2: 1.034
}
s(s1);
方式二:
interface S1 {
m1: string;
m2: number;
}
interface S2 {
(x: S1): void;
}
const s: S2 = ({ m1, m2 }) => {
const arr = m1.split(', '); // 字符串对象有split方法
const str = m2.toFixed(2); // 数字对象有toFixed方法
console.log(arr, str);
}
const s1 = {
m1: 'a, b, c',
m2: 1.034
}
s(s1);
TypeScript学习笔记 | |
---|---|
当前篇 | 《TypeScript学习笔记-1-基础类型、字面量类型、类型声明》 |
下一篇 | 《TypeScript学习笔记-2-联合类型&交叉类型、泛型、类型守卫、类型推断》 |
《TypeScript学习笔记-3-枚举、函数、类、装饰器》 | |
《TypeScript学习笔记-4-模块、命名空间》 | |
[《TypeScript学习笔记-5-tsc指令、TS配置、部分更新功能、通用类型》]( |