TS每一块其实都值得深挖,此篇意在总结高频应用场景!顺序不分先后!
1、定义简单约束
场景:定义一个变量的类型约束
//常规定义exportinterfacePerson{
name:string;
age:number;}//name和age必填,其他属性开放式exportinterfacePerson{
name:string;
age:number;[key:string]:string|number;}//带限制条件的(慎用)exportinterfacePerson{
name:string;
age?:number;//可选readonly sex:string;//只读}
type也可以实现上述,还可以定义联合类型
//定义联合类型typealias='source'|'math'|'english';//承接类型;typealias1=typeof obj;// typeof 将对象转换为类型typealias2=keyof Iobj;// 将类型的健,提取为联合类型//定义元组类型(基本用不到)typealias3=[string,number]
提取一个对象的健作为一个类型的key
constA1={
age:100,
name:'Alince'}// in 用于判断 前者是否属于后者typeA2={[KinkeyoftypeofA1]:string}
巧用Record,Record<key,value> 可以直观的定义对象的key-value健值对
let obj: Record<string,number[]>={
a:[1,2],
b:[3,4]}
总结官方推荐使用 interface,其他无法满足需求的情况下用 type。很简单,表达一些复杂约束,用type更加灵活。
2、类型工具
Partial (常用)
将类型全部变为可选
//源码typePartial<T>={[PinkeyofT]?:T[P];};//用法interfacePerson{
name:number;
age:string;}//这里的newPerson 所有的属性变成了可选typenewPerson= Partial<Person>;
Required 变为不可选
//与此相对的是 Required<T> 全变为必填typeRequired<T>={[PinkeyofT]-?:T[P];};
Record(高频)
定义对象的健值类型,Record<健, 值>
/**
* Construct a type with a set of properties K of type T
*/typeRecord<Kextendskeyofany,T>={[PinK]:T;};//示例:let obj: Record<string,number[]>={
a:[1,2],
b:[3,4]}
Pick
挑选 一个当前类型的key,作为新类型
/**
* From T, pick a set of properties whose keys are in the union K
*/typePick<T,KextendskeyofT>={[PinK]:T[P];};
Exclude(常用)
排除类型 Exclude<类型, 要排除类型> ,常用语联合类型
/**
* Exclude from T those types that are assignable to U
*/typeExclude<T,U>=TextendsU?never:T;//用法typenum= Exclude<'1'|'2'|'3','1'>// '2' | '3'
Readonly(常用)
让所有的属性都变为只读
/**
* Make all properties in T readonly
*/typeReadonly<T>={readonly[PinkeyofT]:T[P];};
Omit (常用)
删除 属性中的key
/**
* Construct a type with the properties of T except for those in type K.
*/typeOmit<T,Kextendskeyofany>= Pick<T, Exclude<keyofT,K>>;//示例interfacePerson{
name:number;
age:string;}typedel= Omit<Person,'name'>//{ age: string;} name已被删除
Ï
// 新增的话 可以用extends去继承新类型,type 用&
NonNullable
排除联合类型的null 和undefined
/**
* Exclude null and undefined from T
*/typeNonNullable<T>=T&{};//示例typedel= NonNullable<'a'|null|undefined|'b'>// 'a' | 'b'
Parameters
获取函数参数类型 作为元组
/**
* Obtain the parameters of a function type in a tuple
*/typeParameters<Textends(...args:any)=>any>=Textends(...args:inferP)=>any?P:never;//示例functiontest(a:string,b:number){return{
a,b
}}typetesttype= Parameters<typeof test>// type testtype = [a: string, b: number]// 获取的是 类型值typetesttype1= Parameters<typeof test>[1]// type testtype1 = number
ReturnType
获取函数返回值类型
/**
* Obtain the return type of a function type
*/typeReturnType<Textends(...args:any)=>any>=Textends(...args:any)=>inferR?R:any;interfacetypeb{
name:string,
age:number,}let person:typeb ={
name:'xiaoming',
age:12}functiontest(person:typeb){return person
}typetesttype2= ReturnType<typeof test>// type testtype2 = typeb //真够奇葩的,还有这种的
总结正所谓,使用TS轻则伤筋动骨,重则半生不遂,一定慎重使用!
3、其他好用的功能
enum
定义顺序常量,提高代码的可靠性和可维护性
enumDRECTOR{LEFT,RIGHT,CENTER,TOP,BOTTOM}let directiion =DRECTOR.LEFT;
类型断言
as
基本用法
const foo ={};
foo.bar =123;// Error: 'bar' 属性不存在于 ‘{}’
foo.bas ='hello';// Error: 'bas' 属性不存在于 '{}'//可以用as作提前推断interfacePerson{
name:string;
age:number;}let foo ={}as Person;
foo.name ='alince';// ✅
foo.age =20;// ✅
将一个联合类型断言为其中一个类型
interfaceCat{
name:string;run():void;}interfaceFish{
name:string;swim():void;}/*
我们需要在还不确定类型的时候就访问其中一个类型特有的属性或方法,比如
*/functionisFish(animal: Cat | Fish){// 获取 animal.swim 的时候会报错if(typeof animal.swim ==='function'){returntrue;}returnfalse;}/*
此时可以使用类型断言,将 animal 断言成 Fish,从而解决报错
*/functionisFish(animal: Cat | Fish){if(typeof(animal as Fish).swim ==='function'){returntrue;}returnfalse;}
总之,就是将导致爆红的,模糊不定的类型,确定为符合编译的类型。注意:这样骗过了编译器,会增加运行时出现bug的几率。
as const
如果没有声明变量类型,let 命令声明的变量,会被类型推断为 TypeScript 内置的基本类型之一;
const 命令声明的变量,则被推断为值类型常量。
// 类型推断为基本类型 stringlet s1 ='JavaScript';// 类型推断为字符串 “JavaScript”const s2 ='JavaScript';
有时候,会有意想不到的错误
let s ='JavaScript';typeLang=|'JavaScript'|'TypeScript'|'Python';functionsetLang(language:Lang){/* ... */}setLang(s);// 报错 类型“string”的参数不能赋给类型“Lang”的参数。//这时候需要就会需要 as const出场了 // 改为 let s = 'JavaScript' as const; 即可!
常用示例:对象断言,加as const 收缩类型
const router ={
home:'/',
admin:'/admin',
user:'/user'}asconst// 加了as const 让类型系统知道这个对象是常量,类型范围变窄了//'const' 断言只能作用于枚举成员、字符串、数字、布尔值、数组或对象字面量。// let router2 = router as const //报错const goToRoute =(r:'/'|'/admin'|'/user')=>{}goToRoute(router.admin)
枚举断言
enum Foo {X,Y,}let e1 = Foo.X;// Foolet e2 = Foo.Xasconst;// Foo.X
上面示例中,如果不使用
as const
断言,变量
e1
的类型被推断为整个 Enum 类型;
使用了as const断言以后,变量e2的类型被推断为 Enum 的某个成员,这意味着它不能变更为其他成员。
!非空断言
慎用
//举例constf(x?:number|null)=>{validateNumber(x);// 自定义函数,确保 x 是数值console.log(x!.toFixed());}constvalidateNumber(e?:number|null)=>{if(typeof e !=='number')thrownewError('Not a number');}
强调:一定要确保传入的值不是空,才可以使用 !。非空断言会造成安全隐患。
案例:之前有个国际化初始化bug,排查了一下午,到最后原来是同事对于可能是null的变量,使用了非空断言,导致初始化时,一定概率的国际化语言设置失败!
4、extends
继承
interface Person {
name: string;
age: number;
}
interface Person2 extends Person {
content: string;
}
let content: Person2 = {
name: 'Alince',
age: 20,
content: ''
}
判断
A1,A2两个接口,满足A2的接口一定可以满足A1,所以条件为真,A的类型取string
// 示例2interfaceA1{
name:string}interfaceA2{
name:string
age:number}// A的类型为stringtypeA=A2extendsA1?string:number//A2的接口是否满足A1 stringconst a:A='this is string'
typeA1='x'extends'x'?string:number;// stringtypeA2='x'|'y'extends'x'?string:number;// number 前者类型是否满足后者typeA3='x'extends'x'|'y'?string:number;//string 前者类型是否满足后者
到此,extends用法平平无奇~
泛型分配律
注意:不常用,但面试会问
当作为泛型传入的时候
typeP<T>=Textends'x'?string:number;typeA3=P<'x'|'y'>// ? 猜猜看
这里直接给结论~
typeP<T>=Textends'x'?string:number;typeA3=P<'x'|'y'>// A3的类型是 string | number
why?
如果传入泛型,就会根据分配律进行判断
该例中,extends的前参为T,T是一个泛型参数。在A3的定义中,给T传入的是’x’和’y’的联合类型
'x' | 'y'
,满足分配律,于是’x’和’y’被拆开,分别代入
P<T>
P<‘x’ | ‘y’> => P<‘x’> | P<‘y’>
'x’代入得到
'x' extends 'x' ? string : number => string
'y’代入得到
'y' extends 'x' ? string : number => number
然后将每一项代入得到的结果联合起来,得到
string | number
总之,满足两个要点即可适用分配律:第一,参数是泛型类型,第二,代入参数的是联合类型
特殊的never
注:如果要写基础ts定义的,这里需要熟悉。对于日常开发太偏了
// never是所有类型的子类型typeA1=neverextends'x'?string:number;// stringtypeP<T>=Textends'x'?string:number;typeA2=P<never>// never
wc,居然不一样?
实际上,还是走条件分配在起作用。never被认为是空的联合类型。也就是说,没有联合项的联合类型,所以还是满足上面的分配律,然而因为没有联合项可以分配,所以
P<T>
的表达式其实根本就没有执行,所以A2的定义也就类似于永远没有返回的函数一样,是never类型的。
5、泛型
约束返回值
场景:常用于约束 接口返回值。
/**
* 通用返回值
*/exportinterfaceResult<T=unknown>{
code:number;
data:T;
msg:string;}/**
* 用户列表返回值
*/exportinterfaceResultData<T=unknown>{
list:T[];
page:{
pageNum:number|0;
pageSize:number|0;
total:number|0;};}//使用interfaceIUser{
id:number;
name:string;}typeA1= Result<IUser>typeA2= ResultData<IUser>
约束普通函数
场景:一个函数,不知道需要传什么类型,传入的类型且于函数其他类型有关联
//写法functionid<T>(arg:T):T{return arg;}let myId:<T>(arg:T)=>T= id;let myId:{<T>(arg:T):T}= id;//应用 个人觉得知道这个语法就行,实际应用有待商榷。functiongetFirst<T>(arr:T[]):T{return arr[0];}
6、命名空间
场景:面对复杂的类型定义,将类型分组,是不错的选择
例如:
/** 面板模块 */exportnamespace Dashboard {exportinterfaceReportData{
driverCount:number;
totalMoney:number;
orderCount:number;
cityNum:number;}exportinterfaceLineData{
label:string[];
order:number[];
money:number[];}}/** 用户管理模块 */exportnamespace User {exportinterfaceUserItem{
userId:number;
deptId:string;
userName:string;
userEmail:string;
state:number;
mobile:string;
job:string;
role:number;
roleList:string;
createId:number;
deptName:string;
userImg:string;}/**
* 分页数据请求参数
*/exportinterfaceParamsextendsPageParams{
userId?:number;
userName?:string;
state?:number;}}//使用。鼠标悬停可以非常清楚的展示const[report, setReport]=useState<Dashboard.ReportData>();
7、 declare
declare 关键字的重要特点是,它只是通知编译器某个类型是存在的,不用给出具体实现。
比如,只描述函数的类型,不给出函数的实现,如果不使用
declare
,这是做不到的。这样的话,编译单个脚本就不会因为使用了外部类型而报错。
declare可以描述: const let var type interface class enum 函数function 模块module 命名空间namespace等
定义简单类型
//xx.d.ts
declare let x:number;
注意,declare 关键字只用来给出类型描述,是纯的类型代码,不允许设置变量的初始值,即不能涉及值。
// 报错
declare let x:number = 1;
如果想把变量、函数、类组织在一起,可以将 declare 与 module 或 namespace 一起使用。
declare namespace AnimalLib {
class Animal {
constructor(name:string);
eat():void;
sleep():void;
}
type Animals = 'Fish' | 'Dog';
}
// 或者
declare module AnimalLib {
class Animal {
constructor(name:string);
eat(): void;
sleep(): void;
}
type Animals = 'Fish' | 'Dog';
}
//标注:declare module 和 declare namespace 里面,加不加 export 关键字都可以。
declare namespace Foo {
export var a: boolean;
}
declare module 'io' {
export function readFile(filename:string):string;
}
例如
场景1: vue3初始化 ,将.vue结尾的识别为组件
//用于声明导出的vue后缀结尾的是组件
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
场景2:当前脚本使用
myLib
这个外部库,它有方法
makeGreeting()
和属性
numberOfGreetings
。
let result = myLib.makeGreeting('你好');
console.log('欢迎词:' + result);
let count = myLib.numberOfGreetings;
myLib
的类型描述就可以这样写。
declare namespace myLib {
function makeGreeting(s:string):string;
let numberOfGreetings:number;
}
场景3:第三方模块,例如模块联邦导出,原始作者可能没有提供接口类型,这时可以在自己的脚本顶部加上下面一行命令
declare module "模块名";
// 例子
declare module "hot-new-module";
场景4:如果要为 JavaScript 引擎的原生对象添加属性和方法,可以使用
declare global {}
语法。
export {};
declare global {
interface String {
toSmallString(): string;
}
}
//使用
String.prototype.toSmallString = ():string => {
// 具体实现
return '';
};
场景4: 声明css文件,图片格式的定义
declare module '*.png' {
const src: string;
export default src;
}
declare module '*.webp' {
const src: string;
export default src;
}
declare module '*.module.scss' {
const classes: { readonly [key: string]: string };
export default classes;
}
declare module '*.module.sass' {
const classes: { readonly [key: string]: string };
export default classes;
8、/// 三斜杠
/// 就是三斜杠
如果类型声明文件的内容非常多,可以拆分成多个文件,然后入口文件使用三斜杠命令,加载其他拆分后的文件。
有如下类型
_///_ <reference path="" />
是最常见的三斜杠命令,告诉编译器在编译时需要包括的文件,常用来声明当前脚本依赖的类型文件。
path
参数指定了所引入文件的路径
/// <reference path="./lib.ts" />
let count = add(1, 2);
_///_ <reference types="" />
types
参数用来告诉编译器当前脚本依赖某个 DefinitelyTyped 类型库,通常安装在
node_modules/@types
目录。
_///_ <reference lib="" />
lib
属性的值,允许脚本文件显式包含内置 lib 库,等同于在
tsconfig.json
文件里面使用
lib
属性指定 lib 库。里面的
lib
属性的值就是库文件名的
description
部分,比如
lib="es2015"
就表示加载库文件
lib.es2015.d.ts
。
/// <reference lib="es2017.string" />
//上面示例中,es2017.string对应的库文件就是lib.es2017.string.d.ts。
注意:它只能用在文件的头部,如果用在其他地方,会被当作普通的注释。/// 前面只能有单行注释,多行注释,和其他 ///
9、配套的可选链,注释?
原文链接:Javascript + Typescript 特殊运算符号_typscript双感叹号-CSDN博客
??
针对于异常数据的处理
console.log(null??'deault');// "deault"console.log(undefined??'deault');// "deault"console.log(false??'deault');// false 注意console.log(NaN??'deault');// NaN 注意console.log(""??'deault');// "" 注意console.log(0??'deault');// 0 注意
可选链 ?.
用作调用对象的属性,不至于报错
let grade1 ={
data:{},
resp:'success',
code:1000}// 使用了可选链式运算符之后直接写,如果在其中一层没找到会自动赋值为undefined,不再向 后查找,避免了使用if进行一层层判断。console.log(grade1.data?.productList?.name);// undefined
取整 ~~
~~ 不四舍五入,抹掉零头式取整
console.log(~~10.8);// 10console.log(~~10.3);// 10console.log(~~-5.9);// -5
取中间值 >>
// 求(2,7)的中间值console.log(Math.floor(2+(7-2)/2));// 4console.log(Math.floor((2+7)/2));// 4console.log((2+7)>>1);// 4
双感叹号 !!
通常使用双感叹号来将一个空状态强制转换为boolean类型
constformateData=(data:Array<string>)=>{//TO DO... 可能返回undefinedreturn data.length >0?true:null}const result =(res:Array<string>):boolean=>{returnformateData(res)!!}
!非空断言(慎用)
let name:string|null="Tom";// 避免了编译器的空值检查,但是生成的js文件中还是name.length,如果此时name为null,那么就会出现运行时异常console.log(name!.length);functiongreet(name:string):string{return`Hello, ${name}`;}const name2:string|null="Tom";console.log(greet(name2!));// 告诉编译器此变量不会为null或undefined
+ 转number
如果你确定字符串内容是一个有效的数字,并且你想要一个数字类型的结果可以:
let str ="123";let num =parseInt(str);// 转换为整数123// 如果需要浮点数,可以使用parseFloatlet floatNum =parseFloat(str);// 转换为浮点数123.0//更简洁的做法let str ="123";let num =+str;// 转换为数字123
10、tsconfg.json配置文件
tsconfig.json
是 TypeScript 项目的配置文件,放在项目的根目录。反过来说,如果一个目录里面有
tsconfig.json
,TypeScript 就认为这是项目的根目录。
{"compilerOptions":{"target":"es5","lib":["dom","dom.iterable","esnext"],"allowJs":true,"skipLibCheck":true,"esModuleInterop":true,"allowSyntheticDefaultImports":true,"strict":true,"forceConsistentCasingInFileNames":true,"noFallthroughCasesInSwitch":true,"module":"esnext","moduleResolution":"node","resolveJsonModule":true,"isolatedModules":true,"noEmit":true,"jsx":"react-jsx"},"include":[// 指定所要编译的文件列表,既支持逐一列出文件,也支持通配符。对于当前配置文件"src"]}
属性太多了,更加具体的属性可查阅: tsconfig.json
11、常见节点类型总结
写在前面:TS类型鼠标悬停至变量都可以看到。
React元素相关
ReactNode
。表示任意类型的React节点,这是个联合类型,包含情况众多;ReactElement
/JSX
。从使用表现上来看,可以认为这两者是一致的,属于ReactNode
的子集,表示“原生的DOM组件”或“自定义组件的执行结果”。
const App: React.ReactNode =
null
|| undefined
|| <div></div>
|| <MyComp title="world" />
|| "abc"
|| 123
|| true;
const b: React.ReactElement =
<div>hello world</div> || <MyComp title="good" />;
const c: JSX.Element =
<MyComp title="good" /> || <div>hello world</div>;
原生DOM相关
react中,原生dom被合成为了react事件,内部通过事件委托来优化内存。通用格式:xxxEvent,常见的有MouseEvent、ChangeEvent、TouchEvent,是一个泛型类型,泛型变量为触发该事件的 DOM 元素类型。
// input输入框输入文字
const handleInputChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
console.log(evt);
};
// button按钮点击
const handleButtonClick = (evt: React.MouseEvent<HTMLButtonElement>) => {
console.log(evt);
};
// 移动端触摸div
const handleDivTouch = (evt: React.TouchEvent<HTMLDivElement>) => {
console.log(evt);
};
与Hooks结合
//state ❌这样写是不必要的,因为初始值0已经能说明count类型
const [count, setCount] = useState<number>(0);
// ✅这样写好点
const [count, setCount] = useState(0);
//ref
const inputRef = useRef<HTMLInputElement>(null!);
//其他略
规约
一些TS命名规范技巧,不遵守也行。
1、子组件的入参名为 【组件名】Props,首字母以大写开头。如
// 比如当前组件名为InfoCard
export interface InfoCardProps {
name: string;
age: number;
}
2、为后端接受的出入参数书写interface,同时使用利于编辑器提示的jsdoc风格做注释。如:
//使用时,鼠标悬停会有提示,易于阅读。
export interface GetUserInfoReqParams {
/** 名字 */
name: string;
/** 年龄 */
age: number;
/** 性别 */
gender: string;
}
12、unknown,any
结论:unkunow会进行ts检查,any不会
**any **
表示任意类型,放弃了ts类型检查。您一定见过Anyscript!
type T1 = keyof any;// string | number | symbol | ...
unknown
可以把任何值赋给unknown类型,但unknown不能赋值给除(any|unknow) 外的任何类型
letA1:unknown;A1="akuna";//okA1=1124;//okletA2=A1+20;//error “A1”的类型为“未知”//注意:unknown类型的变量,不能直接赋值给其他类型。除了any类型和unknown类型,否则要指明类型。letA3=(A1asnumber)+20;//函数返回值指明类型functionisFunction(x:unknown):unknown{return x asFunction;}
//不能直接调用unknown类型变量的方法和属性。let v1:unknown={ foo:123};
v1.foo // 报错let v2:unknown='hello';
v2.trim()// 报错let v3:unknown=(n =0)=> n +1;v3()// 报错//正确做法(v3 asFunction)()
类型限制范围 any > unknow > …string,number…Object… > never
小技巧:可以适当的把any类型修饰的变量,改为unknown修饰。使用的时候再指明。
欢迎各位小伙伴补充TS实用用法~
参考手册
《阮一峰《TypeScript 教程》
官网
TypeScript 中文网
版权归原作者 三寸日光呼 所有, 如有侵权,请联系我们删除。