TypeScript学习
TypeScript在JS的基础上添加了类型系统,所以写TS还是平时JS的写法,只不过写的时候加上类型,文件名要改成.ts。TS没有运行环境,需要编译成JS,用JS运行时运行编译后的JS,因此学习TS,一是学习类型系统,二是学习怎么编译。新建ts-learning目录,目录下新建type.ts,
let a: number = 3; // (变量名: 变量类型) console.log(a)
写TS是不是写JS的时候添加类型?怎么编译?typescript包提供了编译器,不过需要提供配置文件(tsconfig.json),执行tsc命令时,编译器根据配置文件进行编译。npm init -y && npm install typescript -D, npx tsc --init创建默认的tsconfig.json,npx tsc 进行编译,生成了type.js和其他文件,此时用Node.js运行type.js。其他文件暂时不需要,可以在tsconfig.json中,// Other Outputs 下面全部注释。这里需要注意,tsc type.ts(文件名)是使用默认配置,只有单独执行tsc,后面什么都不加,才会使用根目录中的tsconfig.json提供的规则进行编译。
TS的基本类型和JS的基本类型一致,都是number, string, boolean, symbol, undefined和null。但undefined和null在默认情况下,也可以赋值给任意类型,type.ts中
let a: number = null
VS Code中a报错了,鼠标移动到a上,null不能赋值给number类型,这是因为tsconfig中 strict默认配置成true,如果注释掉,就不会报错了。有了类型,null只能赋值给null类型,undefined只能赋给undefined类型,所以始终建议配置"strict": true。VS code怎么报错的?它内置了typescript包,右下角有{},点击,可以看到它内置的版本(5.2.2)

typescript包有tsserver(TypeScript Language Service),写TS代码时,编辑器会时时与它进行通信,实实编译代码(编译到内存中),编译就会进行类型检查,如果不符合类型要求,就会报错,编辑器就会标红提示错误。如果内置的版本和npm install的版本不一致,可以点击Select Version,选择 Use Workspace Version。null字面量是null类型,那3字面量就是number类型,声明变量的时候初始化,TS完全可以推断出变量的类型,就没有必要显示声明变量类型。

尽量让TS去推断类型,只有当TS不能正确推断类型时,才指定类型。对象类型,JS只有Object,但在TS使用鸭式类型(结构化类型),类型是对象的形状,它有哪些属性,属性是什么类型。

所以声明对象类型是声明对象的形状,怎么声明对象类型?可以用type。
type ANumProps = { a: number }
ANumProps类型表明对象有一个number类型的a属性。结构化类型有一个问题,就是它只关心有没有它定义的属性,多了的属性,它不管。
let obj1 = { a: 1, b: 2 }
obj = obj1 // 不会报错
这个无解,只能少用,就当对象只有类型定义的属性。但把对象字面量赋值给有类型的变量时,TS则会严格检查,对象字面量中的属性,既不能多,也不能少,必须正好,否则报错,这叫excess property check
type ANumProps = { a: number } let obj: ANumProps = { a: 1, b: 2 } // 报错,b不存在
对象字面量赋值初始化时,就不要声明类型了,直接类型推断了。其实还有object, Object,{}来表示对象类型,比如 let obj: object ={a: 2},但obj.a就会报错,它仅表示obj是个对象,什么也做不了。Object和{}更没有用,let obj: Object;可以给obj赋任何值,所以几乎不用它们。如果对象有时有某个属性,有时又没有,可使用?: 标示某个属性或许有,有时特殊情况太多,使用索引表达式 [key: 类型]: 类型。 key(可以取任意名字)表示对象的属性,[key: number]: boolean,表示属性是number类型,值是布尔类型。
type objProps = { a: number; b?: string; [key: number]: boolean }
objProps类型表明对象必须有a属性,且是number类型。b属性可能有也可能没有,如果有,它的值可以是undefined。如果还有其属性,它们必须是number类型,且属性值必须是布尔值。
数组更关心数组中的元素是什么类型,因为通常数组中的元素只有一种类型,所以数组类型声明是元素的类型后面跟上[]
let arr: number[] = [2, 3, 4, 5]; // arr数组中每一个元素都是number类型
数组类型还有一个子类叫元组类型,直接指定数组中的每一个元素的类型,也就就决定了数组的长度,所以元组就是一个元素类型固定且长度固定的数组
let turple: [number, string] = [2, 'name']; // turple是2个元素的数组,第一个元素的类型是number,第二个元素的类型是string。
函数则更关心它的参数类型和返回值。
function sum(a: number, b: number): number { // 参数a,b都是number类型,参数列表()后面的:number表示返回值类型,是number类型。
return a + b;
}
function print(a?: string): void { // a参数可传,可不传;函数没有返回值, 返回值类型是void
console.log(a);
}
const substract = (a: number, b: number) => a - b; // 如果TS能推断出函数的返回值类型,返回值类型可以省略。
把鼠标放到substract上,
![]()
TS推断出来了函数的类型是(a: number, b: number ) => number,=> 前面是参数列表,后面是返回值类型。这也是定义函数类型的一种方式,更常用于函数是参数,或函数作为返回值。
type CalNum = (a: number, b: number ) => number; /* 或 type CalNum = { (a: number, b: number): number } 因为在JS中函数是一个对象,声明一个函数,也是声明一个对象,可以用对象字面量来定义类型。 */ const substract: CalNum = (a, b) => a - b;
如果参数没有定义类型?
function sum(a, b) {
return a + b;
}
报错了,鼠标移动到a上,a被推断成是any类型,这叫隐式any,因为没有写any,变量却是any类型。any类型可以给它赋任何值,也可以把它赋值给任何类型,跳过了TS的类型检查,尽量不要用any类型,参数确实是any类型,要显示地标示出来。tsconfig.json配置strict: true,就禁用隐式any。比any类型稍微好一点的是unknown类型。任何类型的值都可以赋给unknown类型,但如果要使用unknown类型,使用之前一定缩小它的类型范围(narrow type)。缩小类型范围的方法有很多,比如typeof
let b: unknown; b = 3; if (typeof b === 'string'){ console.log(b.toUpperCase()); }
联合类型:两个或多个类型使用 | 联合在一起,形成一个新的类型。
type StrOrNum = string | number; // StrOrNum 或是string类型,或是number类型。 type threeNumber = 1 | 2 | 3; // 只能取1,2,3。1,2,3成了类型,称为字面量类型 type Cat = {name: string, purrs: boolean} type Dog = {name: string, barks: boolean, wags: boolean} type CatOrDogOrBoth = Cat | Dog
由于或是这个类型,或是那个类型,给联合类型变量赋值时,可以赋给它联合的任何一个类型,或这些类型的结合体
let c:CatOrDogOrBoth = {name: 'cat', purrs: false } // Cat类型
let d:CatOrDogOrBoth = {name: 'dog', barks: false, wags: false } // dog类型
let cdBoth: CatOrDogOrBoth = { name: 'both', barks: true, purrs: true, wags: true}
赋值时有多爽,使用时就多难受。类型的不确定性,只能使用联合类型中所有类型的共性(c.name),或做判断(类型收窄),如果是某个类型时做什么。类型收窄除了typeof 还有类型断言和类型守卫。类型断言是,直接告诉编译器值是什么类型,使用as。
(cdBoth as Cat).purrs
对象类型守卫,一个是可以使用in操作符来检查属性,一个是类型判断函数
function isDog(testObj: any): testObj is Dog { // 返回值是testObj is Dog,函数参数是什么类型 return testObj.barks !== undefined; } if(isDog(obj)){}
类型联合时要注意,如果两个类型中有相同的属性,但属性类型不同,属性的两个类型也进行联合(union)
type Product = { id: number, name: string };
type Person = { id: string, name: string };
// UnionType 实际上是 { id: number | string, name: string } 类型
type UnionType = Product | Person
在UnionType中,id属性的类型是联合类型 number | string ,就是因为id在Product中是number 类型,但是在Person类型中是string类型。name属性在两个类型中都是string, 所以在新的联合类型中,name属性也是string
type insertection(交叉类型):两个或多个类型使用 & 联合在一起,形成一个新的类型,既是这个类型,又是那个类型
type Person = { id: string; name: string; city: string; }
type Employee = { company: string; dept: string }
// PersonAndEmpoyee实际上是 { id: string; name: string; city: string; company: string; dept: string } 类型,把所有交叉类型的属性全部放到一起
type PersonAndEmpoyee = Person & Employee
// 既是这个类型,又是那个类型,所以给一个交叉类型赋值时,值必须包含所有属性
let bob: Person & Employee = { id: "bsmith", name: "Bob", city: "London", company: "Acme Co", dept: "Sales" };
当要交叉类型有相同的属性,但类型不同时,这个属性的类型,也是不同类型进行交叉,分别给Person和Employee 定义一个contact属性,类型不同
type Person = { id: string; contact: number; }
type Employee = { id: string; contact: string; }
// PersonAndEmpoyee实际上是 { id: string; contact: number & string; }
type PersonAndEmpoyee = Person & Employee
这就出现问题了,没有一个值,它既是number 又是string。解决办法,就是相同的属性不要使用原始类型,要使用对象
type Person = { id: string; contact: {phone: number}; }
type Employee = { id: string; contact: {name: string} }
// PersonAndEmpoyee实际上是 { id: string; contact: {phone: number} & {name: string}; }
type PersonAndEmpoyee = Person & Employee
泛型:函数在定义的时候,并不确定它能应用到的准确类型,只有使用的时候才确定准确类型,就先用一个类型的占位符。主要起到类型限定的作用,确保函数的参数和返回值存在关系,起到一定的关联关系
function firstElement<T>(arr: T[]): T { return arr[0]!; } const numbers = [1, 2, 3]; const firstNum = firstElement<number>(numbers); // Type: number const words = ["hello", "world"]; // 调用函数的时候,能通过参数推断出T的类型,可以直接调用函数 const firstWord = firstElement(words); // Type: string
T虽然不是准确的类型,也是代表着一种类型。另外一种声明函数类型的方式
type funct1 = { <T>(name: T): T } type funct2<T = number> = { // 默认类型 (name: T): T }
当泛型用到类上,是参数化的类型,声明类型时,可以带参数,类型也可以定制化,成立一个类型生产工厂。真正使用泛型时,传递真正类型参数,确定类型。
class DataCollection<T> { //类型 DataCollection带有参数T protected items: T[] = []; constructor(initialItems: T[]) { this.items.push(...initialItems); } filter(predicate: (target: T) => boolean) { return this.items.filter(item => predicate(item)); } } type Person = { id: number; phone: number; } const people: Person[] = [{id: 1, phone: 123}, {id: 2, phone: 456}] let data = new DataCollection<Person>([...people]); /** 使用泛型时,传递真正的类型Person,泛型中的T被替换成Person * 相当于 * class DataCollection { protected items: Person[] = []; constructor(initialItems: Person[]) { this.items.push(...initialItems); } filter(predicate: (target: Person) => boolean) { return this.items.filter(item => predicate(item)); } } */ let filteredProducts = data.filter(data => data.id > 1)
extends是限制传递给泛型类的参数的类型。class DataCollection<T extends Person>,使用DataCollection时,传递的类型参数必须是Person类或和Person结构相同的类(鸭式类型)。
操作类型的小工具
typeof 返回一个变量的类型,当用到复杂类型时,它返回的是鸭式类型。
const button = ['p', 's', 'd'] // button的类型是 string[] const butonTypes = ['p', 's', 'd'] as const; // as const表示数组不会变,类型是 readonly['a', 'b', 'c'] // typeof butonTypes是数组类型,要取出数组的每一个元素作为类型,加number, number表示对数组进行遍历,取出每一个值作为类型。 type Button = (typeof butonTypes)[number] // Button的类型是‘p’|'s'|'d' // 如果把number改成1,就取第一个元素作为类型 // type Button = (typeof butonTypes)[1] // Button的类型是's' const obj = { name: "c", age: 3 } // 后面也可以加 as const type ObjType = typeof obj; // ObjType的类型是 {name: string, age: string}
keyof返回一个联合类型,联合的是keyof操作的对象类型的所有key名。当有一个key时,类型名称[key名]可以获取到这个key在某个类型中定义的类型。
type Keys = keyof ObjType // "name" | "age" type NameType = ObjType['name'] // string type AllTypes = ObjType[Keys] // number | string
type Person = { name: string; age: number; }
// rl 的类型 时 { readonly name: string; readonly age: number; }
type rl = Readonly<Person>
Readonly的实现方式
type Readonly<T> = { readonly [P in keyof T]: T[P]; // [P in keyof T] 是forEach 迭代, p 取联合类型中的每一个类型 进行迭代 };
Partial和Required,一个是把类型中的key都变成可选,一个是把类型中key限定为必选
type User = { id: number; email?: string; }
type UserUpdate = Partial<User>; // type UserUpdate = { id?: number; email?: string; };
type UserInsert = Required<User> // type UserInsert = { id: number; email: string; }
Pick取一个类型中的某些属性,组成一个新的类型,omit 则是省略类型中某些属性,
type PersonOnlyEmail= Pick<User, 'email' >; // type PersonOnlyEmail = { email?: string; } type PersonOnlyId= Omit<User, 'email' >; // type PersonOnlyId = { id: number; }
Exclude排除联合类型中的某些类型,Nonullable 去掉联合类型中的null 或undefined
type NoA = Exclude<'a'| 'b'| 'c', 'a'> // type NoA = b'| 'c' type NonNullableString = NonNullable<string | null | undefined>; // type NonNullableString = string
Parameters:接受一个函数类型,返回参数的类型,是个数组类型,ReturnType 则是返回函数类型的返回值类型
function greet(name: string, age: number): string { return `Hello, ${name}! You are ${age} years old.`; } type MyFunctionParams = Parameters<typeof greet>; // type MyFunctionParams = [name: string, age: number] type MyFunctionReturnType = ReturnType<typeof greet>; // type MyFunctionParams = string
编译选项
target指定编译到哪个JS版本,同时也指定了能使用的JS功能,因为JS版本除了规定语法外,还会新增功能,比如ES2015新增Map,ES5中就没有,如果把target设为ES5,在TS中使用Map就会报错。如果非要使用不存在的功能,就要指定lib,告诉TS,这个功能在运行时是有的,你就不要报错了。
"lib": ["es2015.collection"]
ES5怎么有Map?提供polyfill。tsc只是编译语法,比如把箭头函数()=>null转换成普函数function(){return null},但它不会新增功能,编译成功后,Map会原封不动的存在。如果写了lib,就一定要提供polyfill,否则代码部署后就会报错,在IE9下使用Map一定报错。
module指定编译后的文件使用哪种模块化方式。moduleResolution: 解析查找模块的算法,官网https://www.typescriptlang.org/docs/handbook/modules/guides/choosing-compiler-options.html给了建议,如果使用构建工具,就用(I’m using a bundler)的配置
{ "compilerOptions": { // module-related 设置. // 必须配置 "module": "esnext", "moduleResolution": "bundler", "esModuleInterop": true, // 建议参照构建工具的官网进行配置 "customConditions": ["module"], // 建议 "noEmit": true, // or `emitDeclarationOnly` "allowImportingTsExtensions": true, "allowArbitraryExtensions": true, "verbatimModuleSyntax": true, // or `isolatedModules` } }
如果只是进行Node.js开发,
{ "compilerOptions": { // 必须配置 "module": "nodenext", // `"module": "nodenext"`: 意味着以下配置 // "moduleResolution": "nodenext", // "esModuleInterop": true, // "target": "esnext", // 建议配置 "verbatimModuleSyntax": true, } }
如果想在.ts文件中使用ES模块,要么文件名使用.mts,要么在package.json设置 "type": "module",
esModuleInterop主要设置import引入CommonJS 模块的语法。CommonJs 导出API的方式有两种,exports.sum ={} 和module.exports=sum. 当以exports.sum的方式导出时,ts可以 import {sam} from, 命名方式引入,但当以module.exports=sum导出时,只能使用import * as sum from。配置esModuleInterop: true, 可以使用default方式导入,import sum from
extends选项,去继承另外一个配置文件,比如npm install @tsconfig/node16, 配置文件就可以写么写
{ “extends”: "@tsconfig/node16/tsconfig.json", "include": ["src/**/*"] }
此时仍然可以写compilierOptions, 它会和extends里面的配置项合并,
{ “extends”: "@tsconfig/node16/tsconfig.json", “compilerOptions”: {"outDir": "dist"} "include": ["src/**/*"] }
declaration: 为每一个typescript文件生成 .d.ts的类型文件。
类型声明文件
如果文件中使用全局变量,比如$(), 声明文件用 declare function $(a: number): number.
export declare const Sizzle: SizzleStatic; export declare function findNodes(node: ts.Node): ts.Node[]
类型声明文件告诉tsc,如果在TS代码中看到Sizzle或findNodes(),不要紧张。在运行时,应用程序将包含具有这些类型的JS代码(使用包含 const Sizzle和 findNodes()的JS库)。此类声明称为环境声明 - 这就是您告诉编译器相关变量将在运行时存在的方式。类型声明文件告诉TS,这些东西(变量/函数等)已经在JS中存在(定义)了,只是现在描述给你。在类型声明中,top-level的值需要 declare关键字(declare let, declare function, declare class 等),top-level 的interface和type不用。
但如果TS文件引入的是第三方JS模块,如果已经有了类型声明,比如@type/react,就安装上,如果没有,你也可以不写,直接在import模块的地方 //@ts-ignore

对整个引入的模块进行类型文件声明, 使用declare module
declare module 'module-name' {
export type Mytype = number;
export type MyDefaultType = {a: string};
export let myExport: Mytype;
let myDefaultExport: MyDefaultType;
export default myDefaultExport
}
模块的名称('module-name') 要和import from的路径一致,就能告诉TS模块的API
import ModlueName from 'module-name'
ModlueName.a // string
放到项目的哪里都可以。如下

在index.ts中 import foo模块,types.d.ts中foo模块声明就会使用。这是由于TS查找第三方JS模块的类型声明的算法决定。它会先查找本地的类型声明文件有没有对模块的声明,如果有就用,如果没有,就看模块package.json中有没有定义types(指向类型声明文件),如果还是没有,就看node_modules中@types有没有模块声明,比如@types/react。还是没有,再查找js的同级目录,如果没有,就是any了。你可能看到过本地声明文件中,declare module module 名字,比如 declare module 'react',什么都没做,它只是告诉TS有一个模块,但没有类型信息。模块以及模块export的内容都是any。

浙公网安备 33010602011771号