TypeScript初步
1.TypeScript vs JavaScript
TypeScript
的JavaScript
的超集,其实就是JavaScript
有的TypeScript
都有;TypeScript
对TypeScript
进行了扩展,在TypeScript
中引入了类型的概念,并且添加了很多新特性;TypeScript
和JavaScript
的关系,就好比sass/less
和css
之间的关系,sass/less
对css
进行了扩展;
2.TypeScript 的特性
2.1 类型批注
类型批注是指在声明变量时明确指定变量的类型。 通过使用冒号(:
)来指定变量的类型,开发者可以明确告诉编译器该变量的预期类型,从而提高代码的健壮性和可读性。例如:
let age:number = 25; //表示变量 age 的类型是 number。
let name:string = "John";//表示变量 name 的类型是 string。
let isStudent:boolean = true; //表示变量 isStudent 的类型是 boolean
类型批注的优势:
- 通过类型批注,编译器可以在编译阶段发现潜在的错误,从而减少运行时错误。
- 增强可读性:明确的类型批注使得代码更容易理解,尤其是对于团队开发来说,可以减少误解。
- 自动完成和提示:在使用支持TypeScript的编辑器(如VS Code)时,类型批注可以帮助自动完成和提供类型相关的提示,提高开发效率。
2.2 类型推断
当类型没有给出时候,Typescript
编译器利用类型推断机制来推断类型,如下
let str = "string";//推断为String类型
//如果推断不出来,就推断any类型
2.3 接口(ECMAScript中没有这个概念)
简单来说就是描述对象的类型。基本的数据类型已经可以用Number
、String
、Boolean
等描述;而对象类型则需要用接口描述。(用的很广泛)
interface Person{
name:String,
age:Number
}
let student:Person={
name:"jack",
age:12
}
2.4 枚举
enum Color {Red, Green, Blue}
let color1: Color = Color.Green;
console.log(color1)
2.5 元组
TypeScript中的元组(Tuple
)是一种特殊的数据结构,允许表示一个固定长度和固定类型的数组,其中每个元素可以有不同的数据类型。与普通的数组不同,元组中的每个位置可以拥有不同的数据类型,这使得它在表示固定长度和类型的数据结构时非常有用。
元组的定义方式是将多个类型用逗号分隔,然后用方括号[]
将它们括起来。例如:
let myTuple: [string, number, boolean];
这个声明表示myTuple是一个包含三个元素的元组,分别是一个字符串、一个数字和一个布尔值。可以将值赋给元组的方式也很直观:
myTuple = ["hello", 123, true];
2.6 其它
- mixin
- 泛型编程
- 名字空间
- …
3.TypeScript的基础数据类型
3.1JavaScript已有类型
- string
- number
- boolean
- undefined
- null
- bigint
- symbol
- object
3.2 TypeScript新增类型
- array数组类型
- tuple元祖类型
- enum枚举类型
- any类型
- void类型
- never类型
- unknown类型
3.3 array类型
(1)元素类型后面跟上[]
let arr1:number[]=[1,2,3]
// arr1=['1','2']//爆红
(2)使用数组泛型,Array<元素类型>
let arr2:Array<String>=['1','2','3']
// arr2=[1,2]//爆红
3.4 分析一下object类型
let object1:object={age:1};
let object2:object=[];
// let object3:object=12;//爆红,不是object类型
let object4:{ name : String}={name:"jack"}
//在属性后面加?表示该属性是可选的
let object5:{ name : String,age?:number}={name:"jack",age:12}
let object6:{ name : String,age?:number}={name:"jack"}
//[propName:string]:any表示任意类型的属性
let object7:{name:String,[propName:string]:any}={name:"jack",grade:89}
3.5 tuple元祖类型
(1)元组和数组有什么不同
- 数组:由同类型的值组成
- 元祖:确定了元素个数和类型,类型可以不相同
(2)示例代码
let tuple1:[string,boolean,number]=["jack",true,12];
// let tuple2:[string,boolean,number]=["jack",true];//爆红
// 元祖类型的剩余元素
type restTupleType=[number,...string[]];
// let restTuple1:restTupleType=[12,13]//爆红
let restTuple2:restTupleType=[12,"mike","jack"]
// restTuple2[0]="111";//报错
restTuple2[0]==99;//报错
//只读元祖类型(不能写)
const point:readonly [number,number]=[12,15];
// point[0]=12//爆红
3.6 enum枚举类型
enum Color {Red, Green, Blue}
let color1: Color = Color.Green;
3.7 any类型类型
ps:尽量不用any,因为它会失去TypeScript的保护优势
let a:any=666;
a=false;//不爆红
a="111"//不爆红
a=undefined;//不爆红
a=[]//不爆红
let b;
b=false;//不爆红
b="111"//不爆红
b=undefined;//不爆红
b=[]//不爆红
let arrayList: any[] = [1, false, 'fine'];
arrayList[1] = 100;
3.8 null和undefined类型
ps:null和undefined类型是所有类型的子类型;可以把null和undefined赋值给其他所有类型;
let str:string="666";
// str=12//爆红
str=undefined//不爆红
str=null//不爆红
3.9 void类型
用于标识方法返回的类型,表示该方法没有返回值;
// function greet1():number {
// console.log("你好");
// }//爆红
// function greet2():number {
// console.log("你好");
// return "666"
// }//爆红
// function greet3():void {
// console.log("你好");
// return "666"
// }//爆红
function greet4():void {
console.log("你好");
}//不爆红
function greet5():void {
console.log("你好");
return
}//不爆红
3.10 never类型
never是其它类型(包含undefined和null)的子类型,可以赋值给任何类型,代表的是永远不会出现的值;
一般用于指定抛出异常、无限循环,如下:
let a:never;
// a = 123; //爆红
a = (() => { //
throw new Error(' ');
})()
function error(message: string): never {
throw new Error(message);
}
// 推断的返回值类型为never
function fail() {
return error("Something failed");
}
// 返回never的函数必须存在无法达到的终点
function infiniteLoop(): never {
while (true) {
}
}
下面来看一个很好的例子----使用never类型的特性实现全面性检查:
type Foo = string | number;
function controlFlowAnalysisWithNever(foo: Foo) {
if (typeof foo === "string") {
// 这里 foo 被收窄为 string 类型
} else if (typeof foo === "number") {
// 这里 foo 被收窄为 number 类型
} else {
// foo 在这里是 never
//注意在 else 分支里面,我们把收窄为 never 的 foo 赋值给一个显示声明的 never 变量。
const check: never = foo;
}
}
如上代码,看起来优点多此一举,也不会报错。但如果有一天,需求改变了,需要给Foo
加一个boolean
类型。
于是程序员修改代码如下,但忘记同时修改controlFlowAnalysisWithNever
方法中的控制流程(实际中用到Foo
的地方可能还有很多,程序员可能大多数地方修改了相应的代码,但一小部分可能会遗忘,毕竟是人就会犯错误)。
type Foo = string | number | boolean;
这时候会发现else
中会报错,这样子就能很快识别出错误(这时候else
分支的foo
类型会被收窄为 boolean
类型,导致无法赋值给never
类型,这时就会产生一个编译错误。)。
通过这个方式,我们可以确保 controlFlowAnalysisWithNever
方法总是穷尽了 Foo
的所有可能类型。得出一个结论:使用 never
避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码。
3.11 unknown类型
面试题:any
和unknown
的区别?
any
表示任意类型,任何类型的值都可以赋值为any
,而any
类型的值也可以赋值为任何类型unknown
:只有unknown
和any
的值可以赋值为unknown
;unknown
类型的值也可以赋值为任何类型
//----------------------------------------
//下面这一块说明:
// any类型的值可以赋值给任何类型的变量
// 可以将任何类型的值赋值给any类型的变量
let anyValue: any = 4;
anyValue = "18";
anyValue = true;
let x: number = 12;
x = anyValue;
console.log(x);
let y: string = "123";
y = anyValue;
console.log(y);
//----------------------------------------
//下面这一块说明:
// unknown类型的值只能赋值给unknwn和any类型的变量
// 可以将任何类型的值赋值给unknown类型的变量
let unknownVaule: unknown = 4;
unknownVaule = "18";
unknownVaule = true;
let b: unknown = "123";
b = unknownVaule; //不报错,说明unknown类型的值可以赋值给unknown类型的变量
console.log(b);
let c: unknown = "123";
let anyValue2: any = false;
c = anyValue2; //不报错,说明any类型的值可以赋值给unknown类型的变量
console.log(c);
let d: number = 12;
//d = unknownVaule; //报错,不能将类型unknown分配给类型number
let e: string = "12";
// e= unknownVaule;//报错,不能将类型unknown分配给类型string
let f: boolean = true;
//f= unknownVaule;//报错,不能将类型unknown分配给类型boolean
4. TypeScript的高级数据类型
4.1 交叉类型&
function extend<T,U>(first: T, second: U):T&U {
let result:any= {};
for (let key in first) {
result[key] = first[key]
}
for (let key in second) {
if(!result.hasOwnProperty(key)) {
result[key] = second[key]
}
}
return result
}
let object1={
name:"jack",
age:12
}
let object2={
name:"mike",
grade:29
}
let object3=extend(object1,object2)
console.log(object3)
交叉类型允许将多个类型合并为一个类型。这意味着可以将多个接口或类型别名合并为一个单一的复合类型,这个类型将拥有所有合并类型成员的属性。假设我们有两个接口,分别描述了人的基本信息和职业信息:
interface PersonInfo {
name: string;
age: number;
}
interface JobInfo {
position: string;
company: string;
}
如果我们想创建一个同时拥有PersonInfo
和JobInfo
属性的类型,我们可以使用交叉类型:
type FullPersonInfo = PersonInfo & JobInfo;
//FullPersonInfo 类型现在包含了 PersonInfo 和 JobInfo 的所有属性:
let person: FullPersonInfo = {
name: "Alice",
age: 30,
position: "Software Engineer",
company: "Tech Corp"
};
交叉类型的特性如下:
- 属性覆盖:如果两个类型中有相同的属性但类型不同,交叉类型会保留这些属性的最宽泛的类型。例如,如果两个类型都有一个 age 属性,但一个是 number 另一个是 string,结果类型中的 age 将是 number | string。
- 可选属性:如果一个属性在其中一个类型中是可选的(即属性后面有 ?),而在另一个类型中是必须的,那么在交叉类型中该属性会是可选的。例如,如果 PersonInfo 中的 age 是可选的,而 JobInfo 中的 age 是必须的,那么在交叉类型中,age 将是可选的。
- 只读属性:如果一个属性在其中一个类型中是只读的,那么在交叉类型中该属性也将是只读的。
4.2 联合类型
联合类型是一种允许值是多种类型之一的类型。这对于那些可能具有多种类型的变量或属性特别有用。举个例子:
interface Bird {
type: "bird";
name: string;
}
interface Hour {
type: "hour";
hour: number;
}
type Animal = Bird | Hour;
function isBird(animal: Animal) {
return animal.type === "bird";
}
4.3 类型别名(自定义类型)
简单示例:
type some = boolean | string
const b: some = true // ok
const c: some = 'hello' // ok
// const d: some = 123 //报错
类型别名可以是泛型:
type Container<T> = { value: T };
let test1:Container<number>={value:"1222"}//爆红
let test2:Container<string>={value:12}//爆红
可以在类型别名中引用自己:
type tree<T>={
val:T,
left:tree<T>,
right:tree<T>
}
let tree1:tree<number>={
val:18,
left:{
val:16,
left:{
val:21,
left:null,
right:null
},
right:null
},
right:{
val:21,
left:null,
right:undefined
}
}
ps:类型别名和接口很相似,都可以描述一个对象和函数。区别是接口只定义对象类型;类型别名除此之外还可以定义交叉、联合、原始类型等,所以类型别名用法更广泛;
4.4 索引类型
(1)基本概念
索引类型是一种强大的特性,它允许根据对象的属性名称(即键)来获取对应的属性类型。这对于那些在运行时键名未知或者键名动态生成的场景非常有用。
你可以使用索引签名来定义一个对象,其中对象的属性可以通过索引访问。例如:
interface StringArray {
[index: number]: string;
}
let myArray: StringArray = ["apple", "banana", "cherry"];
在这个例子中,StringArray 是一个接口,它有一个索引签名 [index: number]: string
;。这意味着任何通过数字索引访问的属性都应该是字符串类型。
索引类型查询是另一种使用索引类型的方式,它允许你从一个对象类型中提取键的类型,并基于这些键的类型创建一个新的类型。这在处理对象映射时特别有用。
例如,如果你有一个对象,其属性是不同类型的值,你可以创建一个类型来描述这些属性的键和值:
interface Person {
name: string;
age: number;
location: string;
}
type PersonKeys = keyof Person; // "name" | "age" | "location"
//相当于type PersonKeys= "name" | "age" | "location"
type PersonValues = Person[keyof Person]; // string | number | string
在这里,keyof Person
生成一个联合类型 "name" | "age" | "location"
,这是 Person 接口所有属性的键的类型。而 Person[keyof Person]
则生成一个联合类型 string | number | string
,这是通过 Person 接口的所有属性值组成的类型。
4.5 映射类型
一个常见的需求是将一个已知的类型每个属性都变为可选的:
interface PersonPartial {
name?: string;
age?: number;
}
或者我们想要一个只读版本:
interface PersonReadonly {
readonly name: string;
readonly age: number;
}
这在JavaScript里经常出现,TypeScript提供了从旧类型中创建新类型的一种方式 — 映射类型。 在映射类型里,新类型以相同的形式去转换旧类型里每个属性。 例如,你可以令每个属性成为 readonly
类型或可选的。 下面是一些例子:
type MyReadonly<T> = {
readonly [P in keyof T]: T[P];
};
type MyPartial<T> = {
[P in keyof T]?: T[P];
};
interface Person {
name: String;
age: number;
}
type PersonPartial = MyPartial<Person>;
//所以type MyReadonly<Person> = {
// readonly [P in keyof Person]: Person[P];
//};
//即type MyReadonly<Person> = {
// readonly name: String;
// readonly age: number;
//};
type ReadonlyPerson = MyReadonly<Person>;
let person1: PersonPartial = { age: 18 }; //不报错
let person2: PersonPartial = { age: 18, name: "bob" }; //不报错
let person3: PersonPartial = { age: "18", name: "bob" }; //报错
let person4: PersonPartial = { age: 18, name: "bob", sex: "男" }; //报错
person1.age = 12; //不报错
let person5: ReadonlyPerson = { age: 78, name: "bob" }; //不报错
person5.age = 19; //会报错
4.6 条件类型
基本语法如下:
T extends U ? X : Y
假设我们想创建一个条件类型,用于判断一个类型是否为字符串,如果是,则返回字符串类型的别名,否则返回 never
:
type StringOrNever<T> = T extends string ? string : never;
在这个例子中,如果 T
是 string
类型,StringOrNever<T>
将是string
;否则,它将变为 never
。
可以看下面这个例子:
interface Animal {
live(): void;
}
interface Dog extends Animal {
woof(): void;
}
type Example1 = Dog extends Animal ? number : string; // Example1 为 number
type Example2 = RegExp extends Animal ? number : string; // Example2 为 string
let obj1: Example1 = 12;
let obj2: Example1 = "12"; //爆红
let obj3: Example2 = 12; //爆红
let obj4: Example2 = "12";
4.7 类型约束
通过extends关键字进行约束
type BaseType = string | number | boolean
//这里表示copy的参数只能是字符串、数字、布尔值这几种标准类型
function copy<T extends BaseType>(arg: T): T {
return arg
}
再看一个例子,这个例子也可以帮助理解keyof。
function getValue<T, K extends keyof T>(obj: T, key: K) {
return obj[key]
}
const obj = { a: 1 }
const a = getValue(obj, 'a')
如果把getValue(obj, 'a')
改为getValue(obj, 'b')
会报错:
5. TypeScript接口
5.1 概述
接口是对一个对象的属性和方法进行描述,但不具体实现;在TypeScript中经常用于对类型进行检测
5.2 使用方式
(1)可选属性
interface User {
name: string
age?: number
}
(2)只读属性
interface User {
name: string
age?: number
readonly isMale: boolean
}
let user1:User={
name: "JACK",
isMale: true
}
// user1.isMale=false;//爆红
(3)属性中有函数
interface User {
name: string
age?: number
readonly isMale: boolean
say: (words: string) => string
}
(4)类型断言
interface User {
name: string;
age: number;
}
const getUserName = (user: User) => user.name;
getUserName({ color: "yellow" }); //爆红
getUserName({ color: "yellow" } as any); //不爆红
getUserName({ name: "bob" }); //爆红
getUserName({ name: "bob" } as any); //不爆红
getUserName({ name: "bob", age: 12 }); //不爆红
(5)给接口添加字符串索引签名
interface User {
name: string
age: number
[propName: string]: any;
}
(6)实现继承
interface Father {
color: String
}
interface Mother {
height: Number
}
interface Son extends Father,Mother{
name: string
age: Number
}
let user1:Son={
color:"red",
height:180,
name:"jack",
age:12
}
6. TypeScript类
6.1 ECMAScript6的类和TypeScript的类:
- ECMAScript6提出了class关键字,虽然本质依然是构造函数,但使用方便了很多,但还有一些特性没加入,如修饰符和抽象类;
- TypeScript中则支持oop(面向对象)的所有特性,包含接口、类等
6.2 使用方式
class Car {
//字段
engine:string;
//构造函数
constructor(engine:string) {
this.engine = engine
}
//方法
disp():void {
console.log("发动机为: "+this.engine)
}
}
6.3 继承
class Animal {
move(distanceInMeters: number = 0) {
console.log(`Animal moved ${distanceInMeters}m.`);
}
}
class Dog extends Animal {
bark() {
console.log('Woof! Woof!');
}
}
const dog = new Dog();
dog.bark();//Woof! Woof!
dog.move(10);//Animal moved 10m.
dog.bark();//Woof! Woof!
我们需要知道一些基本的概念:
- 基类(超类,父类)
- 派生类(子类)
- 方法的重写
注意方法重写和重载的区别。
重载:在同一个类中,定义多个同名方法,但参数列表(参数类型、数量或顺序)不同。
重写:子类重新定义父类中的方法,子类方法名、参数类型及个数必须与父类保持一致。重写方法的返回类型可以是父类返回类型的子类。 - super关键字
通过super关键字对父类直接引用,可以引用父类的属性和方法;
下面来看一下super关键字的例子:
class PrinterClass {
doPrint():void {
console.log("父类的doPrint()方法");
}
}
class StringPrinter extends PrinterClass {
doPrint():void {
super.doPrint() //
console.log("子类的doPrint()方法");
}
}
let stringPrinter1=new StringPrinter();
stringPrinter1.doPrint();
6.4 修饰符
TypeScript在ECMAScript6基础上加了三种修饰符。
(1)private
只能在该类内部访问,实例对象不能够访问。
class Father{
private name:string
constructor(name:string) {
this.name=name;
console.log("内部访问name属性:"+this.name)
}
greet(){
console.log(`hello,我是${this.name}`)
}
}
let testFather=new Father("JACK");
testFather.greet();
// console.log(testFather.name)//爆红
问题:那么怎么访问或者修改私有属性呢?
在类中写get和set方法。如下:
class Father{
private name:string
constructor(name:string) {
this.name=name;
console.log("内部访问name属性:"+this.name)
}
greet(){
console.log(`hello,我是${this.name}`)
}
getName(){
return this.name;
}
setName(val){
this.name=val;
}
}
let testFather=new Father("JACK");
testFather.setName("mike");
console.log(testFather.getName())//mike
(2)protected
protected 成员在定义它的类及其派生类中是可见的,实例对象不能够访问。
class Parent {
protected name: string = "Parent";
constructor() {
console.log(this.name); // 可以访问
}
}
class Child extends Parent {
constructor() {
super();
console.log(this.name); // 可以访问
}
}
const parent = new Parent(); // 正确:可以创建Parent类的实例
console.log(parent.name); // 错误:无法从外部访问protected成员
(3)public
修饰的属性可以在任何位置访问,没写默认为public。
6.5 类属性(静态属性)和普通属性;类方法和普通方法
类属性、类方法(static关键字)只能由类去调用;普通属性、普通方法只能由创建的对象去调用;
//静态属性
class Config {
static baseUrl = 'https://api.example.com';
}
console.log(Config.baseUrl); // 输出: https://api.example.com
//普通属性
class User {
name: string;
constructor(name: string) {
this.name = name;
}
}
const user1 = new User('Alice');
console.log(user1.name); // 输出: Alice
6.6 抽象类(以abstract开头)
不会被实例化。它与接口又有点不同,它可以包含成员的实现细节。
abstract class Animal {
abstract makeSound(): void;
move(): void {
console.log('roaming the earch...');
}
}
class Cat extends Animal {
makeSound() {
console.log('miao miao')
}
}
const cat = new Cat()
cat.makeSound() // miao miao
cat.move() // roaming the earch...
7. TypeScript枚举
7.1 数字枚举
当申明一个枚举类型时候,没给它赋值,它的默认值就是默认的数字类型,从0开始累加。
enum Direction {
Up, // 默认值为0
Down, // 默认值为1
Left, // 默认值为2
Right // 默认值为3
}
console.log(Direction.Up === 0); // true
console.log(Direction.Down === 1); // true
console.log(Direction.Left === 2); // true
console.log(Direction.Right === 3); // true
当对第一个值进行赋值时候,后面的值也会根据前一个值进行累加:
enum Direction {
Up = 10,
Down,
Left,
Right
}
console.log(Direction.Up, Direction.Down, Direction.Left, Direction.Right);
// 10 11 12 13
7.2 字符串枚举
enum Direction {
Up = 'Up',
Down = 'Down',
Left = 'Left',
Right = 'Right'
}
console.log(Direction['Right'], Direction.Up); // Right Up
7.3 异构枚举
如下,把数字枚举和字符串枚举结合起来混合使用。
enum BooleanLikeHeterogeneousEnum {
No = 0,
Yes = "YES",
}
7.4 枚举的本质
enum Direction {
Up,
Down,
Left,
Right
}
通过编译成js后代码如下:
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 0] = "Up";
Direction[Direction["Down"] = 1] = "Down";
Direction[Direction["Left"] = 2] = "Left";
Direction[Direction["Right"] = 3] = "Right";
})(Direction || (Direction = {}));
看Direction[Direction["Up"] = 0]= "Up"
;实际上做了两个操作:Direction["Up"] = 0
和Direction[0]= "up"
。
console.log(Direction[0])//Up
console.log(Direction.Up===0)//true
8. TypeScript函数与JavaScript函数区别?
8.1 使用方式
const add = (a: number, b: number) => (a + b)
console.log(add(2,4))//6
//完整写法:
let add: (a: number, b: number) => number =
function(a: number, b: number): number { return a+b; };
8.2 可选参数
const add = (a: number, b?: number) => a + (b ? b : 0)
8.3 剩余类型
const add = (a: number, ...rest: number[]) => rest.reduce(((a, b) => a + b), a)
8.4 函数重载
函数名相同,函数的参数类型或者个数不相同。
// 定义函数签名
function add2(a: number, b: number): number;
function add2(a: string, b: string): string;
// 实现函数体
function add2(a: any, b: any): any {
if (typeof a === "number" && typeof b === "number") {
return a + b;
} else if (typeof a === "string" && typeof b === "string") {
return a + b;
}
throw new Error("Invalid arguments");
}
// 使用函数
console.log(add2(1, 2)); // 输出: 3
console.log(add2("Hello", "World")); // 输出: HelloWorld
console.log(add2(1, "a")); // 爆红
console.log(add2("Hello", false)); // 爆红
8.5 在JavaScript中和TypeScript中函数的区别
- TypeScript中有函数参数类型,返回值类型;
- TypeScript有可选参数
- TypeScript中有函数重载功能
9. TypeScript泛型
9.1 使用方式
函数声明:
function funName<T>(param:T):T{
return param;
}
console.log(funName(true));//true
console.log(funName(111));//111
function funName1<T,U>(tuples:[T,U]):[U,T]{
return [tuples[1],tuples[0]];
}
console.log(funName1([12,"12"]))//['12',12]
接口声明:
interface ReturnItemFn<T> {
(para: T): T
}
//可以这样子声明函数
const returnItem: ReturnItemFn<number> = para => para;
类声明:
class Stack<T> {
private arr: T[] = []
public push(item: T) {
this.arr.push(item)
}
public pop() {
this.arr.pop()
}
}
const stack = new Stack<number>()
若上述只能传递string和number类型,则需要用<T extends xx>
的方式实现约束泛型。
type params=number | string
class Stack<T extends params> {
private arr: T[] = []
public push(item: T) {
this.arr.push(item)
}
public pop() {
this.arr.pop()
}
}
const stack1 = new Stack<number>()
// const stack2 = new Stack<boolean>()//爆红
9.2 索引类型和约束类型
function getValue<T extends object, U extends keyof T>(obj: T, key: U) {
return obj[key] // ok
}
let obj1={
name:"ldh",
age:19
}
console.log(getValue(obj1,"name"))
// console.log(getValue(obj1,"name1"))//爆红
console.log(getValue(obj1,"age"))
let obj2 = {
name: "ldh",
sex: 1,
};
console.log(getValue(obj2, "sex"));
console.log(getValue(obj2, "age")); //爆红
10. TypeScript装饰器
详细内容见https://www.tslang.cn/docs/handbook/decorators.html。
11. 模块与命名空间
11.1 命名空间
命名空间是一种将代码组织在模块中的方式,它可以用来封装类和函数,避免全局作用域的污染。从 TypeScript 2.0开始,官方推荐使用模块而不是命名空间,因为模块提供了更好的封装性和更清晰的语法。但是,了解命名空间的使用仍然有其价值,特别是在与旧代码兼容或特定场景下。
命名空间本质上是一个对象;作用是把一系列相关的全局变量组织到一个对象中去;若我们要在外部可以调用,需要给该属性加上export关键字。
namespace Letter {
export let a = 1;
export let b = 2;
export let c = 3;
// ...
export let z = 26;
}
namespace SomeNameSpaceName {
export interface ISomeInterfaceName { }
export class SomeClassName { }
}
console.log(Letter.a)//1
11.2 模块
使用方法和es6相似,可以导出类型别名。
//export.ts
export const a = 1
export type Person = {
name: String
}
//在另外一个文件中引入模块
import { a, Person } from './export';
11.3 命名空间和模块的区别
二者都可以用于组织代码。
- 命名空间是位于全局环境下的一个对象,它很难去识别组件中的依赖关系,不适合在大型应用中使用;
- 模块更适合用于大型项目或者需要复用代码的场景,它可以声明自己的依赖;
12. 面向对象三大特性
封装、继承、多态。
- 封装
将对象的属性和方法封装在一个对象,形成一个独立的实体 - 继承
子类继承父类的属性和方法 - 多态
龙生九子各不相同。当一个父类引用指向一个子类对象,调用的方式是子类的方法,这就多态性的体现
13.其它知识点
13.1 字面量类型
JavaScript
中:字面量都是一个值;TypeScript
中:字面量不仅仅可以是一个值,也可以是一个类型
如下图:
{
let specifiedStr: 'this is string' = 'this is string';
let str: string = 'any string';
specifiedStr = str; // ts(2322) 类型 '"string"' 不能赋值给类型 'this is string'
str = specifiedStr; // ok
}
13.2 类型拓宽
所有通过let
或var
定义的变量、函数的形参、对象的非只读属性,如果满足指定了初始值且未显式添加类型注解的条件,那么它们推断出来的类型就是指定的初始值字面量类型拓宽后的类型,这就是字面量类型拓宽。
下面来看一个例子:
let str = 'this is string'; // 类型是 string
let strFun = (str = 'this is string') => str; // 类型是 (str?: string) => string;
const specifiedStr = 'this is string'; // 类型是 'this is string'
let str2 = specifiedStr; // 类型是 'string'
let strFun2 = (str = specifiedStr) => str; // 类型是 (str?: string) => string;
因为第1~2
行满足了 let
、形参且未显式声明类型注解的条件,所以变量、形参的类型拓宽为string
(形参类型确切地讲是string | undefined
)。
因为第3
行的常量不可变更,类型没有拓宽,所以specifiedStr
的类型是'this is string'
字面量类型。
第 4~5
行,因为赋予的值specifiedStr
的类型是字面量类型,且没有显式类型注解,所以变量、形参的类型也被拓宽了。其实,这样的设计符合实际编程诉求。我们设想一下,如果 str2
的类型被推断为 'this is string'
,它将不可变更,因为赋予任何其他的字符串类型的值都会提示类型错误。
基于字面量类型拓宽的条件,我们可以通过如下所示代码添加显示类型注解控制类型拓宽行为。
{
const specifiedStr: 'this is string' = 'this is string'; // 类型是 '"this is string"'
let str2 = specifiedStr; // 即便使用 let 定义,类型是 'this is string'
}
13.3 类型缩小
类型缩小是TypeScript中一种通过条件判断来限制变量类型的机制,例如使用typeof
、instanceof
或in
运算符。这允许在代码的不同执行路径中,将类型缩小到比初始声明更具体的类型。例如,通过typeof
检查变量是否为特定类型,或者使用in
检查对象是否包含某个属性,从而确定其类型。
类型缩小的方法有很多。如:
typeof
方法缩小:
type IDType = number | string;
function prientID(id: IDType) {
if (typeof id === "string") {
console.log(id.toLocaleUpperCase);
} else {
console.log(id);
}
}
相等性缩小:
可以使用 switch
语句和 ===
、!==
、==
和 !=
等相等性检查来缩小类型。
function example(x: string | number, y: string | boolean) {
if (x === y) {
// We can now call any 'string' method on 'x' or 'y'.
x.toUpperCase();
// (method) String.toUpperCase(): string
y.toLowerCase();
// (method) String.toLowerCase(): string
} else {
console.log(x);
// (parameter) x: string | number
console.log(y);
// (parameter) y: string | boolean
}
}
如上,检查 x
和y
是否相等时,TypeScript知道它们的类型也必须相等。由于string
是x
和 y
都可以采用的唯一通用类型,因此 TypeScript 知道第一个分支中的 x
和 y 必须是 string
,所以调用 string
类型的方法(toUpperCase
)不会报错。这里就用了相等性缩小
instanceof
缩小:
function prientTime(time:string | Date){
if(time instanceof Date){
console.log(time.toUTCString)
}else{
console.log(time)
}
}
`in缩小:
type Fish = { swinming: () => void };
type Dog = { running: () => void };
function walk(animal: Fish | Dog) {
if('swinming' in animal){
animal.swinming
}else{
animal.running
}
}
const fish: Fish = {
swinming() {
console.log("swinming");
},
};
还有其他很多方式就不一一列举。
13.4 类型断言
有些情况下,变量的类型对于我们来说是很明确,但是TS编译器却并不清楚,此时,可以通过类型断言来告诉编译器变量的类型。
类型断言好比其他语言里的类型转换,但是不进行特殊的数据检查和解构。它没有运行时的影响,只是在编译阶段起作用。
TypeScript 类型检测无法做到绝对智能,毕竟程序不能像人一样思考。有时会碰到我们比 TypeScript 更清楚实际类型的情况,比如下面的例子:
const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = arrayNumber.find(num => num > 2); // 爆红
其中,greaterThan2
一定是一个数字(确切地讲是 3
),因为 arrayNumber
中明显有大于 2
的成员,但静态类型对运行时的逻辑无能为力。
在 TypeScript 看来,greaterThan2
的类型既可能是数字,也可能是 undefined
,所以上面的示例中提示了一个 ts(2322)
错误--------此时我们不能把类型 undefined
分配给类型 number
。
不过,我们可以使用一种笃定的方式——类型断言(类似仅作用在类型层面的强制类型转换)告诉 TypeScript 按照我们的方式做类型检查。
比如,我们可以使用 as
语法做类型断言,如下代码所示:
const greaterThan2: number = arrayNumber.find(num => num > 2) as number;
13.5 非空断言
在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符 !
可以用于断言操作对象是非 null
和非 undefined
类型。具体而言,x!
将从 x
值域中排除 null
和 undefined
。
具体看以下示例:
let mayNullOrUndefinedOrString: null | undefined | string ="12";
mayNullOrUndefinedOrString!.toString(); // ok
mayNullOrUndefinedOrString.toString(); // ts(2531)
13.6 确定赋值断言
允许在实例属性和变量声明后面放置一个 !
号,从而告诉 TypeScript 该属性会被明确地赋值。为了更好地理解它的作用,我们来看个具体的例子。
let x: number;
initialize();
// Variable 'x' is used before being assigned.(2454)
console.log(2 * x); // Error
function initialize() {
x = 10;
}
很明显该异常信息是说变量 x
在赋值前被使用了,要解决该问题,我们可以使用确定赋值断言:
//加了'!'
let x!: number;
initialize();
console.log(2 * x); // Ok
function initialize() {
x = 10;
}