TypeScript
1、Typescript 基础语法
1.1 类型定义
类型定义的语法很简单
const name: string = 'heyi'
const age: number = 18
enum Color{//枚举
Red,
Green,
Blue
}
const a:Color = Color.Red
//any 任何类型
//void 空值,用在函数返回值或者undefined,null
function test():void{
}
let unuseable:void =undefined
//null
const test1:null =null//没啥意义
//never 永远不存在的,一般抛出异常到不了返回
function error(message:string):never {
throw new Error(message)
}
1.2 类型联合与交叉
类型联合用于指定一个值的类型可以是多个,我们可以把一个业务化的数据变量指定成既可以是 string 又可以是 number。
let myFavoriteNumber: string | number;
类型交叉是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。
type NameProtocal = {name: string}
type PersonLikeProtocal = {age: number; say: () => void}
type Student = NameProtocal & PersonLikeProtocal
1.3 类型断言
let value:number =123
let valueLength:number =(value as unknown as string).length//把value 转成string
let valueLength:number =(<unknown><string>value).length//把value 转成string
2、Typescript 泛型与类型体操
2.1 泛型的概念和使用
2.1.1 泛型的概念
定义:泛型(Generics)
是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。泛型,可以用汉语言中的 “ 代词 ” 类比。
为什么要使用泛型?
需求:实现一个函数,传入的函数参数是什么类型的,返回值的类型也要跟函数参数的类型相同,并且函数只能接收一个参数,你会怎么做?
- 首先,我们先创建一个identity函数,将他的类型和返回值都固定为number类型,但是,该函数只能接收number类型的数据,如果我调用函数的时候传入字符串或者布尔值类型的值,此时就会报错
const identity: (value: number) => number = (value) => value
console.log(identity(10)); // 输出:10,类型是number
console.log(identity('Echo')); // 报错:类型“string”的参数不能赋值给类型“number”的参数
- 为了让函数能够接收任意类型,可以将参数类型改为any,但是,这样就失去了 TS
的类型保护,会丢失一些信息:传入的类型与返回的类型应该是相同的。 我们传入一个数字,任何类型的值都有可能被返回。
const identity: (value: any) => any = (value) => value
console.log(identity('Echo')); // 输出:Echo
console.log(identity(26)); // 输出:26
console.log(identity(true)); // 输出:true
- 因此,我们需要一种方法使返回值的类型与传入参数的类型是相同的。 这里,我们使用了类型变量,它是一种特殊的变量,只用于表示类型而不是值。
function identity<T>(value: T): T {
return value;
}
console.log(identity<string>('Echo')); // 输出:Echo
console.log(identity<number>(26)); // 输出:26
console.log(identity<boolean>(true)); // 输出:true
上述代码中,给identity
添加了类型变量T
。 T会捕获用户传入的类型(如:number、string)。 之后再次使用了T当做返回值类型。现在我们可以知道参数类型与返回值类型是相同的了。 这允许我们跟踪函数里使用的类型的信息。
我们把这个版本的identity函数叫做泛型,因为它可以适用于多个类型。
2.1.2 泛型的使用
- 首先,先定义一个函数
function identity<T>(value: T): T {
return value;
}
//const func:<T>(value: T)=> T = identity
- 第一种使用方法:
传入所有的参数,包含类型参数
let output = identity<string>("myString");
明确的指定了T是string类型,并做为一个参数传给函数,使用了<>括起来而不是()。
- 第二种使用方法:利用类型推论 – 即
编译器会根据传入的参数自动地帮助我们确定T的类型
let output = identity("myString");
没必要使用尖括号(<>)来明确地传入类型;编译器可以查看myString的值,然后把T设置为它的类型。 类型推论帮助我们保持代码精简和高可读性。
2.1.3 泛型类
- 泛型类(Generic Class) 是指在定义类时使用泛型类型参数的类。它允许我们在类的属性、方法、构造函数以及实例化时使用泛型。
- 泛型类使用(<>)括起泛型类型,跟在类名后面。
class Container<T> {
private items: T[] = [];
addItem(item: T) {
this.items.push(item);
}
getItem(index: number): T {
return this.items[index];
}
getItems(): T[] {
return this.items;
}
}
const container = new Container<number>(); // 实例化一个泛型类,指定类型参数为 number
container.addItem(1);
container.addItem(2);
container.addItem('e');//报错
console.log(container.getItems()); // [1, 2]
类有两部分:静态部分和实例部分。 泛型类指的是实例部分的类型,所以 类的静态属性不能使用这个泛型类型
。
2.1.4 泛型约束
在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法
function getLength<T>(arg: T): T {
console.log(arg.length);// index.ts(2,19): error TS2339: Property 'length' does not exist on type 'T'.
return arg;
}
上例中,泛型 T 不一定包含属性 length,所以编译的时候报错了。
- 我们通过 extends 关键字进行类型约束,只允许这个函数传入那些包含 length 属性的变量。这就是泛型约束:
interface ILength {
length: number;
}
function getLength<T extends ILength>(value: T): T {
console.log(value.length);
return value;
}
getLength([1, 2, 3]) // 正确,因为数组有 length 属性
getLength('Echo') // // 正确,因为字符串有 length 属性
getLength({ length: 10, name: 'Echo' }) // 正确,因为传入的参数有 length 舒心
getLength(10) // 报错:类型“number”不能赋值给类型“ILength”的参数,因为数字不具有 length 属性
- 多个类型参数之间也可以互相约束
function getProperty(obj,key){
return obj[key]
}
这种情况,我们需要约束,key是obj的键值
function getProperty<T,K extends keyof T>(obj:T,key:K){ //keyof表示,K继承于T的键值
return obj[key]
}
2.2 interface 接口
我们在JavaScript中定义一个函数,用于获取一个用户的姓名和年龄的字符串:
const getUserInfo = function(user) {
return `name: ${user.name}, age: ${user.age}`
}
正确的调用方法应该是下面的方式:
getUserInfo({name: "coderwhy", age: 18})
当项目比较大,或者多人开发时,会出现错误的调用方法:需要加上类型限制
const getUserInfo = (user: {name: string, age: number}): string => {
return `name: ${user.name} age: ${user.age}`;
};
这样确实可以防止出现错误的调用,但是我们在定义函数的时候,参数的类型和函数的类型都是非常长的,代码非常不便于阅读。
于是 参数类型使用接口
定义
我们先定义一个IUser接口:
// 先定义一个接口
interface IUser {
name: string;
age: number;
}
接下来我们看一下函数如何来写:
const getUserInfo = (user: IUser): string => {
return `name: ${user.name}, age: ${user.age}`;
};
// 正确的调用
getUserInfo({name: "coderwhy", age: 18});
// 错误的调用,其他也是一样
getUserInfo();
继续优化,对函数返回值也做出限制
interface IUser {
name: string;
age: number;
}
interface IUserInfoFunc {
(user: IUser): string;
}
const getUserInfo: IUserInfoFunc = (user) => {
return `name: ${user.name}, age: ${user.age}`;
};
// 正确的调用
getUserInfo({name: "coderwhy", age: 18});
// 错误的调用
getUserInfo();
定义接口中不仅仅可以有属性,也可以有方法:属性可以可选,可以只读
interface Person {
readonly name: string;//只读
age?: number;//可选
run(): void;
eat(): void;
study?(): void;//可选
}
2.3 extends 的重要性
2.2.1 接口继承
extends用来做继承功能,相信大家都不陌生,ES6的Class语法也是用它来做类的继承用。在TS中用法也类似,来看示例
T1 {
name: string
}
interface T2 {
sex: number
}
// 多重继承,逗号隔开
interface T3 extends T1,T2 {
age: number
}
// 合法
const t3: T3 = {
name: 'xiaoming',
sex: 1,
age: 18
}
2.2.2 条件判断
extends用来条件判断的语法和JS的三元表达是很相似,如果问号前面的判断为真,则将第一个类型string赋值给A,否则将第二个类型number赋值给A。
那么,接下来的问题就是,extends判断条件真假的逻辑是什么?
很简单,如果extends前面的类型能够赋值给extends后面的类型,那么表达式判断为真,否则为假。
比如
// 示例2
interface A1 {
name: string
}
interface A2 {
name: string
age: number
}
// A的类型为string
type A = A2 extends A1 ? string : number
const a: A = 'this is string'
A1,A2两个接口,满足A2的接口一定可以满足A1,所以条件为真,A的类型取string(即A2可以从A1继承而来)
上述A2可以改为
interface A2 extends A1 {
age: number
}
2.4 Infer 推断
infer 表示在 extends 条件语句中待推断的类型变量。
type ParamType<T> = T extends (arg: infer P) => any ? P : T;
这个条件语句 T extends (arg: infer P) => any ? P : T
中,infer P
表示待推断的函数参数。
整句表示为:如果 T
能赋值给 (arg: infer P) => any
,则结果是 (arg: infer P) => any
类型中的参数 P
,否则返回为 T
。
interface User {
name: string;
age: number;
}
type Func = (user: User) => void;
type Param = ParamType<Func>; // Param = User
//Func是T 他的函数参数 User 是P,
//Func=(user: User) => void 也就是 T符合(arg: infer P) => any
//所以返回P ,也就是type ParamType<Func> 返回P,也就是 Param = User
type AA = ParamType<string>; // string
//string 是T,不符合 (arg: infer P) => any ,所以返回T,也就是string
所以这个案例的作用是:假如我们定义了一个函数,可以获取参数的类型,同时非函数的话返回本身
3、Typescript 综合
interface 和type的区别是什么?
interface IProps{
a:number
}
type Props={
a:number
}
type(类型别名)、interface(接口)
相同点:
1、都可以用来定义对象和函数
type Position = {
x: number;
y: number;
};
type SetPosition = (x: number, y: number) => void;
interface Position {
x: number;
y: number;
}
interface SetPosition {
(x: number, y: number): void;
}
2、都可以实现继承
对于 interface 来说,继承是通过 extends 实现的;而 type 是通过 & 来实现的,也可以叫做交叉类型。
// type 继承 type
type Person {
name: string
}
type Student = Person & { stuId: number }
// type 继承 interface
interface Person {
name: string
}
type Student = Person & { stuId: number }
// interface 继承 interface
interface Person {
name: string
}
interface Student extends Person {
stuId: number
}
// interface 继承 type
type Person {
name: string
}
interface Student extends Person {
stuId: number
}
不同点:
1、type 可以做到而 interface 不能做到
- type 可以声明基本类型。
type userName = string;
- type 可以声明联合类型。
type userMsg = string | number;
- type 可以声明元组类型。
type Data = [number, string];
- type 可以通过 typeof 操作符来声明
type myType = typeof someObj;
2、interface 可以做到而 type 不能做到
- interface 可以声明合并。
interface test {
name: string
}
interface test {
age: number
}
/*
test实际为 {
name: string
age: number
}
*/
如果是 type 的话,就会报重复定义的警告,因此是无法实现声明合并的。
总结:
- type支持定义非对象类型,如联合类型、交叉类型、映射类型等,而interface只能定义对象类型。
- interface支持继承和合并,可以通过extends关键字实现接口的扩展,也可以多次声明同一个接口来合并成员,而type不支持继承和合并(type不能声明合并,不过可以通过通过 & 来实现继承,也叫交叉类型)
- type是类型别名,不会创建新的类型,只是给已有类型命名,而interface是定义了一个接口类型,可以在运行时获取类型信息。
- type和interface都可以用于定义对象的类型和形状,但interface更适合面向对象编程,而type更适合组合不同类型。
使用建议
1、官方推荐使用 interface,其他无法满足需求的情况下用 type。但是因为联合类型和交叉类型是比较常用的,所以避免不了大量使用 type 的场景,一些复杂类型也需要通过组装后形成类型别名来使用。
2、如果想保持代码统一,还是可选择使用 type。通过上面的对比,type 其实可涵盖 interface 的大部分场景。
3、对于 React 组件中 props 及 state,推荐使用 type,这样能够保证使用组件的地方不能随意在上面添加属性。如果有自定义需求,可通过 HOC(高阶组件)二次封装。
4、编写三方库时使推荐使用 interface,其更加灵活自动的类型合并可应对未知的复杂使用场景。