基本类型
| ES6 的数据类型 | TypeScript 的数据类型 |
|---|---|
| Boolean | Boolean |
| Number | Number |
| String | String |
| Array | Array |
| Function | Function |
| Object | Object |
| Symbol | Symbol |
| undefined | undefined |
| void | |
| any | |
| never | |
| 元祖 | |
| 枚举 | |
| 高级类型 |
类型注解
作用:相当于强类型语言中的类型声明
语法:(变量/函数): type
// 原始类型
let bool: boolean = true;
let num: number = 123;
let str: string = 'abc';
// 数组
let arr1: number[] = [1, 2, 3];
let arr2: Array<number | string> = [1, 2, 3, '4'];
// 元祖
let tuple: [number, string] = [0, '1'];
// 函数
let add = (x: number, y: number): number => x + y;
let compute: (x: number, y: number) => number;
compute = (a, b) => a + b;
// 对象
let obj: { x: number, y: number } = { x: 1, y: 2 };
obj.x = 3;
// symbol
let s1: symbol = Symbol();
let s2 = Symbol();
console.log(s1 === s2);
// undefined, null
let un: undefined = undefined;
let nu: null = null;
// void
let noReturn = () => { }
// any
let x;
x = 1;
x = [];
x = () => { };
// never
let error = () => {
throw new Error('error')
}
let endless = () => {
while (true) { }
}
枚举类型
枚举:一组有名字的常量集合。
// 数字枚举
enum Role {
Reporter = 1,
Developer,
Maintainer,
Owner,
Guest
}
console.log(Role.Reporter);
console.log(Role);
// 字符串枚举
enum Message {
Success = '恭喜你,成功了',
Fail = '抱歉,失败了'
}
// 异构枚举: 数字和字符串混用。这里 N 默认是 0,Y 为指定字符串。(不建议使用)
enum Answer {
N,
Y = 'Yes'
}
// 枚举成员
enum Char {
// const
a,
b = Char.a,
c = 1 + 3,
// computed
d = Math.random(),
e = '123'.length,
f = 4,
g,
h
}
// 常量枚举
const enum Month {
Jan,
Feb,
Mar
}
let month = [Month.Jan, Month.Feb, Month.Mar];
// 枚举类型
enum E { a, b }
enum F { a = 0, b = 1 }
enum G { a = 'apple', b = 'banana' }
let e: E = 3;
let f: F = 3;
// e === f // Error
let e1: E.a = 1;
let e2: E.b;
// e1 === e2 // Error
let e3: E.a = 1;
// e1 === e3; // Normal
let g1: G = G.b;
let g2: G.a = G.a;
// let g3: G = 'aaa'; // Error
// let g4: G.a = 'aaa'; // Error
// let g4: G.a = 'apple'; // Error
// 数字枚举类型可以赋值任意数字,字符串枚举类型只能赋值枚举的值
接口
对象类型接口
interface List {
readonly id: number;
name: string;
// [x: string]: any
age?: number
}
interface Result {
data: List[]
}
function render(result: Result) {
result.data.forEach((value) => {
console.log(value.id, value.name)
if (value.age) {
console.log(value.age)
}
// value.id++;
})
}
let result = {
data: [
{ id: 1, name: 'A', sex: 'male' },
{ id: 2, name: 'B' }
]
}
render(result);
// render(<Result>{
// data: [
// {id: 1, name: 'A', sex: 'male'},
// {id: 2, name: 'B'}
// ]
// })
// render({
// data: [
// { id: 1, name: 'A', sex: 'male' },
// { id: 2, name: 'B' }
// ]
// } as Result)
// 上面两个 render 等价,因为 <Result> 在 React 中有歧义,故不推荐使用
interface StringArray {
[index: number]: string
}
let chars: StringArray = ['A', 'B']
interface Names1 {
[x: string]: string;
// [z: number]: number; // Error 问题一
[z: number]: string;
}
interface Names2 {
[x: string]: any;
[z: number]: number
}
let names2: Names2 = {
'1': 123,
// 2: '456' // Error 问题二
2: 456
}
/**
* 上面两个问题
*
* 问题一 Error 的原因是:属性的类型声明 string 和 number 存在隐式转换问题,声明时,number 值类型必须是 string 值类型的子集。
*
* 问题二 Error 的原因是:若 string 和 number 互相转换成功,则需要同时满足两者的类型声明。
*
*/
函数类型接口
// let add1: (x: number, y: number) => number;
// interface Add {
// (x: number, y: number): number
// }
type Add = (x: number, y: number) => number
let add1: Add = (a, b) => a + b
// 混合接口
interface Lib {
(): void;
version: string;
doSomething(): void;
}
function getLib() {
let lib: Lib = (() => { }) as Lib;
lib.version = '1.0';
lib.doSomething = () => { }
return lib;
}
let lib1 = getLib();
lib1();
lib1.doSomething();
let lib2 = getLib();
函数定义
// 函数定义
function add11(x: number, y: number) {
return x + y;
}
let add22: (x: number, y: number) => number;
type add33 = (x: number, y: number) => number;
interface add44 {
(x: number, y: number): number
}
// add1(1, 2, 3)
function add55(x: number, y?: number) {
return y ? x + y : x;
}
add55(1)
function add66(x: number, y = 0, z: number, q = 1) {
return x + y + z + q;
}
console.log(add66(1, undefined, 3));
function add77(x: number, ...rest: number[]) {
return x + rest.reduce((pre, cur) => pre + cur)
}
console.log(add77(1, 2, 3, 4, 5))
function add88(...rest: number[]): number;
function add88(...rest: string[]): string;
function add88(...rest: any[]): any {
let first = rest[0];
if (typeof first === 'string') {
return rest.join('')
}
if (typeof first === 'number') {
return rest.reduce((pre, cur) => pre + cur)
}
}
console.log(add88(1, 2, 3));
console.log(add88('1', '2', '3'));
类的实现 类的继承 类的成员修饰符
class Dog {
constructor(name: string) {
this.name = name;
}
name: string;
run() {
}
private pri() { }
protected pro() { }
readonly legs: number = 4;
static food: string = 'bones';
}
console.log(Dog.prototype);
let dog = new Dog('wangwang');
console.log(dog);
// dog.pri()
// dog.pro()
console.log(Dog.food);
// console.log(dog.food);
class Husky extends Dog {
constructor(name: string, public color: string) {
super(name);
// this.color = color;
// this.pri();
this.pro();
}
// color: string;
}
let husky = new Husky('husky', 'red');
// console.log(husky.food);
console.log(Husky.food);
抽象类 多态
abstract class Animal {
eat() {
console.log('eat');
}
abstract sleep(): void;
}
// let animal = new Animal();
class Dog1 extends Animal {
constructor(name: string) {
super();
this.name = name;
}
name: string;
run() { }
sleep() {
console.log('dog sleep');
}
}
let dog1 = new Dog1('wangwang');
dog1.eat();
class Cat extends Animal {
sleep() {
console.log('Cat sleep')
}
}
let cat = new Cat();
let animals: Animal[] = [dog1, cat];
animals.forEach(i => {
i.sleep();
})
class WorkFlow {
step1() {
return this;
}
step2() {
return this;
}
}
new WorkFlow().step1().step2();
class Myflow extends WorkFlow {
next() {
return this;
}
}
new Myflow().next().step1().next().step2();
类与接口
/*
1、类实现接口必须实现接口中声明的所有的属性
2、接口只能约定类的公有成员
3、接口不能约束类的构造函数
*/
interface Human {
// new(name: string): void
name: string;
eat(): void;
}
class Asian implements Human {
constructor(name: string) {
this.name = name;
}
name: string
eat() { }
sleep() { }
}
/*
接口继承
*/
interface Man extends Human {
run(): void
}
interface Child {
cry(): void
}
interface Boy extends Man, Child { }
let boy: Boy = {
name: '',
run() { },
eat() { },
cry() { }
}
class Auto {
state = 1
private state2 = 0
}
interface AutoInterface extends Auto {
}
// class C implements AutoInterface {
// state = 1
// }
class C extends Auto implements AutoInterface {
state = 1
}
class Bus extends Auto implements AutoInterface {
}
泛型 泛型函数与泛型接口
泛型:不预先确定的数据类型,具体的类型在使用的时候才能确定。
// 方式一
function log<T>(value: T): T {
console.log(value);
return value;
}
log<string[]>(['a', 'b']);
log(['a', 'b'])
// 方式二
// type Log = <T>(value: T) => T
// let myLog: Log = log
// 方式三
// interface Log {
// <T>(value: T): T
// }
// interface Log<T> {
// (value: T): T
// }
interface Log<T = string> {
(value: T): T
}
let myLog: Log<number> = log
myLog(1)
let myLog2: Log = log
myLog2('1')
class Log<T> {
// static a: T = 1; // 静态成员不能使用泛型
run(value: T) {
console.log(value);
return value;
}
}
let log1 = new Log<number>();
log1.run(1)
let log2 = new Log()
log2.run('1')
interface Length {
length: number
}
function log<T extends Length>(value: T): T {
console.log(value, value.length);
return value;
}
log(1);
log('123')
log({ length: 1 })
泛型的好处:
- 函数和类可以轻松地支持多种类型,增强程序的扩展性
- 不必写多条函数重载,冗长的联合类型声明,增强代码可读性
- 灵活控制类型之间的约束
类型检查机制
TypeScript 编译器在做类型检查时,所秉承的一些原则,以及表现出的一些行为。
作用:辅助开发,提高开发效率。
类型推断
不需要指定变量的类型(函数的返回值类型),TypeScript 可以根据某些规则自动地为其推断出一个类型。基础类型推断、最佳通用类型推断、上下文类型推断
// 从右往左推断
let a = 1;
let b = [1, null]
let c = (x = 1) => x + 1
// 从左往右推断
window.onkeydown = (event) => {
// console.log(event.button);
}
interface Foo {
bar: number
}
let foo = {} as Foo;
foo.bar = 1;
let foo2: Foo = {
bar: 1
}
类型兼容性
export { }
/**
* 当一个类型 Y 可以被赋值给另一个类型 X 时,我们就可以说类型 X 兼容类型 Y
* X 兼容 Y:X(目标类型)= Y(源类型)
* 口诀:
* 结构之间兼容:成员少的兼容成员多的
* 函数之间兼容:参数多的兼容参数少的
*/
let s: string = 'a';
s = null; // 这里 s 会报错,需要在 tsconfig.json 中将 "strictNullChecks" 设置为 false
// 接口兼容性
interface X {
a: any;
b: any;
}
interface Y {
a: any;
b: any;
c: any;
}
let x: X = {
a: 1,
b: 2
}
let y: Y = {
a: 1,
b: 2,
c: 3
}
x = y;
// y = x; // 报错
// 这里 y 可以赋值给 x ,x 不能赋值给 y,因为 y 的属性包含了 x 的属性,所有能兼容 x
// 函数兼容性
type Handler = (a: number, b: number) => void
function hof(handler: Handler) {
return handler
}
// 1) 参数个数
let handler1 = (a: number) => { }
hof(handler1);
let handler2 = (a: number, b: number, c: number) => { }
// hof(handler2) // 这里 handler2 不兼容,原因是 handler2 参数数量大于 Handler 参数数量,就比如 Handler 只能处理两个参数,你传进来一个参数或者两个参数我都能处理,你传进来三个超出了我的能力范围,就只能报错了
// 2) 可选参数和剩余参数
let a = (p1: number, p2: number) => { }
let b = (p1?: number, p2?: number) => { }
let c = (...args: number[]) => { }
a = b;
a = c;
// b = c;
// b = a;
// b = c 和 b = a 这里会报错,报错的原因是:b 只能接收两个可选参数,a 有两个固定参数,所以 b 有可能处理不了,就报错了。 c 有扩展参数,参数个数不定,所以 b 也没能力处理。
interface Point3D {
x: number;
y: number;
z: number;
}
interface Point2D {
x: number;
y: number;
}
let p3d = (point: Point3D) => { };
let p2d = (point: Point2D) => { };
p3d = p2d;
// p2d = p3d; // 报错,报错原因:这里的 interface 跟上面的 interface 是反的,上面的是属性少的兼容属性多的,这里是属性多的兼容属性少的,这里可以把 interface 中的属性当作参数来看,参数少的兼容参数多的。 p3d 参数多,所以 p2d 不兼容 p3d 导致报错。如果要 p2d 兼容 p3d 也是可以做到的,需要在 tsconfig.json 中设置 "strictFunctionTypes" 为 false
// 3) 返回值类型
// ts 要求函数的返回值类型必须相同或者为其子类型。
let f = () => ({ name: 'Alice' });
let g = () => ({ name: 'Alice', location: 'Beijing' })
f = g
// g = f // 报错,原因: g 要求对象有两个属性,但是 f 返回值对象只有一个属性
// 函数重载
// 1) 函数重载的列表
function overload(a: number, b: number): number; // 函数定义 // 目标函数
function overload(a: string, b: string): string; // 函数定义 // 目标函数
// 2) 函数的具体实现
function overload(a: any, b: any): any { }; // 函数实现 // 源函数
// function overload(a: any): any { }; // 函数实现 // 源函数
// 程序在运行的时候,编译器会查找重载列表,然后使用第一个匹配的定义来执行相应函数。所以,在重载列表中,目标函数的参数要多于源函数的参数,而且返回值类型也要符合相应要求。
// 枚举类型兼容性
enum Fruit { Apple, Banana }
enum Color { Red, Yellow }
// 枚举类型和数值类型是互相兼容的
let fruit: Fruit.Apple = 3;
let no: number = Fruit.Apple
// 枚举之间是完全不兼容的
// let color: Color.Red = Fruit.Apple // 报错
// 类兼容性
// 类的兼容性和接口比较相似,只比较结构。
// 在比较两个类是否兼容的时候,静态成员和构造函数是不参与比较的。
// 如果两个类具有相同的实例成员,那么他们的实例就可以相互兼容。
class A {
constructor(p: number, q: number) { }
id: number = 1
// private name: string = ''
}
class B {
static s = 1
constructor(p: number) { }
id: number = 2
// private name: string = ''
}
let aa = new A(1, 2);
let bb = new B(1);
aa = bb;
bb = aa;
// 如果类中含有私有成员,只有父类和子类可以相互兼容。
class C extends A { }
let cc = new C(1, 2);
aa = cc;
cc = aa;
// 泛型兼容性
interface Empty<T> {
}
let obj1: Empty<number> = {};
let obj2: Empty<string> = {};
// obj1 = obj2; // 泛型接口中没有任何成员的时候,这两个变量是互相兼容的。只有类型参数 T 在接口中使用的时候,才会影响泛型的兼容性。
// 泛型函数兼容性
let log1 = <T>(x: T): T => {
console.log('x');
return x;
}
let log2 = <U>(y: U): U => {
console.log('y');
return y;
}
log1 = log2;
log2 = log1;
// 如果两个泛型函数的定义相同,但是没有指定类型参数。那么他们之间也是可以相互兼容的。
类型保护
类型保护
TypeScript 能够在特定的区块中保证变量属于某种确定的类型.
可以在此区块中放心地引用此类型的属性,或者调用此类型的方法。
/**
* 类型保护
* TypeScript 能够在特定的区块中保证变量属于某种确定的类型。
* 可以在此区块中放心地引用此类型的属性,或者调用此类型的方法。
*/
export { }
enum Type { Strong, Week }
class Java {
helloJava() {
console.log('Hello Java')
}
java: any
}
class JavaScript {
helloJavaScript() {
console.log('Hello JavaScript')
}
javascript: any
}
// 方案五 通过创建类型保护函数 来判断对象类型
function isJava(lang: Java | JavaScript): lang is Java {
return (lang as Java).helloJava !== undefined;
}
// function getLanguage(type: Type, x: string | number) { // 结合方案四使用
function getLanguage(type: Type) {
let lang = type === Type.Strong ? new Java() : new JavaScript();
// if (lang.helloJava) { // 报错
// lang.helloJava()
// } else {
// lang.helloJavaScript()
// }
// // 方案一 as
// if ((lang as Java).helloJava) {
// (lang as Java).helloJava()
// } else {
// (lang as JavaScript).helloJavaScript()
// }
// // 方案二 instanceof
// if (lang instanceof Java) {
// lang.helloJava()
// } else {
// lang.helloJavaScript()
// }
// // 方案三 in // 用 in 来判断属性是不是属于某个对象
// if ('java' in lang) {
// lang.helloJava();
// } else {
// lang.helloJavaScript()
// }
// // 方案四 typeof 只能判断基础类型
// if (typeof x === 'string') {
// x.toString()
// } else {
// x.toFixed(2)
// }
// 方案五
if (isJava(lang)) { // 因为上面 isJava 返回类型是 lang is Java,所以作用域内可以使用 helloJava
lang.helloJava()
} else {
lang.helloJavaScript()
}
return lang
}
getLanguage(Type.Strong);
高级类型
所谓高级类型就是 TypeScript 为了保障语言的灵活性所引用的一些语言特性,这些特性将有助于我们应对复杂多变的开发场景。
交叉联合与联合类型
交叉类型
交叉类型是指将多个类型合并为一个类型,新的类型将具有所有类型的特性,所以交叉类型特别适合对象混用的场景。
交叉类型 '&' 符号连接,交叉类型听起来像是取类型的交集,但实际上是取类型的并集。
// 交叉类型
// 交叉类型是指将多个类型合并为一个类型,新的类型将具有所有类型的特性,所以交叉类型特别适合对象混用的场景。
// 交叉类型 '&' 符号连接,交叉类型听起来像是取类型的交集,但实际上是取类型的并集。
interface DogInterface {
run(): void
}
interface CatInterface {
jump(): void
}
let pet: DogInterface & CatInterface = {
run() { },
jump() { }
}
联合类型
所谓联合类型是指,声明的类型并不确定,可以为多个类型中的一个。
联合类型符号 '|',联合类型给人的感觉是可以取所有成员的并集,但真实情况是只能取所有成员的交集。
// 联合类型
// 所谓联合类型是指,声明的类型并不确定,可以为多个类型中的一个。
// 联合类型符号 '|'
let a: number | string = 'a'
// 顺便介绍 字面量类型,不仅限定变量的类型,还限定变量取值在一定范围内
// 字符串字面量联合类型
let b: 'a' | 'b' | 'c' = 'b'
// 数值字面量联合类型
let c: 1 | 2 | 3 = 2
// 对象的联合类型
class Dog implements DogInterface {
run() { }
eat() { }
}
class Cat implements CatInterface {
jump() { }
eat() { }
}
enum Master { Boy, Girl }
function getPet(master: Master) {
let pet = master === Master.Boy ? new Dog() : new Cat();
pet.eat(); // 在类型没有确定的情况下,只能访问所有类型的共有成员。联合类型给人的感觉是可以取所有成员的并集,但真实情况是只能取所有成员的交集。
// pet.run();
return pet;
}
// 可区分的联合类型,实际是结合了联合类型和字面量类型的一种类型保护方法。
// 他的核心思想是一个类型如果是多个类型的联合类型,并且每个类型之间有一个类型的公共属性,那么我们可以凭借这个公共属性创建类型保护区块。
interface Square {
kind: 'square';
size: number;
}
interface Rectangle {
kind: 'rectangle';
width: number;
height: number;
}
interface Circle {
kind: 'circle';
r: number;
}
type Shape = Square | Rectangle | Circle; // Circle 为新加
function area(s: Shape) {
switch (s.kind) {
case 'square':
return s.size * s.size;
case 'rectangle':
return s.height * s.width;
}
}
console.log(area({ kind: 'circle', r: 1 })); // Shape 类型扩展 Circle 后,switch case 遗漏,TypeScript 没有报错,area 返回值为 undefined , 如何用 TypeScript 约束这种模式,给出相应的错误提示呢(目的是有 case 遗漏的时候 TypeScript 能给出错误提示)?
// // 方案一 使用函数返回类型约束
// function area1(s: Shape): number { // 这里报错是对的,TypeScript 检查到 case 有遗漏。Shape 添加 Cirice 类型后,number 这里报错:Function lacks ending return statement and return type does not include 'undefined'.
// switch (s.kind) {
// case 'square':
// return s.size * s.size;
// case 'rectangle':
// return s.height * s.width;
// }
// }
// // 方案二 利用 never 类型
// function area2(s: Shape) {
// switch (s.kind) {
// case 'square':
// return s.size * s.size;
// case 'rectangle':
// return s.height * s.width;
// // case 'circle':
// // return Math.PI * s.r ** 2;
// default:
// return ((e: never) => { throw new Error(e) })(s); // 这里报错是正确的,说明 TypeScript 检查到 case 有遗漏。这里函数的作用是检查 s 是不是 never 类型,如果 s 是 never 类型,说明前面分支都被覆盖了,这个分支永远不会走到;如果 s 不是 never 类型,说明前面分支有遗漏。Argument of type 'Circle' is not assignable to parameter of type 'never'.
// }
// }
索引类型
从对象中获取属性的值,然后建立一个集合。
export { }
// 索引类型
// 从对象中获取属性的值,然后建立一个集合。
let obj = {
a: 1,
b: 2,
c: 3
}
function getValues(obj: any, keys: string[]) {
return keys.map(key => obj[key])
}
console.log(getValues(obj, ['a', 'b'])); // [1, 2]
console.log(getValues(obj, ['e', 'f'])); // [undefined, undefined] // 返回 undefined ,但是 TypeScript 并没有报错。TypeScript 如何对这种模式进行约束呢,这里就用到了索引类型。
// 了解索引类型前,先了解一些概念
// 1、keyof T ,含义是表示类型 T 的所有公共属性的字面量的联合类型。例子:
// keyof T
interface Obj {
a: number,
b: string
}
let key: keyof Obj; // let key: 'a' | 'b'; key 的类型变成了 'a' 和 'b' 的字面量联合类型。key = 'a'; key = 'b';
// 2、T[K] 含义表示对象 T 的属性 K 所代表的类型。 例子:
// T[K]
let value: Obj['a']; // let value: number; value 的类型变成了 Obj['a'] 所指定的类型。 value = 1; value = 2;
// 3、泛型约束
// T extends U 表示泛型变量可以通过继承某个类型获得某些属性
// 了解上面三个概念后,我们来改造 getValues 函数。
function getValues1<T, K extends keyof T>(obj: T, keys: K[]): T[K][] {
return keys.map(key => obj[key])
}
console.log(getValues1(obj, ['a', 'b'])); // [1, 2]
console.log(getValues1(obj, ['e', 'f'])); // 这里 e f 会报错,这是正确的,指定不在 obj 里面的属性的话,TypeScript 会报错。
/*
解析
第一步 泛型 T 约束 obj 参数类型。
function getValues1<T>(obj: T, keys) {
return keys.map(key => obj[key])
}
第二步 泛型 K 约束 keys 参数类型。
function getValues1<T, K>(obj: T, keys: K[]) {
return keys.map(key => obj[key])
}
第三步 K extends keyof T 进一步约束 keys 参数类型,keyof T 在这里相当于 'a' | 'b' | 'c',K extends keyof T 说明 K 上面只存在 a b c 属性。
function getValues1<T, K extends keyof T>(obj: T, keys: K[]) {
return keys.map(key => obj[key])
}
第四步 T[k][] 约束了函数返回类型,是个 T[K] 类型数组,T[K] 在这里相当于 number,因为 T 是 obj, T['a']、T['b']、T['c'] 都是 number,所以 T[K][] 在这里是 number 类型数组。如果 T['b'] 是个 string 值的话(比如:obj.b = '2'),那这里 T[K][] 代表的是 number string 交叉类型数组。
function getValues1<T, K extends keyof T>(obj: T, keys: K[]): T[K][] {
return keys.map(key => obj[key])
}
console.log(getValues1(obj, ['a', 'b'])); // [1, 2]
console.log(getValues1(obj, ['e', 'f']));
这里 e f 会报错,这是正确的,指定不在 obj 里面的属性的话,TypeScript 会报错。
由此可见索引类型可以实现对对象属性的查询和访问,然后在配合泛型约束是我们能够建立对象、对象属性、对象属性值之间的约束关系。
*/
映射类型
通过映射类型,我们可以从一个旧的类型生成一个新的类型。
比如把一个类型中的所有属性变为只读。
interface Obj {
a: string;
b: number;
c: boolean;
}
// 同态类型,只会作用于 Obj 属性,不会引入新的属性。
// 将所有属性变为只读
type ReadonlyObj = Readonly<Obj>;
// type Readonly<T> = {
// readonly [P in keyof T]: T[P];
// };
// type ReadonlyObj = {
// readonly a: string;
// readonly b: number;
// readonly c: boolean;
// }
// 将所有属性变为可选
type PartialObj = Partial<Obj>;
// type Partial<T> = {
// [P in keyof T]?: T[P];
// };
// type PartialObj = {
// a?: string | undefined;
// b?: number | undefined;
// c?: boolean | undefined;
// }
// 选择部分属性生成新的类型
type PickObj = Pick<Obj, 'a' | 'b'>
// type Pick<T, K extends keyof T> = {
// [P in K]: T[P];
// };
// type PickObj = {
// a: string;
// b: number;
// }
// 非同态类型,会创建新的属性,第一个参数是新的属性,第二个参数是一个已知的类型
type RecordObj = Record<'x' | 'y', Obj>
// type Record<K extends keyof any, T> = {
// [P in K]: T;
// };
// type RecordObj = {
// x: Obj;
// y: Obj;
// }
条件类型
条件类型是一种由条件表达式所决定的类型。
T extends U ? X : Y ,意思是如果类型 T 可以被赋值为类型 U,那么结果类型就是 X 类型,否则是 Y 类型。
条件类型是类型具有了不唯一性,同样增加了语言的灵活性。
// 条件类型,条件类型嵌套
// T extends U ? X : Y
type TypeName<T> =
T extends string ? 'string' :
T extends number ? 'number' :
T extends boolean ? 'boolean' :
T extends undefined ? 'undefined' :
T extends Function ? 'function' :
'object';
type T1 = TypeName<string>; // T1: string
type T2 = TypeName<string[]>; // T2: object
// 分布式条件类型
// (A | B) extends U ? X : Y
// 拆解后:(A extends U ? X : Y) | (B extends U ? X : Y)
type T3 = TypeName<string | string[]> // T3: "string" | "object"
type Diff<T, U> = T extends U ? never : T;
type T4 = Diff<'a' | 'b' | 'c', 'a' | 'e'>
// 拆解
// Diff<'a', 'a' | 'e'> | Diff<'b', 'a' | 'e'> | Diff<'c', 'a' | 'e'>
// 结果: never | 'b' | 'c' // never 表示永远不存在的值的类型
// 结果:'b' | 'c'
type NotNull<T> = Diff<T, undefined | null>; // 过滤 undefined 和 null
type T5 = NotNull<string | number | undefined | null>; // T5: string | number
// Diff 和 NotNull 官方已经帮我们实现了,官方对应的是 Exclude<T, U> 和 NonNullable<T>,实际使用的时候直接调用就行。
// 官方还预定了一些条件类型,Extract<T, U> 与 Exclude 相反,Exclude 是过滤掉可以赋值给 U 的类型,Extract 是抽取出可以赋值给 U 的类型。
type T6 = Extract<'a' | 'b' | 'c', 'a' | 'e'>; // T6: 'a'
// ReturnType<T> 可以获取一个函数返回值的类型
type T7 = ReturnType<() => string>
// type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any; // infer 待推断、延迟推断,需要根据实际情况来确定
ES6 与 CommonJS 的模块系统
src/es6/a.ts
// 单独导出
export let a = 1;
// 批量导出
let b = 2;
let c = 3;
export { b, c }
// 导出接口
export interface P {
x: number;
y: number;
}
// 导出函数
export function f() { }
// 导出时起别名
function g() { }
export { g as G }
// 默认导出,无需函数名
export default function () {
console.log("i'm default")
}
// 引入外部模块,重新导出
export { str as hello } from './b';
src/es6/b.ts
let str = 1
export {
str
}
src/es6/c.ts
import { a, b, c } from './a'; // 批量导入
import { P } from './a'; // 导入接口
import { f as F } from './a'; // 导入时起别名
import * as All from './a'; // 导入模块中所有成员,绑定在 all 上
import myFunction from './a'; // 不加 {},导入默认
console.log(a, b, c);
let p: P = {
x: 1,
y: 1
}
console.log(All);
myFunction();
src/es6/d.ts
export = function () { // TypeScript 特殊语法,兼容语法。 这个语法会被编译成 module.exports,相当于 commonJS 中的顶级导出了。同时意味着这个文件中不能有其他导出了,如果想导出其他数据,需要合并到一个对象中进行导出。
console.log("i'm default")
}
// export let a = 1
src/node/a.node.ts
export { }
let a = {
x: 1,
y: 1
}
// 整体导出
module.exports = a;
src/node/b.node.ts
// exports === module.exports
// 导出多个变量
// module.exports = {} 会覆盖 exports.c exports.d 的导出
exports.c = 3
exports.d = 4
src/node/c.node.ts
let c1 = require('./a.node')
let c2 = require('./b.node')
let c3 = require('../es6/a')
console.log(c1)
console.log(c2)
// es6 与 commonjs 混用,不建议混用,很容易发生错误
// c3() // 报错
console.log(c3)
c3.default() // 不报错,需要调用 default 方法,反直觉,容易发生错误
// 如何处理两个模块间的不兼容性问题呢?两个方案:一是两个模块系统不要混用;二是使用 TypeScript 的兼容性语法。详见 d.ts
import c4 = require('../es6/d'); // TypeScript 特殊语法,兼容语法。 如果 "esModuleInterop": true, 也可以直接使用 es6 语法导入:import c4 from '../es6/d';
c4();
// c3 和 c4 对比,c3 es6 和 commonJS 混用时,需要调用 default 方法才行。c4 es6 和 commonJS 混用时,使用了 TypeScript 特殊兼容语法,可以直接调用 c4(),不需要调用 default 函数。
使用命名空间
src/a.ts
// 命名空间可以有效的避免全局污染。使用全局类库的时候,命名空间仍然是一个比较好的解决方案。
namespace Shape { // 在命名空间内可以定义任意多的变量,这些变量只能在 Shape 命名空间下可见。
const pi = Math.PI;
export function cricle(r: number) { // 如果想让成员对全局可见的话,可以使用 export 关键字导出。
return pi * r ** 2
}
// export function square(x: number) { // 在命名空间中,导出的成员是不可以重复定义的,接口中是可以重复定义的。
// return x * x
// }
}
// 命名空间也可以进行拆分,可见 b.ts 文件。也存在 Shape 命名空间,他们共享一个命名空间。
src/b.ts
/// <reference path='a.ts' />
namespace Shape {
export function square(x: number) {
return x * x
}
}
// 命名空间的调用方法。
Shape.cricle(1); // 报错,cricle 是 a.ts 中的,这里需要对 a.ts 进行引用。引用语法:/// <reference path='a.ts' />
Shape.square(1);
// 明确一个原则: 命名空间和模块不要混用,不要在一个模块中使用命名空间。命名空间最好在一个全局的环境中使用。
// 命名空间成员别名
import cricle = Shape.cricle; // 这里的 import 和模块的 import 没有任何关系。
cricle(1);
// 在 TypeScript 的早期版本中,命名空间也叫内部模块,本质上就是一个闭包,可以用于隔离作用域。
// 随着 ES6 模块的引入,内部模块这个名称现在已经不在叫了,TypeScript 保留的命名空间,更多是考虑对全局变量时代的一种兼容。
// 现在在一个完全的模块化系统中,我们可以不必使用命名空间。
理解声明合并
所谓声明合并就是程序会把多个地方的相同声明合并为一个声明。 这样做有一个好处,就是把程序中散落在各个地方的重名声明合并在一起, 比如你在程序中在多个地方定义了同样名字的接口,那么你在使用接口的时候就会对这个多出的定义具有感知,通过声明合并就会避免对接口成员的遗漏。
src/merge.ts
export { }
// 声明合并
// 所谓声明合并就是程序会把多个地方的相同声明合并为一个声明。
// 这样做有一个好处,就是把程序中散落在各个地方的重名声明合并在一起,
// 比如你在程序中在多个地方定义了同样名字的接口,那么你在使用接口的时候就会对这个多出的定义具有感知,通过声明合并就会避免对接口成员的遗漏。
// 接口的声明合并
interface A {
x: number
// y: string // 报错,对于接口中的非函数成员,要保证唯一性,如果不唯一的话类型必须相同。 y: number
foo(bar: number): number // 5 // 对于函数成员,每一个函数都会被声明为一个函数重载
foo(bar: 'a'): number // 2
}
interface A {
y: number
foo(bar: string): string // 3
foo(bar: number[]): number[] // 4
foo(bar: 'b'): number // 1
}
let a: A = {
x: 1,
y: 1,
foo(bar: any) { // 函数重载的时候需要注意函数声明的顺序,因为编译器会按顺序进行匹配,顺序原则:接口内部按照书写顺序,接口之间的话后面的接口会排在前面。也有一个例外,如果函数的参数是字符串字面量,那么这个声明就会被提升到函数的最顶端。
return bar
}
}
// 命名空间和函数的合并
function Lib() { }
namespace Lib {
export let version = '1.0' // export 相当于给 Lib 函数增加了一个属性,JavaScript 中给函数增加属性是很常见的模式。
}
console.log(Lib)
// 命名空间和类的合并
class C { }
namespace C {
export let state = 1; // 相当于给类添加了一个静态类型属性
}
// 命名空间和枚举进行合并
enum Color {
Red, Yellow, Blue
}
namespace Color {
export function mix() { } // 相当于给枚举对象添加了一个静态方法
}
// 命名空间与函数和类进行合并时,必须要放在后面,否则 TypeScript 会报错。与枚举类型合并不受限制。
// 在我们的程序中,如果有多处同名的声明,这并不是一个好的模式,最好是把他们封装在一个模块内,TypeScript 具有这种特性是为了照顾旧的开发模式,这使我们在工程中如果引入 TypeScript 可以和老的代码共存。
如何编写声明文件
如何在 TypeScript 中引入外部类库,如何为他们编写声明文件。
类库一般分为三类,全局类库,模块类库,UMD 类库。
src/libs/index.ts
export { }
// 如何编写声明文件
// 如何在 TypeScript 中引入外部类库,如何为他们编写声明文件。
// 类库一般分为三类,全局类库,模块类库,UMD 类库。
// JQuery 是 UMD 类库。
// 如果社区 @types 有生命文件,可以直接安装使用。有时候类库的声明文件包含在源码中,不需要安装。
// npm install -S jquery
// npm install -D @types/jquery
import $ from 'jquery';
$('.app').css('color', 'red');
// 如何编写声明文件
// 全局类库
globalLib({ x: 1 }) // 报错无法找到声明文件,创建 global-lib.d.ts 后恢复。
globalLib.doSomething();
// 模块类库
import moduleLib from './module-lib'; // 报错,无法找到声明文件。添加 module-lib.d.ts 后恢复。
moduleLib.doSomething();
// UMD 库
import umdLib from './umd-lib'; // 报错,无法找到声明文件。添加 umd-lib.d.ts 后恢复。
umdLib.doSomething(); // umd 库可以和 globalLib 库一样使用全局引用,TypeScript 会报错,不建议使用全局引用,设置 "allowUmdGlobalAccess": true, 可以关闭报错。
// 插件
// 模块插件
// npm install -S moment
import m from 'moment';
m.myFunction = () => { // 报错,类型“typeof moment”上不存在属性“myFunction”。ts(2339);我们可以使用 declare module 处理这个问题,这样我们可以给一个外部类库增加自定义方法。
}
declare module 'moment' {
export function myFunction(): void
}
// 全局插件,这会对全局命名空间造成污染,一般不建议这么做。
declare global {
namespace globalLib {
function doAnything(): void
}
}
globalLib.doAnything = () => { }
// 声明文件的依赖
// 查看 JQuery 声明文件中的依赖。
/**
/// <reference types="sizzle" /> // 模块依赖,在 @types 目录下
/// <reference path="JQueryStatic.d.ts" /> // 路径依赖,同级路径下
/// <reference path="JQuery.d.ts" />
/// <reference path="misc.d.ts" />
/// <reference path="legacy.d.ts" />
export = jQuery;
*/
src/libs/global-lib.js
// 全局库
function globalLib(options) {
console.log(options);
}
globalLib.version = '1.0.0';
globalLib.doSomthing = function () {
console.log('globalLib do something');
}
src/libs/global-lib.d.ts
// 声明文件
// declare 关键字,可以为外部变量提供一些声明。
declare function globalLib(options: globalLib.Options): void;
declare namespace globalLib {
const version: string;
function doSomething(): void;
interface Options { // 这个接口可以放在全局,放在全局就对外暴漏了,如果不想对外暴漏就放在命名空间内。
[key: string]: any
}
}
src/libs/module-lib.js
// 模块类库
const version = '1.0.0';
function doSomething() {
console.log('moduleLib do something');
}
function moduleLib(options) {
console.log(options);
}
moduleLib.version = version;
moduleLib.doSomething = doSomething;
module.exports = moduleLib;
src/libs/module-lib.d.ts
// 模块声明文件
declare function moduleLib(options: Options): void
interface Options { // 这里放在外面不会向外暴漏,原因是因为这个声明文件是个模块,所以这个接口就不会向外暴漏。
[key: string]: any
}
declare namespace moduleLib {
const version: string
function doSomething(): void
}
export = moduleLib; // TypeScript 特殊语法,兼容语法。 这个语法会被编译成 module.exports,相当于 commonJS 中的顶级导出了。同时意味着这个文件中不能有其他导出了,如果想导出其他数据,需要合并到一个对象中进行导出。
src/libs/umd-lib.js
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define(factory);
} else if (typeof module === 'object' && module.exports) {
module.exports = factory();
} else {
root.umdLib = factory();
}
}(this, function () {
return {
version: '1.0.0',
doSomething() {
console.log('umdLib do something')
}
}
}));
src/libs/umd-lib.d.ts
declare namespace umdLib {
const version: string
function doSomething(): void
}
// 专为 umd 库设置的语句。如果我们编写 umd 库,这个语句是不可缺少的。
export as namespace umdLib
export = umdLib