TS每一块其实都值得深挖,此篇意在总结高频应用场景!顺序不分先后!
1、定义简单约束
场景:定义一个变量的类型约束
//常规定义
export interface Person {
name: string;
age: number;
}
//name和age必填,其他属性开放式
export interface Person {
name: string;
age: number;
[key:string]: string | number;
}
//带限制条件的(慎用)
export interface Person {
name: string;
age?: number; //可选
readonly sex: string; //只读
}
type也可以实现上述,还可以定义联合类型
//定义联合类型
type alias = 'source' | 'math' | 'english';
//承接类型;
type alias1 = typeof obj; // typeof 将对象转换为类型
type alias2 = keyof Iobj; // 将类型的健,提取为联合类型
//定义元组类型(基本用不到)
type alias3 = [string, number]
提取一个对象的健作为一个类型的key
const A1 = {
age: 100,
name: 'Alince'
}
// in 用于判断 前者是否属于后者
type A2 = {
[K in keyof typeof A1]: string
}
巧用Record,Record<key,value> 可以直观的定义对象的key-value健值对
let obj: Record<string, number[]> = {
a: [1, 2],
b: [3, 4]
}
总结官方推荐使用 interface,其他无法满足需求的情况下用 type。很简单,表达一些复杂约束,用type更加灵活。
2、类型工具
Partial (常用)
将类型全部变为可选
//源码
type Partial<T> = {
[P in keyof T]?: T[P];
};
//用法
interface Person {
name: number;
age: string;
}
//这里的newPerson 所有的属性变成了可选
type newPerson = Partial<Person>;
Required 变为不可选
//与此相对的是 Required<T> 全变为必填
type Required<T> = {
[P in keyof T]-?: T[P];
};
Record(高频)
定义对象的健值类型,Record<健, 值>
/**
* Construct a type with a set of properties K of type T
*/
type Record<K extends keyof any, T> = {
[P in K]: 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
*/
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
Exclude(常用)
排除类型 Exclude<类型, 要排除类型> ,常用语联合类型
/**
* Exclude from T those types that are assignable to U
*/
type Exclude<T, U> = T extends U ? never : T;
//用法
type num = Exclude<'1' | '2' | '3', '1'> // '2' | '3'
Readonly(常用)
让所有的属性都变为只读
/**
* Make all properties in T readonly
*/
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
Omit (常用)
删除 属性中的key
/**
* Construct a type with the properties of T except for those in type K.
*/
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
//示例
interface Person {
name: number;
age: string;
}
type del = Omit<Person,'name'> //{ age: string;} name已被删除
Ï
// 新增的话 可以用extends去继承新类型,type 用&
NonNullable
排除联合类型的null 和undefined
/**
* Exclude null and undefined from T
*/
type NonNullable<T> = T & {};
//示例
type del = NonNullable<'a' | null | undefined | 'b'> // 'a' | 'b'
Parameters
获取函数参数类型 作为元组
/**
* Obtain the parameters of a function type in a tuple
*/
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
//示例
function test(a:string,b:number) {
return {
a,b
}
}
type testtype = Parameters<typeof test>
// type testtype = [a: string, b: number]
// 获取的是 类型值
type testtype1 = Parameters<typeof test>[1]
// type testtype1 = number
ReturnType
获取函数返回值类型
/**
* Obtain the return type of a function type
*/
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
interface typeb {
name: string,
age: number,
}
let person:typeb = {
name:'xiaoming',
age:12
}
function test(person:typeb) {
return person
}
type testtype2 = ReturnType<typeof test>
// type testtype2 = typeb
//真够奇葩的,还有这种的
总结正所谓,使用TS轻则伤筋动骨,重则半生不遂,一定慎重使用!
3、其他好用的功能
enum
定义顺序常量,提高代码的可靠性和可维护性
enum DRECTOR {
LEFT,
RIGHT,
CENTER,
TOP,
BOTTOM
}
let directiion = DRECTOR.LEFT;
类型断言
as
基本用法
const foo = {};
foo.bar = 123; // Error: 'bar' 属性不存在于 ‘{}’
foo.bas = 'hello'; // Error: 'bas' 属性不存在于 '{}'
//可以用as作提前推断
interface Person {
name: string;
age: number;
}
let foo = {} as Person;
foo.name = 'alince';// ✅
foo.age = 20; // ✅
将一个联合类型断言为其中一个类型
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
/*
我们需要在还不确定类型的时候就访问其中一个类型特有的属性或方法,比如
*/
function isFish(animal: Cat | Fish) {
// 获取 animal.swim 的时候会报错
if (typeof animal.swim === 'function') {
return true;
}
return false;
}
/*
此时可以使用类型断言,将 animal 断言成 Fish,从而解决报错
*/
function isFish(animal: Cat | Fish) {
if (typeof (animal as Fish).swim === 'function') {
return true;
}
return false;
}
总之,就是将导致爆红的,模糊不定的类型,确定为符合编译的类型。注意:这样骗过了编译器,会增加运行时出现bug的几率。
as const
如果没有声明变量类型,let 命令声明的变量,会被类型推断为 TypeScript 内置的基本类型之一;
const 命令声明的变量,则被推断为值类型常量。
// 类型推断为基本类型 string
let s1 = 'JavaScript';
// 类型推断为字符串 “JavaScript”
const s2 = 'JavaScript';
有时候,会有意想不到的错误
let s = 'JavaScript';
type Lang =
|'JavaScript'
|'TypeScript'
|'Python';
function setLang(language:Lang) {
/* ... */
}
setLang(s); // 报错 类型“string”的参数不能赋给类型“Lang”的参数。
//这时候需要就会需要 as const出场了
// 改为 let s = 'JavaScript' as const; 即可!
常用示例:对象断言,加as const 收缩类型
const router = {
home: '/',
admin: '/admin',
user: '/user'
} as const // 加了as const 让类型系统知道这个对象是常量,类型范围变窄了
//'const' 断言只能作用于枚举成员、字符串、数字、布尔值、数组或对象字面量。
// let router2 = router as const //报错
const goToRoute = (r: '/' | '/admin' | '/user') => { }
goToRoute(router.admin)
枚举断言
enum Foo {
X,
Y,
}
let e1 = Foo.X; // Foo
let e2 = Foo.X as const; // Foo.X
上面示例中,如果不使用as const
断言,变量e1
的类型被推断为整个 Enum 类型;
使用了as const断言以后,变量e2的类型被推断为 Enum 的某个成员,这意味着它不能变更为其他成员。
!非空断言
慎用
//举例
const f(x?:number|null) => {
validateNumber(x); // 自定义函数,确保 x 是数值
console.log(x!.toFixed());
}
const validateNumber(e?:number|null) => {
if (typeof e !== 'number')
throw new Error('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
// 示例2
interface A1 {
name: string
}
interface A2 {
name: string
age: number
}
// A的类型为string
type A = A2 extends A1 ? string : number //A2的接口是否满足A1 string
const a: A = 'this is string'
type A1 = 'x' extends 'x' ? string : number; // string
type A2 = 'x' | 'y' extends 'x' ? string : number; // number 前者类型是否满足后者
type A3 = 'x' extends 'x' | 'y' ? string : number; //string 前者类型是否满足后者
到此,extends用法平平无奇~
泛型分配律
注意:不常用,但面试会问
当作为泛型传入的时候
type P<T> = T extends 'x' ? string : number;
type A3 = P<'x' | 'y'> // ? 猜猜看
这里直接给结论~
type P<T> = T extends 'x' ? string : number;
type A3 = 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是所有类型的子类型
type A1 = never extends 'x' ? string : number; // string
type P<T> = T extends 'x' ? string : number;
type A2 = P<never> // never
wc,居然不一样?
实际上,还是走条件分配在起作用。never被认为是空的联合类型。也就是说,没有联合项的联合类型,所以还是满足上面的分配律,然而因为没有联合项可以分配,所以P<T>
的表达式其实根本就没有执行,所以A2的定义也就类似于永远没有返回的函数一样,是never类型的。
5、泛型
约束返回值
场景:常用于约束 接口返回值。
/**
* 通用返回值
*/
export interface Result<T = unknown> {
code: number;
data: T;
msg: string;
}
/**
* 用户列表返回值
*/
export interface ResultData<T = unknown> {
list: T[];
page: {
pageNum: number | 0;
pageSize: number | 0;
total: number | 0;
};
}
//使用
interface IUser {
id: number;
name: string;
}
type A1 = Result<IUser>
type A2 = ResultData<IUser>
约束普通函数
场景:一个函数,不知道需要传什么类型,传入的类型且于函数其他类型有关联
//写法
function id<T>(arg:T):T {
return arg;
}
let myId:<T>(arg:T) => T = id;
let myId:{ <T>(arg:T): T } = id;
//应用 个人觉得知道这个语法就行,实际应用有待商榷。
function getFirst<T>(arr:T[]):T {
return arr[0];
}
6、命名空间
场景:面对复杂的类型定义,将类型分组,是不错的选择
例如:
/** 面板模块 */
export namespace Dashboard {
export interface ReportData {
driverCount: number;
totalMoney: number;
orderCount: number;
cityNum: number;
}
export interface LineData {
label: string[];
order: number[];
money: number[];
}
}
/** 用户管理模块 */
export namespace User {
export interface UserItem {
userId: number;
deptId: string;
userName: string;
userEmail: string;
state: number;
mobile: string;
job: string;
role: number;
roleList: string;
createId: number;
deptName: string;
userImg: string;
}
/**
* 分页数据请求参数
*/
export interface Params extends PageParams {
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); // 10
console.log(~~10.3); // 10
console.log(~~-5.9); // -5
取中间值 >>
// 求(2,7)的中间值
console.log(Math.floor(2 + (7 - 2) / 2)); // 4
console.log(Math.floor((2 + 7) / 2)); // 4
console.log((2 + 7) >> 1); // 4
双感叹号 !!
通常使用双感叹号来将一个空状态强制转换为boolean类型
const formateData = (data:Array<string>)=>{
//TO DO... 可能返回undefined
return data.length > 0 ? true :null
}
const result = (res:Array<string>):boolean=>{
return formateData(res)!!
}
!非空断言(慎用)
let name: string | null = "Tom";
// 避免了编译器的空值检查,但是生成的js文件中还是name.length,如果此时name为null,那么就会出现运行时异常
console.log(name!.length);
function greet(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
// 如果需要浮点数,可以使用parseFloat
let 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) 外的任何类型
let A1: unknown;
A1 = "akuna"; //ok
A1 = 1124; //ok
let A2 = A1 + 20; //error “A1”的类型为“未知”
//注意:unknown类型的变量,不能直接赋值给其他类型。除了any类型和unknown类型,否则要指明类型。
let A3= (A1 as number) + 20;
//函数返回值指明类型
function isFunction(x: unknown):unknown{
return x as Function;
}
//不能直接调用unknown类型变量的方法和属性。
let v1:unknown = { foo: 123 };
v1.foo // 报错
let v2:unknown = 'hello';
v2.trim() // 报错
let v3:unknown = (n = 0) => n + 1;
v3() // 报错
//正确做法
(v3 as Function)()
类型限制范围 any > unknow > …string,number…Object… > never
小技巧:可以适当的把any类型修饰的变量,改为unknown修饰。使用的时候再指明。
欢迎各位小伙伴补充TS实用用法~
参考手册
《阮一峰《TypeScript 教程》
官网
TypeScript 中文网