跳到主要内容

typescript

ts和js的区别​

  • JavaScript是弱类型脚本语言,TypeScrirpt让JavaScript由弱变强
  • TypeScript提供类型定义,更易于构建组件和维护,可以大型应用,用ts更合适,js由于是动态类型语言,对类型非常宽松,在代码执行期间可能会发生类型错误,引发很多bug,而ts是静态类型语言,在编译期间就可以进行类型检查,避免运行时的错误
  • ts有类型提示支持,提高开发效率

如何配置环境使得 JavaScript 项目可以支持 TypeScript 语法?​

  • 1.安装typescript
  • 2.新建typescript的配置文件,tsconfig.json
  • 3.在webpack配置中,配置loader,ts-loader插件,让webpack去处理ts的文件,转换为js,让浏览器支持和识别这些模块

TypeScript配置文件​

  • outDir
    • 输入文件的目录
  • rootDir
    • 指定编译文件的根目录,编译前会在根目录查找入口文件

TypeScript 如何自动生成库包的声明文件?​

  • 1.配置tsconfig文件
    • 在文档中,配置选项,找到.d.ts和声明文件的选项
    • declaration :项目中的每个 TypeScript 文件生成 .d.ts文件
      • .d.ts 文件是描述模块外部 API 的类型定义文件
    • declarationDir : 配置声明文件的目录

什么是TypeScript中的类型断言?​

  • 类型断言(Type Assertion)可以用来手动指定一个值的类型。

语法

text
值 as 类型

类型断言用途​

  • 将一个联合类型断言为其中一个类型
  • 将任何一个类型断言为 any
  • 将一个父类断言为更加具体的子类
  • 将 any 断言为一个具体的类型

非空类型断言操作符 !​

  • 如果编译器无法消除 null或 undefined,则可以使用类型断言运算符手动删除它们。语法是后缀 !:identifier!移除了 null与 undefined从类型 identifier:

Js项目如何转成Ts项目?​

  • 在tsconfig.json设置allowJs: true

type和interface的区别​

相同:

  • 都支持泛型

不同:

  • interface 可以多次定义,自动合并为一个 ,type 不支持

  • type 能使用 in 关键字生成映射类型,但 interface 不行

    js
    type KeyType = {
    [key in Keys]: string // 类似for...in
    }
  • interface可以被继承,通过extends关键字, type不能使用extends和implements

  • type 可以声明基本类型别名,联合类型,元组等类型

    js
    // 基本类型别名
    type Name = string

    // 联合类型
    interface Dog {
    wong();
    }
    interface Cat {
    miao();
    }

    type Pet = Dog | Cat

    // 具体定义数组每个位置的类型
    type PetList = [Dog, Pet]
  • type 语句中还可以使用 typeof 获取实例的类型

    • 无法通过typeof获取interface接口类型
    js
    // 当你想获取一个变量的类型时,使用 typeof
    let div = document.createElement('div');
    type B = typeof div
  • type可以通过交叉类型实现extends

    • type D = A & B & C
    • 联合类型 符号 |
    • 联合类型描述的值可以是几种类型之一
    • 交叉类型 符号 &
    • 交叉类型将多个类型组合为一个类型

ts的never何时使用?​

  • 表示的是那些永不存在的值的类型 【 never类型是任何类型的子类型,也可以赋值给任何类型】
  • never 是指没法正常结束返回的类型,一个必定会报错或者死循环的函数会返回这样的类型。
js
function fn1(): never {
while(true) { /*...*/ }
}

function fn2(): never {
throw new Error( /*...*/ )
}

例如,如果一个函数抛出一个错误,那么这个函数就可以用never或者void来表示其返回值

ts
function handlerError(message: string): never {
throw new Error(message)
}
function handlerError(message: string): void {
throw new Error(message)
}

never的另外一个知识点是:如果一个联合类型中存在never,那么实际的联合类型并不会包含never

ts
// 定义
type test = 'name' | 'age' | never
// 实际
type test = 'name' | 'age'

extends理解​

  • extends关键字, 通常用于:类型约束和条件类型以及接口继承

类型继承:

ts
// 类型继承【接口继承】
interface T1 {
name: string
}

interface T2 {
sex: number
}

// 多重继承,逗号隔开
interface T3 extends T1,T2 {
age: number
}

条件判断 【条件约束】 extends的条件类型 类似于 js的三元表达式:

ts
type res1 = true extends boolean ? true : false // true
type res2 = 'name' extends 'name'|'age' ? true : false // true
type res3 = [1, 2, 3] extends { length: number; } ? true : false // true
type res4 = [1, 2, 3] extends Array<number> ? true : false // true

// 内置工具的 Extract就使用到了, Extract通过extends的条件类型来取交集
// 会把T类型中的每一个子类型进行迭代,会把每次迭代的结果组合成一个新的联合类型
type Extract<T, U> = T extends U ? T : never;
type type1 = 'name'|'age'
type type2 = 'name'|'address'|'sex'

// 结果:'name'
type test = Extract<type1, type2>

类型约束,类型约束经常和泛型一起使用:

ts
// 类型约束,类型约束经常和泛型一起使用:
<T, K extends keyof T> // K 是 T 的子类型
// 比如 type T = "a" | "b" | "c";,那么 K 可以是 "a",也可以是 "a" | "c" 或者 "a" | "b" | "c" 等

巧用string & {} 类型​

  • 参考:microsoft/TypeScript#29729
  • 这是个类型魔法,正常我们写"aa" | "bb"的话,类型只接受aa bb,其他会报错,但是加上& {},就可以支持已有字符串枚举的同时支持扩展字符串

object, {} 类型之间的区别​

因为 {} 和 object 是有区别的,{} 表示所有原始类型和非原始类型的集合,object 表示所有非原始类型的集合

TypeScript 2.2 引入了被称为 object 类型的新类型, 它用于表示非原始类型

any 和 unknown 有什么区别,说说他们的使用场景​

  • any: 表示任意类型,相当于忽略类型检查

  • unknown 不可预知的类型,不会忽略语法检查(这就是最大区别)

    • 如果你不知道预先期望哪种类型,但想稍后分配它, 这时可以使用unknown

    • 需要注意点就是,如果直接访问unknown类型的话,会报错,需要断言后才能访问属性

如果一个变量的类型是变化的(比如来自 JS 程序的对象,随时都有可能多一个属性,甚至变成完全不同的类型),就用 any。

如果一个变量的类型是固定的,但是目前还不能确定或不想确定,就用 unknown。要用这个变量的时候就断言一下吧,不能像 any 那样糊里糊涂地用

  • 我个人的经验是能用 unknown 就尽量用 unknown,不要滥用 any。
  • any 的意思是,认为可以是任何类型,所以any 会忽略所有类型检查
  • unknown 的意思是,程序员目前不知道 的类型(未知的),所以一般需要在用之前断言一下,才能通过语法检查

场景:

js
const testUnknown:unknown = {
name:'test'
}
console.log(testUnknown.name) // 直接访问会报错
console.log((testUnknown as {name:string}).name)
const testAny:any = {
name:'test'
}
console.log(testAny.name)

declare,declare global是什么?​

declare 是用来定义全局变量、全局函数、全局命名空间、js modules、class等 declare global 用来定义全局对象 window 增加新的属性

ts
declare global {
interface Window {
csrf: string;
}
}

理解泛型中的 K、T、V​

js
//T: 泛型变量T表示Type ,实际上 T 可以用任何有效的名称代替, 常用T来表示 , 除了 T 之外,常见的泛型变量还有 K、V 和 E 等
// K 全称 Key 表示对象中键的类型;
// V 全称 Value 表示对象中值的类型;
// E 全称 Element 表示元素类型
function getData1<T>(value:T):T{
return value
}
function getData2<T,U>(value:T,message:U):T{
return value
}

理解TS 映射类型​

如何减少以上类型中的重复代码?

  • 可以使用映射类型,它是一种泛型类型,可用于把原有的对象类型映射成新的对象类型

映射类型的语法如下:

js
{ [ P in K ] : T }

P in K 类似于 JavaScript 中的 for...in 语句,用于遍历 K 类型中的所有类型,而 T 类型变量用于表示 TS 中的任意类型。

  • 在映射的过程中,你还可以使用 readonly 和 ? 这两个额外的修饰符。通过添加 + 和 - 前缀,来增加和移除对应的修饰符。**如果没有添加任何前缀的话,默认是使用 **+

常见的映射类型语法:

js
{ [ P in K ] : T }
{ [ P in K ] ?: T }
{ [ P in K ] -?: T }
{ readonly [ P in K ] : T }
{ readonly [ P in K ] ?: T }
{ -readonly [ P in K ] ?: T }

例子:

重复代码:

js
type User = {
name: string; // 姓名
password: string; // 密码
address: string; // 地址
phone: string; // 联系电话
};
type UserPartial = {
name?: string; // 姓名
password?: string; // 密码
address?: string; // 地址
phone?: string; // 联系电话
};;

简化:

js
type MyPartial<T> = {
[P in keyof T]?: T[P];
};

type UserPartial = MyPartial<User>;

巧用infer​

infer关键词的作用是延时推导,它会在类型未推导时进行占位,等到真正推导成功后,它能准确的返回正确的类型。

  • 1、infer用在extends 条件语句中

  • 2、表示在 extends 条件语句中待推断的类型变量

场景:

  • 1、用于提取函数类型的返回值类型
tsx
type ReturnType<T> = T extends (...args: any[]) => infer P ? P : any;
type Func = () => User;
type Test = ReturnType<Func>; // Test = User
  • 2、用于提取构造函数中参数(实例)类型:
tsx
// 获取参数类型
type ConstructorParameters<T extends new (...args: any[]) => any> = T extends new (...args: infer P) => any
? P
: never;

// 获取实例类型
type InstanceType<T extends new (...args: any[]) => any> = T extends new (...args: any[]) => infer R ? R : any;

class TestClass {
constructor(public name: string, public age: number) {}
}

type Params = ConstructorParameters<typeof TestClass>; // [string, number]

type Instance = InstanceType<typeof TestClass>; // TestClass

子类型 、父类型​

ts
interface Animal {
age: number
}

interface Dog extends Animal {
bark(): void
}

在这个例子中,Animal 是 Dog 的父类,Dog是 Animal的子类型,子类型的属性比父类型更多,更具体。

  • 在类型系统中,属性更多的类型是子类型。
  • 在集合论中,属性更少的集合是子集。

也就是说,子类型是父类型的超集,而父类型是子类型的子集,这是直觉上容易搞混的一点。

记住一个特征,子类型比父类型更加具体,这点很关键。

ts
let animal: Animal
let dog: Dog

animal = dog // ✅ok
dog = animal // ❌error! animal 实例上缺少属性 'bark'
  • 子类型是可以赋值给父类型的,也就是 父类型变量 = 子类型变量 是安全的,因为子类型上涵盖了父类型所拥有的的一切属性
  • 如果父类型赋值给子类型就会有问题,因为父类型的属性较少,父类型可能会缺少子类型的属性,因此会发生类型错误

区分联合类型中的运用​

'a' | 'b' | 'c' 是 'a' | 'b' 的子类型吗?它看起来属性更多诶?其实正相反,'a' | 'b' | 'c' 是 'a' | 'b' 的父类型。因为前者比后者更「宽泛」,后者比前者更「具体」。

ts
type Parent = 'a' | 'b' | 'c'
type Son = 'a' | 'b'

let parent: Parent
let son: Son

parent = son // ✅ok
son = parent // ❌error! parent 有可能是 'c'

这里 son 是可以安全的赋值给 parent 的,因为 son 的所有可能性都被 parent 涵盖了。

而反之则不行,parent 太宽泛了,它有可能是 'c',这是 Son 类型 hold 不住的。

这个例子看完以后,你应该可以理解为什么 'a' | 'b' extends 'a' | 'b' | 'c' 为 true 了,在书写 conditional types的时候更加灵活的运用吧。

总结:

  • 在联合类型中,a | b | c 属性多 的是父类型,因为更宽泛,a| b属性少的更为具体
    • 宽泛(抽象)的为父类型,具体的为子类型

实例化类型​

ts
type PerpleType = new () => Perple;
class Perple {
name: any;
constructor(name) {
this.name = name;
}
}

为什么需要使用严格模式​

严格模式可以消除语法里一些不合理,不严谨的地方,可以让TS往更合理、更安全、更严谨的方向去发展

  • 更加有效保障代码运行的安全
  • 提高编译器效率
  • 增加运行速度
  • 消除语法不合理、不严谨地方

协变、逆变、双变和抗变的理解?​

  • 协变 (Covariant):协变表示 Comp<T>类型兼容和 T的一致。
  • 逆变 (Contravariant):逆变表示 Comp<T>类型兼容和 T相反。
  • 双向协变 (Bivariant):双向协变表示 Comp<T>类型与 T类型双向兼容。
  • 不变 (Invariant):不变表示 Comp<T>类型与 T类型双向都不兼容

协变:

  • 协变 X = Y ,Y 类型可以赋值给 X 类型的情况就叫做协变,也可以说是 X 类型兼容 Y 类型
typescript
interface X { name: string; age: number; }
interface Y { name: string; age: number; hobbies: string[] }
let x: X = { name: 'xiaoming', age: 16 }
let y: Y = { name: 'xiaohong', age: 18, hobbies: ['eat'] }
x = y

逆变

  • 父子类型关系逆转了,这就是 逆变(Contravariance)。

一般地子类型可以赋值给父类型, 由于逆变,父类型可以分配给子类型

  • 类型的位置会影响逆变协变,入参应该为逆变

List <Animal> 可以分配给 List <Dog>

  • printY = printX 函数X 类型可以赋值给函数Y 类型,因为函数Y 在调用的时候参数是按照Y类型进行约束的,但是用到的是函数X的X的属性和方法,ts检查结果是类型安全的。这种特性就叫做逆变,函数的参数有逆变的性质。
typescript
let printY: (y: Y) => void
printY = (y) => { console.log(y.hobbies) }
let printX: (x: X) => void
printX = (x) => { console.log(x.name) }
printY = printX

双变(双向协变)

  • X = Y;Y = X父类型可以赋值给子类型,子类型可以赋值给父类型,既逆变又协变,叫做“双向协变”(ts2.x 之前支持这种赋值,之后 ts 加了一个编译选项 strictFunctionTypes,设置为 true 就只支持函数参数的逆变,设置为 false 则支持双向协变) 抗变(不变):非父子类型之间不会发生型变,只要类型不一样就会报错

Tuple类型理解​

元组类型 是另外一种Array数组类型。一般在数组中,都是由同类型的值组成,在ts的元组类型中,可以存储不同类型的值

场景

1、元组定义数组

2、元组可以作为参数传递给函数,函数的rest剩余参数【形参】可以定位为元组类

ts
function rest(...args: [number, string, boolean]): void
//等价
function rest(arg1: number, arg2: string, arg3: boolean): void

非空断言操作符​

在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符 ! 可以用于断言操作对象是非 null 和非undefined 类型。具体而言,x! 将从 x 值域中排除 null 和 undefined 。

一、非空断言有啥用

javascript
function sayHello(name: string | undefined) {
let sname: string = name; // Error
}

对于以上代码,TypeScript 编译器会提示一下错误信息:

javascript
Type 'string | undefined' is not assignable to type 'string'.
Type 'undefined' is not assignable to type 'string'.

要解决上述问题,我们可以简单加个条件判断:

javascript
function sayHello(name: string | undefined) {
let sname: string;
if (name) {
sname = name;
}
}

使用这种方案,问题是解决了。但有没有更简单的方式呢?答案是有的,

就是使用 TypeScript 2.0 提供的非空断言操作符:

  • 告诉编译器该属性在运行时一定会被赋值,从而避免空值null或未定义undefined的错误。
javascript
function sayHello(name: string) {
let sname: string = name!;
}

keyof、typeof 、in操作符理解​

  • keyof 操作符提取其属性的名称

  • typeof 操作符用来获取一个变量或对象的类型

  • keyof和in经常会连在一起使用,当它们连在一起使用时,通常表示一个迭代的过程

  • in操作符的右侧通常跟一个联合类型,可以使用in来迭代这个联合类型,可以理解为 for ... in

关键字is的作用​

  • 关键字is: 判断一个变量属于某个接口|类型

它被称为类型谓词,用来判断一个变量属于某个接口或类型。如果需要封装一个类型判断函数,你应该第一时间想到它,

js
const isString = (s: unknown): s is string => typeof val === 'string'

三斜线指令​

reference 会让编译器在编译过程中添加一些额外引入的文件。

  • /// <reference path="..." /> 声明文件间的依赖。
  • /// <reference types="..." />声明了对某个包的依赖
    • 对这些包的名字的解析与在 import语句里对模块名的解析类似。 可以简单地把三斜线类型引用指令当做 import声明的包。
    • 相当于导入某个包的依赖
text
/// <reference types='vitest' />

命名空间​

TypeScript 的 namespace 关键字用于创建命名空间(也称为模块),它允许将相关的代码组织在一起,以便于维护和重用。

  • 它提供了一种清晰和模块化的代码组织方式,使得代码更易于维护和重用

例子1:

  • 声明菜单命名空间,以便在所有文件都能方便使用到
ts
declare namespace Menu {
interface MenuOptions {
path: string;
name: string;
component?: string | (() => Promise<unknown>);
redirect?: string;
meta: MetaProps;
children?: MenuOptions[];
}
interface MetaProps {
icon: string;
title: string;
activeMenu?: string;
isLink?: string;
isHide: boolean;
isFull: boolean;
isAffix: boolean;
isKeepAlive: boolean;
}
}

enum枚举​

  • enum枚举在 TypeScript 中具有运行时特性
  • 这与其他 TypeScript 的类型注解(如接口、类型别名等)是不同的,后者在编译时会被转换为相应的 JavaScript 代码,而枚举在编译后会保留其原始的枚举值

对于全局的枚举类型,需要通过导出来使用

注意事项:

  • 注意,如果是声明全局枚举类型通过d.ts声明文件,并在项目中直接使用,会报错,出现该枚举类型未定义
    • 需要改为导出来使用,因为使用了 enum 的运行时特性,而其它 TypeScript 的类型注解都是编译时特性
ts
// api.d.ts
// 此全局枚举类型使用会报错
declare enum CodeResult {
Fail = 0,
Success = 1,
}

=====要改为==============
// enum.ts
export enum CodeResult {
Fail = 0,
Success = 1,
}

深入理解d.ts​

ts
declare var 声明全局变量
declare function 声明全局方法
declare class 声明全局类
declare enum 声明全局枚举类型
declare namespace 声明(含有子属性的)全局对象
interface 和 type 声明全局类型
export 导出变量
export namespace 导出(含有子属性的)对象
export default ES6 默认导出
export = commonjs 导出模块
export as namespace UMD 库声明全局变量
declare global 扩展全局变量
declare module 扩展模块
/// <reference /> 三斜线指令

.d.ts 文件中的顶级声明必须以 “declare” 或 “export” 修饰符开头。

  • 当使用第三方库时,如果这个库内置类型声明文件.d.ts,我们在写代码时可以获得对应的代码补全、类型提示等功能
  • 如果某个库没有内置类型声明文件时,我们使用这个库,不会获得Ts的语法提示,甚至会有类型报错警告
    • 像这种没有内置类型声明文件的,我们就可以自己创建一个xx.d.ts的文件来自己声明,ts会自动读取到这个文件里面的内容的

给第三方库声明类型​

  • 给vue-components组件定义声明类型模块
ts
declare module 'vue-components'

在d.ts类型声明引入其他类型​

  • TS2.9开始支持使用import()语法引入类型。
ts
declare namespace Express {
interface Request {
user: import("./user").User;
}
}