TS 是什么
TS:js 的超集,js 有的 ts 都有。TypeScript 可以理解为 Type + JavaScript,在 js 的基础上添加类型支持,能运行 js 的地方也能运行 ts。
为什么加类型支持
添加类型支持之后可以更早的发现错误,TS 是静态语言,JS 是动态语言。静态语言是在编译是做类型检查,而动态语言是在执行时做类型检查,所以 TS 在编译时就可以发现错误;而 JS 要执行到的时候才能发现错误。(代码先编译后执行)
安装 TS 编译包
npm i -g typescript 安装的原因:浏览器和 node.js 都只认识 js 代码,不认识 ts 代码,需要通过编译包把 ts 代码变成 js 代码。 查看版本:tsc -v (tsc:ts提供的命令)
编译 TS
-
创建 TS 文件,后缀名为 .ts
-
在终端输入 tsc .ts文件(比如 tsc helloWorld.ts),会在同目录生成一个 .js 文件
-
执行 .js 文件(node xxx.js)
编译失败的问题
原因:PowerShell 的默认执行策略是 Restricted 它禁止运行任何脚本和配置文件 解决:
-
打开 PowerShell
-
运行
set-ExecutionPolicy RemoteSigned
-
选择 y 或 a
-
运行
get-ExecutionPolicy
简化编译
使用 ts-node 简化,安装命令 npm i -g ts-node。 使用方式:ts-node xxx.ts,这条命令会在内部将 ts 变成 js 然后运行 js 代码。
简化编译失败问题
安装 ts-node 8.5.4 版本或运行 tsc –init 后再 使用 ts-node xxx.ts
类型注解
let str: string = '111'
str = 222 // 报错 不能将类型“number”分配给类型“string”
上面代码中的 :string 就是类型注解,规定了 str 只能是 string 类型的,如果是其他类型就会报错,就行第二行代码一样。约定了什么类型就只能给该变量赋值该类型的值。
常用基础类型
1、JS已有类型 原始类型:number/string/boolean/null/undefined/symbol 对象类型:object(包括Array,Object,Function) 2、TS新增类型 联合类型、自定义类型(类型别名)、接口、元组、字面量类型、枚举、void、any等
原始类型
// 这些类型完全按照 js 中的类型来书写
let str: string = '111'
let num: number = 111
let boolean = true
………………
默认情况下 null 和 undefined 是所有类型的子类型,所以可以赋值给任意类型。
对象类型、TS 新增类型
对象类型在 ts 中更加细化,每个对象都有自己的语法类型。
数组类型、联合类型
// 第一组
let arr1: number[] = [1, 2, 3]; // 写法1
let arr2: Array<string> = ["1", "2", "3"]; // 写法2
// 第二组
let arr3: (number | string)[] = [1, 2, "3"]; // 写法3
let aaa: string | number = 1 // 联合类型
// 第三组
let bbb = (string | number)[] = [1,2,3,'4']
let ccc = string | number[] = [1,2,3]
第一组指定了数组元素的类型,这是两种数组的类型注解。 第二组就是联合类型,表示可以是 string 和 number 其中的一个。(是 | 不是 || ) 第三组的两种写法的区别是:上面那一行表示数组元素可以是 string 或 number;而下面那一行表示变量 ccc 可以是 string 或 number 数组。
自定义类型
使用 type 关键字声明自定义类型。 如果一个复杂的类型使用的较多时可以使用,当创建了别名之后就可以直接使用该别名。
type arrType = (number | object | string)[];
let arr5: arrType = [1, "2", {}];
// arrType 就是一个类型别名
函数类型
函数类型值的是函数参数的类型和返回值的类型,为函数指定类型的两种方式:1、单独制定;2、同时指定。 函数参数小括号后面是返回值的类型。
1、单独制定参数返回值的类型
let fun1 = (num1: number, num2: number): string => {
return num1 + num2 + "";
}
let fun2 = (item1: string, item2: number): boolean => {
return Number(item1) == item2;
}
上面代码的 fun1 里为参数 num1 和 num2 都单独指定了 number 类型,返回值指定了 string 的类型。 在 fun2 里参数 item1 指定了 string 类型,item2 指定了 number 类型,返回值指定了 boolean 类型。
2、同时指定参数、返回值的类型
let fun3: (num1: number, num2: string) => boolean = (num1 = 20, num2 = "30") => {
return num1 == Number(num2);
}
(num1: number, num2: string) => boolean:这一段指定了参数和返回值的类型,这种写法只适合函数表达式。它写在函数名后面,写法和箭头函数相似,箭头后面写的是函数返回值的类型。
void 类型
如果一个函数没有返回值那么它返回值的类型就是 void。
let fun4 = (num1: number, num2: number): void => {
console.log(num1, num2);
};
fun4(11, 22);
函数可传参数
在一个参数后加上 ? 就表示这个参数可传可不传。需要注意的是可传参数必须是函数的最后一个参数,也就是说可传参数后面就不能再有必传参数了。
let fun5 = (item1: string, item2?: string): void => {
console.log(item1, item2);
};
fun5("必传参数", "可传参数");
fun5("必传参数");
对象类型
js 的对象是由属性和方法组成,所以指定对象的类型就是在指定对象的属性类型。
let obj: { name: string; age: number; sayHi(item1: number, item2: number): void } = { name: "Q", age: 17, sayHi(item1, item2) { console.log(item1, item2); } }; console.log(obj); obj.sayHi(10, 20);
写法:let obj: { } = { } 如果一行只指定一个属性的类型那么可以把 ;(分号) 去掉。 方法的类型也可以使用箭头函数的形式:{ sayHi:() => void }。
对象可选属性
可选属性的语法和函数的可选参数的语法一致,都是使用 ?。
let obj2: { name: string; age?: number; sayHi(item1: number, item2: number): void } = { name: "Q", sayHi(item1, item2) { console.log(item1, item2); } }; console.log(obj2); obj2.sayHi(10, 20);
接口( interface )
当对象的类型被多次使用时就可以使用接口( interface )来描述这个对象的类型,达到复用的效果。 接口使用 interface 关键字来声明,语法:interface interfaceName { }。声明接口后直接使用接口名称作为变量类型。
interface objType { name: string; age?: number; sayHi(item1: number, item2: number): void; } let obj2: objType = { name: "Q", sayHi(item1, item2) { console.log(item1, item2); } }; let obj3: objType = { name: "Q", age: 18, sayHi(item1, item2) { console.log(item1, item2); } };
interface(接口) 和 type(类型别名) 的区别
相同点:都可以给对象指定类型。 不同点:接口只能为对象指定类型,类型可以为任意类型指定别名。
接口继承
有两个或以上的接口如果有相同的属性或方法时可以把相同的部分抽出来,然后使用继承来达到复用的效果。 继承使用 extends 关键字实现,语法是:interface aaa extends bbb { };此时接口 aaa 继承了接口 bbb 里的属性和方法,所以 aaa 里就有 bbb 里的属性和方法了。
interface Point2D { x: number; y: number; } interface Point3D extends Point2D { z: number; } // 此时 Point3D 就是 { x: number; y: number; z: number; }
元组(Tuple)
如果一个数组里只有两三个元素并且确定它们的类型的话可以使用元组,元组是另一种类型的数组。它确切的指定了数组里有几个元素并且是什么类型的。
let position: [number, number] = [1, 2]; let arr: [number, string] = [1, "2"]; let arr1: [number, string, boolean, object] = [1, "2", true, {}]; let fun = (item1, item2) => { return item1 + item2; }; let arr2: [(item1: number, item2: string) => string, number, string] = [fun, 20, "22"]; console.log(arr2[0](arr2[1], arr2[2])); let arr3: [number, number] = [1, 2, 3]; // 不能将类型“[number, number, number]”分配给类型“[number, number]”。源具有 3 个元素,但目标仅允许 2 个
上面代码就是对元组的一个使用。
类型推论
在 TS 中,如果没有明确指出类型的地方,TS 的类型推论机制会帮助提供类型。 由于类型推论的存在,在一些地方类型注解就可以省略。 比如:
-
声明变量并初始化。
-
决定函数的返回值。
声明变量并初始化值的时候可以省略类型注解,但是没有初始化的时候就必须添加类型注解,不然就可以给该变量赋任何值。 函数 return 的时候也可以自动推断出类型,所以也可以省略函数返回值的类型注解。 如果不知道该变量的类型的话可以把鼠标放到变量名称上,VsCode 会提示变量类型。
let age = 18; // 可以省略 let num; // 不能省略 num = 1; num = "2"; // 可以省略函数返回值的类型注解 function add(num1: number, num2: number) { return num1 + num2 + ""; }
类型断言
当要拿到一个 DOM 特有的属性时就可以使用类型断言,比如通过 id 获取 img 的时候就不能再获取这个 img 的 src 了,因为这样获取的 img 它的类型是 Element,这个 Element 包含的属性是所有 DOM 都有的属性,比如 id。所以这时候需要用类型断言,语法:获取 DOM 元素 as 具体类型,或<具体类型>获取 DOM,第二种语法不常用。 当自己比 TS 更加明确一个元素的类型时就可以使用类型断言。
let img = document.querySelector(".img") as HTMLImageElement; console.log(img.src);
可以通过 console.dir( DOM ) 打印 DOM 元素,在属性列表最后查看该 DOM 的类型。
字面量类型
let str = "hello world"; const str2 = "hello world";
在上面的示例中 str 的类型是 string,但 str2 的类型是 hello world,这就是一个字面量类型。 在 js 中的任何字面量都可以作为字面量类型(数字、字符串等)。
let str3: 111 = 111; let str4: "ts" = "ts";
枚举
枚举使用 enum 声明,语法是:enum enumName { xxx, xxx }。
enum type { type1, type2, type3, type4 } function f1(item: type) { console.log(item); } f1(type.type4);
在上面的示例中,函数 f1 的形参 item 的类型是枚举 type,所以实参只能是 type 里的其中一个,如果要访问 type 里的内容的话类似于 js 里访问对象,直接通过 . 就行。
数字枚举:枚举成员的值为数字的枚举称为数字枚举,枚举成员的值默认从 0 开始自增。如果其中一个成员赋了值但是它后面没赋值那么它后面那个成员就是当前成员加 1。
enum type { type1, // 0 type2 = 10, type3, // 11 type4 // 12 }
字符串枚举:枚举成员是字符串的枚举称为字符串枚举,因为字符串没有自增,所以所有枚举成员都要赋值。
enum type { type1 = "Q1", type2 = "Q2", type3 = "Q3", type4 = "Q4" } enum type { type1 = "Q1", type2 = "Q2", type3 = "Q3", type4 // 枚举成员必须具有初始化表达式 }
只要当前枚举成员的上一个成员是数值型那么当前成员就可以是数字,或当前成员是数字那么下一个成员就可以不赋值。
枚举的特点 其它类型在编译时会自动去掉,但是枚举会被编译成 js 代码,因为枚举还提供了值。
// 编译前 let str: string = "1"; enum type { type1 = "Q1", type2 = "Q2", type3 = "Q3", type4 = "Q4", type5 = 1, type6 } function f1(item: type) { console.log(item); } f1(type.type4); // 编译后 var str = "1"; var type; (function (type) { type["type1"] = "Q1"; type["type2"] = "Q2"; type["type3"] = "Q3"; type["type4"] = "Q4"; type[type["type5"] = 1] = "type5"; type[type["type6"] = 2] = "type6"; })(type || (type = {})); function f1(item) { console.log(item); } f1(type.type4);
any 类型
不推荐使用 any,因为这会让 TypeScript 变成 AnyScript(失去 ts 的类型保护机制)。当值的类型为 any 时可以对该值进行任意操作,并且没有代码提示。
let str: any = {}; str = 1; str = ""; str();
上面的示例中不会有任何类型错误提示,即使存在错误。 其他具有隐式 any 类型的情况: 1、声明变量不提供了类型 2、函数参数不加类型
typeof
typeof 可以在类型上下文中引用变量或属性的类型。也就是说可以根据已有变量的值,获取该值的类型,来简化类型的书写。typeof 出现在类型注解的后面,名称冒号后面。 语法: let xx: typeof xx = xx function fun1( item1: typeof xxx ) { }
let str = { x: 1, y: 2 }; function fun1(item: typeof str) { console.log(item); } let aaa = "11111"; let bbb: typeof aaa = "22222";
上面的示例中函数 fun1 的形参 item 的类型跟 str 一样,是 { x: number, y: number },bbb 的类型和 aaa 一样,是 string。 需要注意的是 typeof 只能用来查询变量或属性的类型,无法查询函数返回值的类型。
let ccc: typeof fun2(1,2) // 逗号运算符的左侧未使用,没有任何副作用。
class
基本使用
声明一个 class 的方式:class className { xx:xxx; xx:xxx; }。 class 添加实例属性的方式:
-
key: type
-
key = value 或 key: type = value
创建 class 的实例:let xx = new className()。
class info { name: "Q"; age: number; } const t = new info();
构造函数的基本使用
class info2 { name: string; age: number; constructor(name: string, age: number) { this.name = name; this.age = age; console.log("info2的构造函数"); } } let q = new info2("Q", 17);
这样成员初始化完成以后才能通过 q.name 的方式获取内容。constructor 构造函数在 new info2 的时候执行,在里面把传进来的 name 和 age 赋值给当前实例的 name 和 age。
constructor 的形参要加上类型,但是返回值不用加,因为 constructor 是配合 class 来实现实例化对象的过程的,最后实例化出来的对象的类型就是 className,所以不用指定返回值的类型,指定之后还会报错。
class info2 { name: string; age: number; constructor(name: string, age: number): number { // 类型批注不能出现在构造函数声明中。 this.name = name; this.age = age; console.log("info2的构造函数"); } }
class 的实例方法
class 的实例方法和对象的方法写法一样。
class point { x = 20; y = 15; scale(n: number): void { this.x *= n; this.y *= n; } } let p = new point(); p.scale(5);
class 继承
继承的两种方式:
-
extends
-
implements
extends(继承父类)
class Animal { move() { console.log("Animal"); } } class Dog extends Animal {} let dog = new Dog(); dog.move();
上述代码 Dog 类继承了 Animal 类,所以 Dog 里有 Animal 里的属性和方法,而 dog 是 Dog 的实例对象,所以可以调用 move 方法。
implements(实现接口)
interface sing { start(item: string): void; } class Sing implements sing { start(item: string) { console.log(item); } } let s = new Sing(); s.start("!!!!!!!!!!!!!!!!");
在上述示例中定义了一个接口 sing 然后 class Sing 继承了 sing,如果是通过 implements 继承的话那么 Sing 类里必须要有 sing 接口里的属性和方法。 implements 的语法:class className implements interface,此时 className 里必须要有 interface 里的属性和方法。
class 类的可见性修饰符
用来控制 class 的方法或属性对于 class 外的代码是否可见。 可见性修饰符包括:
-
public(公有的)
-
protected(受保护的)
-
private(私有的)
public 修饰符
public:表示公开的,公有成员可以被任何地方访问,默认可见性。 因为属性和方法默认可见所以 public 可以省略。
protected 修饰符
protected:表示受保护的,仅对其声明所在的类和子类中可见,示例对象中不可见。
class class1 { protected fun1() { console.log("受保护的class1的方法"); } fun() { console.log("不受保护的方法"); } } class class2 extends class1 { fun2() { console.log("class2的方法"); this.fun1(); } } let var1 = new class2(); var1.fun2(); var1.fun1(); // 属性“fun1”受保护,只能在类“class1”及其子类中访问 var1.fun();
在上述示例中由于 class1 里的 fun1 是受保护的所以只能在 class1 里和它的子类里使用,示例对象中不能访问。
private 修饰符
private:表示私有的,只能在当前类里使用其它地方不可用。
class class3 { private fun3() { console.log("这是一个私有的方法"); } fun4() { this.fun3(); } } class class4 extends class3 {} let var2 = new class4(); var2.fun4(); var2.fun3(); // 属性“fun3”为私有属性,只能在类“class3”中访问。 let var3 = new class3(); var3.fun3(); // 属性“fun3”为私有属性,只能在类“class3”中访问。
在上述示例中 fun3 是一个私有方法,所以只能在 class3 里使用,其它不管是实例对象还是子类都不能访问。
readonly 修饰符
除了上面三个可见性修饰符之外还有一个常见的修饰符,readonly 只读修饰符。在属性前面添加之后除了在 constructor 构造函数之外就不能再其它地方更这个属性了。
class person { readonly age: number; constructor(num: number) { this.age = num; } } let var4 = new person(20); var4.age = 222; // 无法分配到 "age" ,因为它是只读属性 console.log(var4);
注意:
-
如果像 readonly age = 18 这样,赋值但不声明类型的话 age 就是一个字面量类型。
-
readonly 只能修饰属性不能修饰方法。
-
如果有默认值的话一定要带上类型注解,不然 TS 推论出来的可能不是想要的类型。
-
接口或 {} 表示的对象类型,也可以使用 readonly。
interface IPerson { readonly name: string; } let obj: IPerson = { name: "Q" }; obj.name = "QQQ"; // 无法分配到 "name" ,因为它是只读属性 let obj2: { readonly name: string } = { name: "Q" }; obj2.name = "QQQ"; // 无法分配到 "name" ,因为它是只读属性
类型兼容性
两种类型系统:
-
Structural Type System(结构化类型系统)
-
Nominal Type System(标明类型系统)
TS 采用的是结构化类型系统,也叫做 duck typing(鸭子类型),类型检查关注的是值所具有的形状。也就是说在结构类型系统中,如果两个对象具有相同的形状,则认为它们是同一类型。
class Point { x: number; y: number; } class Point2D { x: number; y: number; } let p: Point = new Point2D();
在上述代码中,声明了 Point 和 Point2D 两个类。然后创建了一个 Point2D 的实例,但这个实例的类型是 Point,这样还不报错,这是因为 Point 和 Point2D 这两个的属性和类型相同,在结构化类型系统中认为这两个是相同的类型所以不报错。这就是类型兼容性。
在结构化类型系统中,只检查结构是否相同(属性和类型是否相同)。 在 Nominal Type System 中(比如 C# JAVA),它们不是相同的类无法兼容。
对象之间的类型兼容性
let name: x = new y() “如果两个对象具有相同的形状,则认为它们是同一类型”,这样说还不太准确。对于对象类型来说,y 的类型至少与 x 的类型相同,这样 x 就会兼容 y(成员多的可以赋值给少的)。 只要 y 里的成员满足 x 的要求那么 x 就能兼容 y;反过来 y 里的成员不满足 x 的要求那么 x 就不能兼容 y。
class Point { x: number; y: number; } class Point3D { x: number; y: number; z: number; } let p2: Point = new Point3D();
在上述示例中 Point3D 的成员至少与 Point 相同,也就是说 Point 里有的,Point3D 里都有,这样 Point 兼容 Point3D。所以成员多的 Point3D 可以赋值给 Point。 如果成员少的赋值给多的那么就会报错。
class Person { x: number; fn(item: string) { console.log(item); } } class IPerson { x: number; } let p3: Person = new IPerson(); // 类型 "IPerson" 中缺少属性 "fn",但类型 "Person" 中需要该属性
接口之间的兼容性
接口之间的兼容性类似于 class。并且 calss 和 interface 之间也可以兼容。
interface Point { x: number; y: number; } interface Point2D { x: number; y: number; } interface Point3D { x: number; y: number; z: number; } class Person { x: number; y: number; z: number; q: number; } let p1: Point; let p3: Point3D; // Point 兼容 Point2D let p2: Point2D = p1; // Point 兼容 Point3D let p4: Point = p3; // class 和 interface 兼容 let p5: Point = new Person(); p3 = p1 // 类型 "Point" 中缺少属性 "z",但类型 "Point3D" 中需要该属性
在上述示例中 p1 的类型是 Point,p3 的类型是 Point3D。p2 的类型是 Point2D 但是把 p1 赋值给了 p2,不报错是因为 Point 和 Point2D 的属性和类型都相同所以这俩是兼容的。 p4 的类型是 Point 把 p3 赋值给了 p4,不报错是因为 p4 要求的东西 p3 里都有,p4 兼容 p3。 p5 的类型是 Point 同时它又是 Person 类的实例,Point 的要求 Person 都能满足所以 p5 能兼容 Person。 p3 = p1 报错的原因是因为:p3 的要求 p1 不能满足,所以 p3 不能兼容 p1(成员少的不能赋值给成员多的)。
函数之间的兼容性
函数之间的兼容性比较复杂,需要考虑参数个数、参数类型、返回值。
参数个数
参数多的可以兼容参数少的(参数少的可以赋值给参数多的)。
type F1 = (item1: number) => void; type F2 = (item1: number, item2: number) => void; let f1: F1; let f2: F2 = f1; // f1 = f2; // 不能将类型“F2”分配给类型“F1”
在上面的示例中 F1 的参数比 F2 的参数少,所以 F2 可以兼容 F1。在函数里参数多的可以兼容参数少的(参数少的可以赋值给参数多的)。
参数类型
相同位置参数的类型相同(或兼容)的话也可以兼容。
// 不能兼容 因为 F3 和 F4 在相同位置的形参的类型不一样 type F3 = (item1: string) => void; type F4 = (item: number) => void; let f3: F3; let f4: F4 = f3; // 不能将类型“F3”分配给类型“F4”。 // 参数“item1”和“item” 的类型不兼容。 // 不能将类型“number”分配给类型“string”。 // 可以兼容 因为 F5 和 F6 形参中的 item 互相兼容 type F5 = (item: { name: string; age: number }) => void; type F6 = (item: { name: string; age: number; xxx: number }) => void; let f5: F5; let f6: F6 = f5;
返回值
返回值的类型只关注返回值本身即可: 返回值是原始类型:相同就能兼容。 返回值是对象类型:成员多的可以赋值给成员少的。
// 错误示例 type F7 = () => string; type F8 = () => number; let f7: F7; let f8: F8; f8 = f7; type F9 = () => { name: string }; type F0 = () => { name: string; age: number }; let f9: F9; let f0: F0; f0 = f9; // 正确示例 type F7 = () => string; type F8 = () => string; let f7: F7; let f8: F8; f8 = f7; type F9 = () => { name: string }; type F0 = () => { name: string; age: number }; let f9: F9; let f0: F0; f9 = f0;
上述的错误示例中因为 F7 和 F8 的返回值类型不一样所以不能兼容;因为 F9 的成员比 F0 的成员少所以不能兼容。
交叉类型
交叉类型(&)可以把多个类型组合成一个类型。语法是:type aa = bb & cc,此时 aa 就同时具备了 bb 和 cc 的属性和方法。
interface Person1 { name: string; } interface Person2 { age: number; } interface Person4 { fun: (item: string) => void; } type Person3 = Person1 & Person2 & Person4; let obj: Person3 = { name: "Q", age: 17, fun(item) { console.log(item); } };
上述代码中 Person3 同时具备了 Person1、Person2 和 Person4 的属性和方法。
交叉类型和接口的区别
相同点:都可以实现对象类型的组合。 不同点:实现类型组合时对于同名属性处理类型冲突的方式不同。
接口会直接报错,而交叉类型会把冲突的类型变成类似于联合类型。
// 接口继承 interface Person5 { fn: (item: string) => string; } // 属性“fn”的类型不兼容 interface Person6 extends Person5 { fn: (item: number) => string; } let obj2: Person6 = { fn() { return "1"; } }; // 交叉类型 interface Person7 { fn: (item: string) => string; } interface Person8 { fn: (item: number) => string; } type Person9 = Person7 & Person8; let obj3: Person9 = { fn(item: number | string) { return "1"; } }
在上面的 Person5 和 Person6 中都有一个 fn 属性,但是他俩的形参 item 的类型不一样,所以直接在 Person6 里报错。 在 obj3 里它的类型是交叉类型 Person9,所以 fn 的 item 可以是 number 也可以是 string。
泛型
泛型可以在保证类型安全的前提下,让函数等与多种类型一起工作从而实现复用。泛型在保护类型安全的同时也可以让函数等与多种不同的类型一起工作,实现复用。 想要函数之类的做到可以同时支持多种不同的类型,又可以做到类型的安全,此时就可以使用泛型。
泛型基本使用
function id<type>(item: type): type { return item; } interface obj { name: string; } console.log(id<number>(1111)); console.log(id<string>("!!!!!!!!!!!!!!!!!")); console.log(id<obj>({ name: "Q" }));
定义泛型函数的语法是:function xxx<type> ( item: type ): type { return item },<> 里的内容是调用者传进来的类型,这个 type 是一个类型变量存放的是类型而不是值。 这样就可以做到传入值的类型和返回值的类型相同,也可以达到复用效果。 调用的语法:funName<type>( xxx )。type 就是要传递过去的类型。
简化调用泛型函数
function id<type>(item: type): type { return item; } let str = id("11");
在调用泛型函数是,可以把 <> 去掉达到简写调用,因为 TS 会根据传递实参自动推断出类型。比如传的是 ‘Q’ 那么 TS 会自动推断出是 string 的类型,但如果无法推断出或推断出的类型不准确时就要自己手动传递类型。
泛型约束
添加泛型约束的作用是收缩类型,以免想用某些属性的时候不能用。添加泛型约束主要有两种方式:
-
指定具体类型
-
添加约束
如果不添加约束的话无法访问任何属性,因为泛型可以代表任何类型,不确定当前类型是否拥有这个属性所以不能访问任何类型。
function fun1<type>(value: type): type { value.length; // 类型“type”上不存在属性“length” return value; }
指定具体类型
function fun1<type>(value: type[]): type[] { value.length; return value; } let str = fun1<string>(["1", "2", "3"]);
上面指定了 value 为 type 类型的数组,所以可以访问 value.length。
添加约束
使用 extends 关键字来添加约束,语法是:<Name extends xxx>,这代表着 Name 的类型必须要满足 xxx。
interface Length { length: number; } function fun2<type extends Length>(value: type): type { value.length; return value; } let str2 = fun2<number>(1); // 类型“number”不满足约束“Length” let str3 = fun2<string>("1");
str2 报错是因为传进的类型是 number,但 number 类型没有 length 属性。str3 传的是 string 类型,它有 length 属性所以不报错。
多个类型变量
泛型的类型变量还可以有多个,多个变量之间用逗号隔开。
function fun4<type, key>(val1: type, val2: key): type | key { return val2; } console.log(fun4("111", 222));
除此之外多个变量类型之间还可以互相约束。
function fun3<type, key extends keyof type>(obj: type, key: key) { return obj[key]; } let object = { name: "Q", age: 17 }; console.log(fun3(object, "name")); console.log(fun3(object, "age")); console.log(fun3(object, "age1")); // 类型“"age1"”的参数不能赋给类型“"name" | "age"”的参数 console.log(fun3([1, 2, 3], "forEach"));
<type, key extends keyof type>:这句代码的意思是 key 的值需要是 type 里的一个键。extends 是对 key 进行约束,而 keyof 的意思是获取 type 里的所有键。 键值对:{ 键:值 }
上面第三个 console.log 报错是因为变量 object 里没有 age1 这个属性。第四个 console.log 传入的 type 是一个数组,key 是 forEach,因为数组里有 forEach 这个键所以代码没问题。
泛型接口
interface NameFun<Type> { name: (value: Type) => Type; names: (value: Type[]) => Type[]; } let obj: NameFun<string> = { name(value) { return value; }, names: value => value }; console.log(obj.name("Q")); console.log(obj.names(["Q", "Q", "Q"]));
上面的 NameFun 就是一个泛型接口,泛型接口也可以提高接口的复用性。在调用泛型接口的时候跟调用泛型函数一样,都是在 <> 里传递一个类型。在调用的时候传一个 string 然后下方 name 里的 value 就自动是 string 的类型了。
泛型类
创建泛型类:class name<type> { },创建和调用跟泛型接口基本一样。
class Name<Type> { name: Type; add: (a: Type, b: Type) => void; } let obj = new Name<string>(); obj.name = "Q";
在有些时候可以不传具体类型。
class Name1<type> { name: type; constructor(value: type) { this.name = value; } } let str = new Name1(1); let str1 = new Name1("Q");
在上面创建 Name1 的实例时第一个传的是 1,TS 会自动推断出类型为 number,同理第二个也可以推断出 string。
泛型工具类型
TS 中内置了一些常用的工具类型,来简化一些常见操作。它们都是基于泛型实现的(泛型适用于多种类型)。主要学习一下:
-
Partial<Type>
-
Readonly<Type>
-
Pick<Type,Keys>
-
Record<Keys, Type>
Partial<Type>
Partial 的作用是复制一个结构相同的类,但都是可选的。
interface Props { id: string; children: number[]; } type PartialProps = Partial<Props>; let p1: Props = { id: "Q", children: [1, 2, 3, 4] }; let p2: PartialProps = { id: "QQQ" };
上面的 PartialProps 就是用 Partial 复制的 Props,它俩的结构是一样的,只不过 PartialProps 都是可选的而 Props 不是。
Readonly<Type>
Readonly 的作用是复制一个结构相同的类型,但都是只读的不能修改。
type ReadonlyProps = Readonly<Props>; let p3: ReadonlyProps = { id: "Q", children: [1, 2, 3, 4] }; p3.id = "111"; // 无法分配到 "id" ,因为它是只读属性
p3 的类型是 ReadonlyProps,它里面的成员都是只读的所以不能修改。
Pick<Type, Keys>
Pick 的作用是选择 Type 里的几个成员,复制到新的类里面,Keys 就是要复制的成员。 语法是:type xx = Pick<type, ‘key1’ | ‘key2’ |……>。
type PickProps = Pick<Props, "id" | "title">; let p4: PickProps = { id: "Q", title: "p4的TItle" };
PickProps 是选择了 Props 里的 id 和 title,所以 p4 里只有 id 和 title。 注意:keys 一定是 Type 里所具备的,不然会出现下面的情况。
type PickProps = Pick<Props, "id" | "title" | 'Q'>; // 类型“"Q" | "id" | "title"”不满足约束“keyof Props”。不能将类型“"Q"”分配给类型“keyof Props”
Record<keys, type>
Record 的作用是创建对象类型,这个对象类型的键是 keys,然后类型都是 type。
type RecordObj = Record<"a" | "b" | "c", string>; let obj: RecordObj = { a: "Q", b: "QQ", c: "QQQ" };
上面在 Record 中指定了 a b c 三个键,类型都是 string。如果不按照 Record 指定的类型来创建对象的话会出现以下的情况。
let obj: RecordObj = { a: "Q", b: "QQ", c: 1, // 速记属性 "d" 的范围内不存在任何值。请声明一个值或提供一个初始值设定项 d // 对象文字可以只指定已知属性,并且“d”不在类型“RecordObj”中。速记属性 "d" 的范围内不存在任何值。请声明一个值或提供一个初始值设定项 };
索引签名类型
当无法确定对象中有哪些属性的时候(对象可以出现多个任意属性时),可以使用索引签名类型。
interface strObject { [key: string]: string; } let obj: strObject = { a: "Q" };
此时只要键和值是 string 类型的都可以添加到对象 obj 里。
数组是一个特殊的对象它的键是数值型的,像这样可以模拟一个数组类型,只要是数值型就可以添加。
interface myArray<Type> { [i: number]: number; } let arr: myArray<number> = [1, 2, 3, 4, "5"]; // 不能将类型“string”分配给类型“number”
映射类型
映射类型:基于旧类型创建新类型(对象类型),减少重复代码。 语法:type type1 = { [key in keys]: type } 这样 type1 里的成员就都是 keys 里的成员了,并且类型是 type。
根据联合类型
type keys = "a" | "b" | "c" | "d" | "e" | "f" | "g"; type type1 = { [key in keys]: number }; /* type type1 = { a: number; b: number; c: number; d: number; e: number; f: number; g: number; } */
需要注意的是映射类型只能在别名中使用,不能在接口中使用。
根据对象类型
映射类型除了根据联合类型之外还可以根据对象类型进行创建。
type props = { a: string; b: number; c: string; }; type type2 = { [key in keyof props]: string }; /* type type2 = { a: string; b: string; c: string; } */
上面首先使用 keyof props 获取到 props 所有键的联合类型,即:’a’ | ‘b’ | ‘c’,然后 key in 映射到 type2 里。
映射类型里如果不指定键的类型那么就会是 any 类型。
type type2 = { [key in keyof props] }; /* type type2 = { a: any; b: any; c: any; } */
索引查询类型
像 T[P] 这样的语法在 TS 里叫做类型查询类型,它的作用是来查询 P 的类型。 语法是:type type1 = key[‘value’],这样 type1 的类型就是 key 里 value 的类型。
type obj = { name: string; age: number; }; type typeA = obj["name"]; // type typeA = string type typeB = obj["name1"]; // 类型“obj”上不存在属性“name1”
这样 typeA 的类型就是 string。 需要注意的是 [] 里的属性 obj 里必须有。
除了查询单个之外还可以查询多个,多个键之间使用 | 隔开。
type obj = { name: string; age: number; a: string; b: number; c: boolean; d: () => void; }; type typeB = obj["name" | "c"]; // type typeB = string | boolean type typeC = obj[keyof obj]; // type typeC = string | number | boolean | (() => void)
typeB 取的是 obj 里的 name 和 c 的联合类型;typeC 取的是 obj 里所有的联合类型。
类型声明文件
类型声明文件:用来为已存在的 JS 库提供类型信息。
TS 中的两种文件类型:
-
.ts 文件
-
.d.ts 文件
.ts 文件:
-
即包含类型信息又可执行代码。
-
可以被编译为 .js 文件,然后执行代码。
-
用途:编写程序代码的地方。
.d.ts 文件:
-
只包含类型信息的类型声明文件。
-
不会生成 .js 文件,仅用于提供类型信息。
-
用途:为 js 提供类型信息。
.ts 是 implementation(代码实现文件),.d.ts 是 declaration(类型声明文件)。
使用已有的类型声明文件
1、内置类型声明文件。 2、第三方库的类型声明文件。
内置类型声明文件
TS 为 JS 运行时可用的所有标准化内置 API 都提供了声明文件。
第三方库的类型声明文件
第三方库的类型声明文件有两种存在形式。 1、库自带类型声明。 2、由 DefinitelyTyped 提供。 正常导入该库,TS 会自动加载库自己的类型声明文件,以提供该库的类型声明。
创建自己的类型声明文件
1、项目内共享类型。 2、为已有的 JS 文件提供类型声明。
项目内共享类型
如果多个 .ts 文件中都用到同一类型,此时可以创建 .d.ts 文件,实现共享。
// index.d.ts 文件 type info = { name: string; age: number; }; export { info }; // 01-index.ts 文件 import { info } from "./index"; let obj1: info = { name: "Q", age: 17 }; // 02-index.ts 文件 import { info } from "./index"; let obj2: info = { name: "Z", age: 17 };
上面示例中在 index.d.ts 类型声明文件中声明了类型 info,并把它暴露出去,在 01-index.ts 和 02-index.ts 中就可以引入使用类型 info。
为已有 JS 文件提供类型声明
TS 项目中也可以使用 .js 文件,在导入 .js 文件时 TS 会自动加载与它同名的 .d.ts 的类型声明文件。 declare:用于类型声明,为其它地方的变量声明类型,而不是创建新的变量。 对于 type、interface 等这些只有 TS 使用的地方可以省略 declare 关键字,但是像 let、function 这些 JS 和 TS 都能使用的地方应该加上 declare 关键字,表示声明类型。
原文地址:http://www.cnblogs.com/0529qhy/p/16807417.html