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使用鸭式类型(结构化类型),类型是对象的形状,它有哪些属性,属性是什么类型。

image

   所以声明对象类型是声明对象的形状,怎么声明对象类型?可以用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
  Readonly 将一个类型转换成只读类型
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。

 

posted @ 2021-09-30 18:09  SamWeb  阅读(453)  评论(0)    收藏  举报