目录
- 介绍
- any 和 unknow
- nerve 的用途
- 断言
- type 和 interface
- declare 关键字的作用
- 联合类型 和 类型守卫
- 交叉类型
介绍
这篇主要介绍下ts 常用的基本类型和一些常用的技巧性技能
any 和 unknow
any 和 unknown 是两个类型关键字,它们用于处理类型不确定或未知的情况。
区别: unknown 只是在接受的传参的时候 忽略检查 当你需要使用的它的属性或者方法的时候 必须给它断言 要不然编译不通过,而any 在接受 和使用 都会去屏蔽 检验。
例子:
场景: 需要定义一个函数给外部使用,但是外部的类型无法确定,要保证所有类型都能接受。但函数内部只对某一些类型处理。这时候就可以使用 any 和 unknow。
使用 any时,下面这段代码ts 不会给出编译错误提示,但实际运行会报错 。因为any 类型 在接受和使用的时候会屏蔽检查
function toFixedTwo(params:any){
return params.toFixed(2)
}
toFixedTwo('100')
使用 unknow时,ts 在函数定义的时候就会给出编译错误提示!
function toFixedTwo(params:unknown){
return params.toFixed(2)
}
结论:unknown 类型要安全得多,因为它迫使我们执行额外的类型检查来对变量执行操作。所以当我们并不知道第三方传过来的字段时,我们用unknow 去接受处理,尽量避免使用any。
最终如下:
function toFixedTwo(params:unknown){
if(typeof params === "number"){
return params.toFixed(2)
}else {
throw new Error("TypeError")
}
}
nerve 的用途
never 是一个特殊的类型声明,它表示一个永不出现的值。never 单独使用的场景比较少,一般在封装工具类型时用的多(这个后续介绍类型体操中会使用的很频繁,运用了never特性。它可以)
下面列几个 never 的例子
function error(message: string): never {
throw new Error(message);
}
function loop(): never {
while (true) {
// do something
}
}
实际开发中的运用:
假如你针对某一个枚举值有处理逻辑
enum TestNeverEnum {
FIRST,
SECOND,
}
function getEnumValue(value: TestNeverEnum):string {
switch (value) {
// 这里 value 被收窄为 TestNeverEnum.FIRST
case TestNeverEnum.FIRST: return "First case";
// 这里 value 被收窄为 TestNeverEnum.SECOND
case TestNeverEnum.SECOND: return "Second case";
// returnValue 是 never 类型
default: const returnValue: never = value; return returnValue;
}
}
因为 case已经处理了全部的情况 所以不会走到default 所以 value 会被推断为 never
后面 假设增加的枚举的类型:
enum TestNeverEnum {
FIRST,
SECOND,
THREE
}
function getEnumValue(value: TestNeverEnum):string {
switch (value) {
// 这里 value 被收窄为 TestNeverEnum.FIRST
case TestNeverEnum.FIRST: return "First case";
// 这里 value 被收窄为 TestNeverEnum.SECOND
case TestNeverEnum.SECOND: return "Second case";
// returnValue 是 never 类型
default: const returnValue:never = value; return returnValue;
}
}
如果你不继续增加case value 的类型就不会是never 就无法被赋值给never 就会编译不过了,可以避免漏写逻辑处理情况的发生。
断言
有时侯 通过内置的api获得的值类型可能 范围很大。但是实际你是知道具体类型的 你就可以使用断言来指定变量的类型。
例子:
场景1:
const myCanvas = document.getElementById("main_canvas")
返回的是 HTMLElement 类型 但是实际你是知道他是一个cavans 容器
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement
就会有对应的代码提示了!
场景2:
你需要遍历某个object 类型时,编译会不通过!
let obj = {
name:'kd',
age:35
}
Object.keys(obj).forEach(item=>{
console.log(obj[item]);
})
实际我们知道这段代码是没有问题的,Object.keys() 方法 返回的是一个 string[] 这时候就需要使用断言来告诉编译器它正确的类型。
有两种解决方法:
- 是缩小item 的类型范围 将 string 类型 变为 obj 所拥有key 的联合类型
let obj = {
name:'kd',
age:35
}
type ObjKeyType = keyof typeof obj
Object.keys(obj).forEach(item=>{
console.log(obj[item as ObjKeyType]);
})
另一种写法:
let obj = {
name: "kd",
age: 35,
};
type ObjKeyType = keyof typeof obj;
(Object.keys(obj) as Array<ObjKeyType>).forEach((item) => {
console.log(obj[item]);
});
2: 去扩大obj 的类型
let obj = {
name: "kd",
age: 35,
};
Object.keys(obj).forEach((item) => {
console.log((obj as Record<string,any>)[item]);
});
总结:断言用于在代码中明确表达对某个条件的假设或要求。它允许你对类型进行额外的检查和限制,以确保代码在运行时的正确性。
type 和 interface
type 和 interface 是用于定义类型的两种不同方式。它们之间有以下区别
- 定义类型范围不同
interface 只能定义对象类型或接口当名字的函数类型
type 可以定义任何类型,包括基础类型 联合类型 交叉类型
interface PeopleType {
age:number
name:string
}
interface Run {
(name:string):void
}
type People = {
age:number
name:string
}
type RunType = (name:string)=>void
type Num = number
type BaseType = string |number|Symbol
type AllType = {name:string}&{age:number}
- interface 可以继承 一个或者多个接口和类。type 无法继承,但是能使用交叉类型来实现部分继承
interface Animal {
name:string
}
interface Size {
long:number
}
interface Cat extends Animal,Size {
jump:(arg:string)=>void
}
let cat :Cat = {
name:'cat',
long:100,
jump(arg) {
console.log(arg);
},
}
- interface 可以合并声明 type 不行
interface People {
name:string,
address:string
}
interface People {
age:number
}
let people: People = {
name:'kd',
address:'usa',
age:35
}
- type还提供了很多的关键字使用,这是interface不具备的 ,比如in extends infer 关键字 (后续再介绍使用)
总结: 选择使用 type 还是 interface 取决于你的需求。如果你需要定义具体的类型并且不希望它被扩展,可以使用 type;如果你需要定义可扩展的接口或具有默认属性的类型,可以使用 interface;
declare 关键字的作用
declare 关键字的重要特点是,它只是通知编译器某个变量是存在的,不用给出具体实现。比如,只描述函数的类型,不给出函数的实现,如果不使用declare,这是做不到的。
场景1: 自己的ts项目使用外部库定义的函数,编译器会因为不知道外部函数的类型定义而报错,这时就可以在自己的脚本里面使用declare关键字,告诉编译器外部函数的类型。
一般来说第三方库都会有对应的d.ts 声明文件,而声明文件中就大量的使用了 declare 来声明类型。
例子:我从外部引入了 一个 js 作用就是 在window 对象上挂载 sayHello 方法,在代码中可以直接使用,但是ts 并不知道 会报错。这时候 declare 关键字就派上用场了。
window.sayHello = function (arg) {
console.log('hello---'), arg;
}
sayHello('hello')
这时候在全局 .d.ts 文件中 使用declare 关键字 声明下
// vite-env.d.ts
declare function sayHello(arg: string): void;
这段代码 就相当于告诉ts 我全局有一个 sayHello 的方法, 编译就通过了 。
场景2: 使用declare关键字声明资源
例如,如果我们想在代码中使用 PNG(直接使用ts 会报找不到模块,因为ts 只认识),我们可以创建如下声明:
declare module '*.png' {
const src: string;
export default src;
}
场景3: 使用声明关键字进行全局增强
注意:在使用 global 全局类型时, ts 不会将d.ts 文件当做一个模块来看,而是一个全局文件,外部文件无法使用 global 下的类型。
//global.d.ts
declare global {
type Type1 = {...}
}
declare type Type2 = {}
//demo.ts
let a:Type1 //error 类型Type1不存在
let b:Type2 //success 因为这个文件中没有任何"import"它是一个全局文件
需要你使用 export 或者 import 关键字 来表明这是一个模块
export {};
declare global {
type Type1 = {...}
}
declare type Type2 = {}
这时候 Type1 可以使用, 但是这时候 Type2 了。因为 这时候 d.ts 文件已经是模块化了,外部无法自己使用 Type2 ,因为 Type1 挂在global 下 所以可以获取。
正常一般推荐 使用 全局类型 全部写到 global下
export {};
declare global {
type Type1 = {...}
type Type2 = {}
}
联合类型 和 类型守卫
在 TypeScript 中,联合类型(Union Types)表示取值可以为多种类型中的一种。使用|分隔每个类型,其格式为:Type1|Type2|Type3。
例如:
type sortStatus = 0|1|2
type codeType = '200'|'400'|'500'
针对对象时使用联合类型时:
使用联合类型的变量 可以接收
1.联合类型中任意一种数据类型全部属性和方法
2.也可以是全量满足其中一种类型,再加上另一个类型的部分属性和方法
type Object1 = {
name:string,age:number,address:string,height:number
}
type Object2 = {
name:string,age:number,run:(arg:string)=>void
}
// 任意一种
let obj1 :Object1|Object2 = {
name:'kd',
age:35,
address:'usa',
height:208
}
let obj2 :Object1|Object2 = {
name:'kd',
age:35,
run(arg) {
console.log(arg);
},
}
// 全量满足其中一种类型,再加上另一个类型的部分属性和方法
let obj3 :Object1|Object2 = {
name:'kd',
age:35,
address:'usa',
run(arg) {
console.log(arg);
},
}
我们可以同时接收 number 和 string 类型,但是再使用的时候,ts 并不知道变量类型是 string 还是 number 。
type Type1 = string|number
const one:Type1 = 'str'
const two:Type1 = 123
function handle(arg:Type1){
arg.length;
arg.toFixed(2)
}
校验过不了,这时候就需要使用类型守卫了。
先介绍下什么是类型守卫?
在 TypeScript 中,类型守卫可以用于在运行时检查变量的类型,
并在代码块内部将变量的类型范围缩小到更具体的类型。
类型守卫通常使用类型断言、类型谓词、typeof 操作符、instanceof 操作符或自定义的谓词函数来判断变量的具体类型,并根据判断结果收窄变量的类型范围。typeof 类型守卫允许使用 typeof 操作符来在代码中根据变量的类型范围进行条件判断。
改造后:
const isString = (arg:any):arg is string=>{
return typeof arg === 'string'
}
function handle(arg:Type1){
if(isString(arg)){
arg.length;
} else {
arg.toFixed(2)
}
}
或者 直接使用typeof
function handle(arg:Type1){
if(typeof arg === 'string'){
arg.length;
} else {
arg.toFixed(2)
}
}
交叉类型
在 TypeScript 中交叉类型是将多个类型合并为一个类型。通过 & 运算符可以将现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。
同名基础类型属性的合并:
interface X {
c: string;
d: string;
}
interface Y {
c: number;
e: string
}
type DD = X & Y
let obj:DD = {
d:'22',
e:'1',
c:''
}
c不能同时为 string 和 number 所以就为 never
同名非基础类型属性的合并:
interface D { d: boolean; }
interface E { e: string; }
interface F { f: number; }
interface A { x: D; }
interface B { x: E; }
interface C { x: F; }
type ABC = A & B & C;
let abc: ABC = {
x: {
d: true,
e: 'semlinker',
f: 666
}
};
在混入多个类型时,若存在相同的成员,且成员类型为非基本数据类型,那么是可以成功合并。
使用场景:
多个类型都有重复相同的字段时, 使用type定义 利用 交叉类型 来实现继承的效果