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)可以用来手动指定一个值的类型。
语法
值 as 类型
类型断言用途
- 将一个联合类型断言为其中一个类型
- 将任何一个类型断言为
any - 将一个父类断言为更加具体的子类
- 将
any断言为一个具体的类型
非空类型断言操作符 !
- 如果编译器无法消除
null或undefined,则可以使用类型断言运算符手动删除它们。语法是后缀!:identifier!移除了null与undefined从类型identifier:
Js项目如何转成Ts项目?
- 在tsconfig.json设置allowJs: true
type和interface的区别
相同:
- 都支持泛型
不同:
-
interface 可以多次定义,自动合并为一个 ,type 不支持
-
type 能使用 in 关键字生成映射类型,但 interface 不行
type KeyType = {[key in Keys]: string // 类似for...in} -
interface可以被继承,通过extends关键字, type不能使用extends和implements
-
type 可以声明基本类型别名,联合类型,元组等类型
// 基本类型别名type Name = string// 联合类型interface Dog {wong();}interface Cat {miao();}type Pet = Dog | Cat// 具体定义数组每个位置的类型type PetList = [Dog, Pet] -
type 语句中还可以使用 typeof 获取实例的类型
- 无法通过typeof获取interface接口类型
// 当你想获取一个变量的类型时,使用 typeoflet div = document.createElement('div');type B = typeof div -
type可以通过交叉类型实现extends
- type D = A & B & C
- 联合类型 符号
| - 联合类型描述的值可以是几种类型之一
- 交叉类型 符号
& - 交叉类型将多个类型组合为一个类型
ts的never何时使用?
- 表示的是那些永不存在的值的类型 【 never类型是任何类型的子类型,也可以赋值给任何类型】
- never 是指没法正常结束返回的类型,一个必定会报错或者死循环的函数会返回这样的类型。
function fn1(): never {
while(true) { /*...*/ }
}
function fn2(): never {
throw new Error( /*...*/ )
}
例如,如果一个函数抛出一个错误,那么这个函数就可以用never或者void来表示其返回值
function handlerError(message: string): never {
throw new Error(message)
}
function handlerError(message: string): void {
throw new Error(message)
}
never的另外一个知识点是:如果一个联合类型中存在
never,那么实际的联合类型并不会包含never// 定义type test = 'name' | 'age' | never// 实际type test = 'name' | 'age'
extends理解
extends关键字, 通常用于:类型约束和条件类型以及接口继承
类型继承:
// 类型继承【接口继承】
interface T1 {
name: string
}
interface T2 {
sex: number
}
// 多重继承,逗号隔开
interface T3 extends T1,T2 {
age: number
}
条件判断 【条件约束】 extends的条件类型 类似于 js的三元表达式:
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>
类型约束,类型约束经常和泛型一起使用:
// 类型约束,类型约束经常和泛型一起使用:
<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 的意思是,程序员目前不知道 的类型(未知的),所以一般需要在用之前断言一下,才能通过语法检查
场景:
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 增加新的属性
declare global {
interface Window {
csrf: string;
}
}
理解泛型中的 K、T、V
//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 映射类型
如何减少以上类型中的重复代码?
- 可以使用映射类型,它是一种泛型类型,可用于把原有的对象类型映射成新的对象类型
映射类型的语法如下:
{ [ P in K ] : T }
P in K 类似于 JavaScript 中的 for...in 语句,用于遍历 K 类型中的所有类型,而 T 类型变量用于表示 TS 中的任意类型。
- 在映射的过程中,你还可以使用
readonly和 ? 这两个额外的修饰符。通过添加 + 和 - 前缀,来增加和移除对应的修饰符。**如果没有添加任何前缀的话,默认是使用 **+
常见的映射类型语法:
{ [ 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 }
例子:
重复代码:
type User = {
name: string; // 姓名
password: string; // 密码
address: string; // 地址
phone: string; // 联系电话
};
type UserPartial = {
name?: string; // 姓名
password?: string; // 密码
address?: string; // 地址
phone?: string; // 联系电话
};;
简化:
type MyPartial<T> = {
[P in keyof T]?: T[P];
};
type UserPartial = MyPartial<User>;
巧用infer
infer关键词的作用是延时推导,它会在类型未推导时进行占位,等到真正推导成功后,它能准确的返回正确的类型。
-
1、infer用在
extends条件语句中 -
2、表示在
extends条件语句中待推断的类型变量
场景:
- 1、用于提取函数类型的返回值类型
type ReturnType<T> = T extends (...args: any[]) => infer P ? P : any;
type Func = () => User;
type Test = ReturnType<Func>; // Test = User
- 2、用于提取构造函数中参数(实例)类型:
// 获取参数类型
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
子类型 、父类型
interface Animal {
age: number
}
interface Dog extends Animal {
bark(): void
}
在这个例子中,Animal 是 Dog 的父类,Dog是 Animal的子类型,子类型的属性比父类型更多,更具体。
- 在类型系统中,属性更多的类型是子类型。
- 在集合论中,属性更少的集合是子集。
也就是说,子类型是父类型的超集,而父类型是子类型的子集,这是直觉上容易搞混的一点。
记住一个特征,子类型比父类型更加具体,这点很关键。
let animal: Animal
let dog: Dog
animal = dog // ✅ok
dog = animal // ❌error! animal 实例上缺少属性 'bark'
- 子类型是可以赋值给父类型的,也就是
父类型变量 = 子类型变量是安全的,因为子类型上涵盖了父类型所拥有的的一切属性 - 如果父类型赋值给子类型就会有问题,因为父类型的属性较少,父类型可能会缺少子类型的属性,因此会发生类型错误
区分联合类型中的运用
'a' | 'b' | 'c' 是 'a' | 'b' 的子类型吗?它看起来属性更多诶?其实正相反,'a' | 'b' | 'c' 是 'a' | 'b' 的父类型。因为前者比后者更「宽泛」,后者比前者更「具体」。
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属性少的更为具体
- 宽泛(抽象)的为父类型,具体的为子类型
实例化类型
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 类型
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检查结果是类型安全的。这种特性就叫做逆变,函数的参数有逆变的性质。
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剩余参数【形参】可以定位为元组类
function rest(...args: [number, string, boolean]): void
//等价
function rest(arg1: number, arg2: string, arg3: boolean): void
非空断言操作符
在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符
!可以用于断言操作对象是非 null 和非undefined 类型。具体而言,x!将从 x 值域中排除null和undefined。
一、非空断言有啥用
function sayHello(name: string | undefined) {
let sname: string = name; // Error
}
对于以上代码,TypeScript 编译器会提示一下错误信息:
Type 'string | undefined' is not assignable to type 'string'.
Type 'undefined' is not assignable to type 'string'.
要解决上述问题,我们可以简单加个条件判断:
function sayHello(name: string | undefined) {
let sname: string;
if (name) {
sname = name;
}
}
使用这种方案,问题是解决了。但有没有更简单的方式呢?答案是有的,
就是使用 TypeScript 2.0 提供的非空断言操作符:
- 告诉编译器该属性在运行时一定会被赋值,从而避免空值null或未定义undefined的错误。
function sayHello(name: string) {
let sname: string = name!;
}
keyof、typeof 、in操作符理解
-
keyof操作符提取其属性的名称 -
typeof操作符用来获取一个变量或对象的类型 -
keyof和in经常会连在一起使用,当它们连在一起使用时,通常表示一个迭代的过程 -
in操作符的右侧通常跟一个联合类型,可以使用in来迭代这个联合类型,可以理解为for ... in
关键字is的作用
- 关键字is: 判断一个变量属于某个接口|类型
它被称为类型谓词,用来判断一个变量属于某个接口或类型。如果需要封装一个类型判断函数,你应该第一时间想到它,
const isString = (s: unknown): s is string => typeof val === 'string'
三斜线指令
reference 会让编译器在编译过程中添加一些额外引入的文件。
/// <reference path="..." />声明文件间的依赖。/// <reference types="..." />声明了对某个包的依赖- 对这些包的名字的解析与在
import语句里对模块名的解析类似。 可以简单地把三斜线类型引用指令当做import声明的包。 - 相当于导入某个包的依赖
- 对这些包的名字的解析与在
/// <reference types='vitest' />
命名空间
TypeScript 的 namespace 关键字用于创建命名空间(也称为模块),它允许将相关的代码组织在一起,以便于维护和重用。
- 它提供了一种清晰和模块化的代码组织方式,使得代码更易于维护和重用
例子1:
- 声明菜单命名空间,以便在所有文件都能方便使用到
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 的类型注解都是编译时特性
// api.d.ts
// 此全局枚举类型使用会报错
declare enum CodeResult {
Fail = 0,
Success = 1,
}
=====要改为==============
// enum.ts
export enum CodeResult {
Fail = 0,
Success = 1,
}
深入理解d.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组件定义声明类型模块
declare module 'vue-components'
在d.ts类型声明引入其他类型
- TS2.9开始支持使用import()语法引入类型。
declare namespace Express {
interface Request {
user: import("./user").User;
}
}