ts出了几年,以前用ts还可能显得逼格高,可是ts3.0后开始更加火了,不再是“加分项”了,更加像是“必会”的了。以前对于ts,一些人人为了用而用,可能只是为了让简历的经历好看一点,并无发挥它的做用。他们对于ts只是一些简单、低级的特性的应用,稍微麻烦一点的,就开始使用any。下面一步步来探究进阶的一些用法,一步步解决一些ts棘手的类型问题,逐步摆脱一些情景对any的依赖前端
强烈建议使用vscode,由于都是同一家,对ts的支持和开发体验是很是棒的,大大增长了开发效率和质量,避免各类错误。node
定义一种type或者interface,能够传入泛型参数,达到类型复用的效果:react
// 一个对象全部的key都是同一类型
// before
const o: { a: number; b: number } = {
a: 1,
b: 2,
}
// after
type OneType<T> = {
[key: string]: T;
}
const o: OneType<number> = {
a: 1,
b: 2,
}
复制代码
若是再包一层数组json
// 实际上就是Array<T>, T[]能够说是一个语法糖,这里为了理解泛型
type ArrType<T> = T[];
const e: ArrType<OneType<number>> = [
{
a: 1,
b: 2
},
{
c: 1,
}
]
复制代码
另外,函数也能够传入泛型参数:canvas
// 一个简单的函数
function f<T>(o: T): T { return o }
f<string>('1')
// 若是是数字,那么就报错了
f<string>(1)
复制代码
在tsx文件中,箭头函数泛型写法有点不同,由于要避免尖括号被误判:数组
const f = <T extends {}>(x: T) => x
复制代码
对于js的对象,咱们能够表示为object[key]。ts也有相似的,即索引访问T[K]。若是T是object的interface或者type、K是key的话,则表示object[key]具备类型T[K]。而这个K不是随便来的,通常须要索引类型查询操做符keyof的做用下返回了索引查询(number 、string类型的key)才会有效,不然报相似Type 'K' cannot be used to index type 'T'
的错误。antd
想一想就知道,没有任何其余条件或者约束(泛型约束),直接这样用T[K],ts怎么可能知道这是什么类型?怎么知道你想干什么?那就报错咯。数据结构
keyof返回某个interface或者type的全部key:react-router
interface IExample { a: number; b: number }
keyof IExample // 'a' | 'b'
复制代码
写一个get函数,输入对象和key,返回对应的value框架
// 这种时候,可能就开始写any了。由于不知道传入的是什么
function getValue(o: any, k: string): any {
return o[k]
}
getValue({ a: 1, b: '2' }, 'a');
// 稍微好一点的多是“以为这是对象因此是object”
// function get(o: object, k: string): any ,但返回值仍是any
// 若是不用any,那就报错object没有属性xxx,😢
复制代码
此时,keyof和泛型配合起来就能够告别any了:
// K extends keyof V 保证第二个泛型参数是属于o的一个key
function getValue<V, K extends keyof V>(o: V, k: K): V[K] {
return o[k]
}
getValue<{ a: number; b: string; }, 'a'>({ a: 1, b: '2' }, 'a');
复制代码
按照常规的思惟,key也就是那三种类型了,写死,而后泛型K就是key值
function getValue<V, K>(o: V, k: string | number | symbol): V[K]
可是这样就会报错:Type 'K' cannot be used to index type 'V',就是由于没有什么约束条件(如keyof操做符保证返回合法的key),K是什么也不知道,因此就直接报错类型K不能用于索引类型V的索引访问
换一种方式实现,须要考虑undefined
// 此时,咱们的getValue须要考虑到没取到值的状况,因此改一下泛型的逻辑
function getValue<V, K>(o: V, k: string | number | symbol): K extends keyof V ? V[K] : undefined {
return o[k]
}
复制代码
这里没有报错,由于返回值里面对K作了约束。若是K不是V的一个key,那么返回值就是undefined类型,所以保证了K不管传什么值都有被覆盖到了:属于V的一个key的K就是正常,不属于则返回undefined类型
最后,使用方法
interface IExample { a: number; b: number }
getValue<IExample, 'a'>({ a: 1, b: 2 }, 'a');
复制代码
这里注意,最后仍是要写死'a',为何呢?由于ts只能帮到你在写代码的时候,明确的告诉ts我要取a的值。若是依赖用户输入的那种key,已经脱离了ts力所能及的范围。此时在vscode中,逻辑仍是能够写着先,只是没有享受到返回值类型推断的这种福利
上面有一段K extends keyof V ? V[K] : undefined
,这是ts的condition type
,可是前面的condition只能使用extends的语句。好比像antd一些组件,仅仅有几种值:
若是咱们想写一个类型逻辑:是那几种type的就是那几种,不然返回default的type,那么就可使用condition type
了
declare const collection: ['a', 'b', 'c', 'default'];
declare type collectionType = (typeof collection)[number];
type isBelongCollection<T extends string> = T extends collectionType ? T : 'default'
type a = isBelongCollection<'a'> // 'a'
type b = isBelongCollection<'b'> // 'b'
type aa = isBelongCollection<'aa'> // 'default'
复制代码
若是想写一个getType函数,保证输入的type必定是那几个的一种:
const arr: collectionType[] = ['a', 'b', 'c', 'default'];
function getSize<T extends collectionType>(size: string): collectionType {
return arr.find(x => x === size) || 'default'
}
复制代码
有时候,咱们想给window加上一些辅助变量,发现会报错:
window.a = 1; // 类型“Window”上不存在属性“a”
复制代码
此时可能就会给window 强行as any了:
(window as any).a = 1;
复制代码
这样作,报错是解决了,可是又是依赖了any,并且还不能享受到在vsc写代码的时候,对window.a的代码提示。若是再次须要读取或者赋值window.a,那又要(window as any).a了。其实,优雅的解决方法是interface。interface能够写多个重名,多个会合并
interface I {
a: number;
}
interface I {
b: string;
}
const o: I = {
a: 1,
b: '2'
}
// 那么对window扩展的话,咱们只须要在项目的根的.d.ts文件里面再写一份扩展的Window interface便可
interface Window {
a: number;
}
复制代码
咱们使用其余方法修改了一些属性,好比装饰器、对象assign,ts代码确定是标红的,但实际上咱们都知道那是没有问题的:
let ao: {
a: number
} = { a: 1 }
ao = Object.assign(ao, { b: 11 })
ao.b // Property 'b' does not exist on type '{ a: number; }'
复制代码
因为后面也是人为的加上的属性b,那么咱们只能一开始的时候就直接声明b属性:
let ao: {
a: number,
b?: number,
} = { a: 1 }
ao = Object.assign(ao, { b: 11 })
ao.b
复制代码
使用装饰器的时候,咱们给Greeter类加上console方法。可是使用的时候会说Property 'console' does not exist on type 'Greeter'
。固然,使用的时候(this as any).console(this.wording)
就能够解决了
function injectConsole(target) {
target.prototype.console = function (txt) {
console.log(txt);
}
}
@injectConsole
class Greeter {
wording: string;
constructor(wording: string) {
this.wording = wording;
}
public greet() {
this.console(this.wording)
}
}
复制代码
实际上,和wording也是同理,事先声明一下console便可优雅解决:
@injectConsole
class Greeter {
wording: string;
console: (txt: string) => void; // 声明一下console方法
constructor(wording: string) {
this.wording = wording;
}
public greet() {
this.console(this.wording)
}
}
复制代码
一些常见的场景
mobx和react一块儿使用的时候,也是有相似场景:
import { inject, observer } from 'mobx-react';
interface IState {
// state的声明
}
// props的声明
type IProps = {
user: UserState; // 来自于inject的props须要提早声明一下
// ...其余本来组件的props声明
};
@inject('user')
@observer
class App extends React.Component<IProps, IState>{
// ...
public componentDidMount() {
console.log(this.props.user); // user是被inject进去的,其实是存在的
// 若是不事先声明user在props上,ts会报user不存在的错
}
}
复制代码
react router的路由匹配的params也是会有这个状况:
import { RouteComponentProps } from 'react-router';
// 前面路由匹配那个组件
<Route path="/a/:id" component={App} />
// props的声明
type IProps = RouteComponentProps<{
id: string; // 使用react-router里面的泛性类型RouteComponentProps给props声明匹配的参数
}> & {
// ...其余本来组件的props声明
};
class App extends React.Component<IProps>{
// ...
public componentDidMount() {
// 这一串在Route的path使用`:<key>`这种方式匹配到的时候会存在
// 当前path为'/a/1'的时候,打印1
console.log(this.props.match.params.id);
}
}
复制代码
当咱们使用别人的库、框架的时候,不懂人家的类型系统、不懂人家的数据结构,代码各类标红。有的人可能又开始按耐不住使用了any大法。此时,我必须站出来阻止:"no way!!"
好比上面的RouteComponentProps,按住cmd点击进入,发现其源码以下
export interface match<P> {
params: P;
isExact: boolean;
path: string;
url: string;
}
export interface RouteComponentProps<P, C extends StaticContext = StaticContext> {
history: H.History;
location: H.Location;
match: match<P>;
staticContext?: C;
}
复制代码
这就很明显的,告诉咱们匹配到的参数就在props.match.params里面。这不只知道告终构,还至关于半个文档,看一下命名就知道是作什么的了
使用antd的时候,忘记了某个组件的props怎么办🤔️?打开antd官网查。不!不须要。只须要按下cmd+鼠标点击组件,进入源码的d.ts文件便可。来,跟我左边一块儿看个文件,右边看下一个文件
// 我要经过接口拉数据展现到table上,并且点击某行要弹出修改
// 我知道这里要用Table组件,但不知道有什么属性,点进去看看
// 一进去就发现Table能够传泛型参数
export default class Table<T> extends React.Component<TableProps<T>, TableState<T>> {}
// TableProps<T>是一个关键,肯定了这个组件的props了,点进去看看
export interface TableProps<T> {
rowSelection?: TableRowSelection<T>;
pagination?: PaginationConfig | false;
size?: TableSize;
dataSource?: T[];
components?: TableComponents;
columns?: ColumnProps<T>[];
rowKey?: string | ((record: T, index: number) => string); rowClassName?: (record: T, index: number) => string;
expandedRowRender?: (record: T, index: number, indent:
onChange?: (pagination: PaginationConfig, filters: Record<keyof T, string[]>, sorter: SorterResult<T>, extra: TableCurrentDataSource<T>) => void;
loading?: boolean | SpinProps;
locale?: TableLocale;
indentSize?: number;
onRowClick?: (record: T, index: number, event: Event) => void;
onRow?: (record: T, index: number) => TableEventListeners;
footer?: (currentPageData: Object[]) => React.ReactNode;
title?: (currentPageData: Object[]) => React.ReactNode;
scroll?: {
x?: boolean | number | string;
y?: boolean | number | string;
};
}
复制代码
type ListType = { name: string };
const list: ListType = [{ name: 'a' }, { name: 'b' }];
return (
<Table<ListType>
dataSource={list}
scroll={{ x: 800 }}
loading={isLodaing}
onChange={onChange}
onRowClick={onRowClick}
>
<Column dataIndex="name" title="大名" />
</Table>
);
// Column组件是另外一种写法,能够不用columns了
复制代码
// 随便找一个比较冷门且小型的库
import parser from 'big-json-parser';
复制代码
若是他不支持,ts也会报错: 没法找到模块“xxx”的声明文件。import就报错了,as any大法都不行了!
既然他没有,那就帮他写。来到本身项目的d.ts根文件下,加入声明:
// 翻了一下源码,这里由于他没有d.ts,因此真的是去node_modules翻了
// 发现这是一个把对象转成schema结构的函数
declare module 'big-json-parser' {
export default function parse(source: string, reviver?: (key: string, value: any)=>any ): string;
}
复制代码
若是想快速使用,或者某一环节代码比较复杂,那就给any也行。若是是认真看源码知道每个参数是干什么的,把全部的函数参数类型补全也不错。对方没有对他的库进行定义,那么你就来给他定义,看文档、看源码搞清楚每个参数和类型,若是不错的话还能够给做者提一个pr呢
最后,给出如何编写d.ts的常见几种模块化方案:
// ES module:
declare const a: string
export { a }
export default a;
// commonjs:
declare module 'xxx' {
export const a: string
}
// 全局,若是是umd则把其余模块化规范也加进来
declare namespace xxx{
const a: string
}
// 此时在业务代码里面输入window. 的时候,提示a
复制代码
像这种状况是真的不能在写代码的时候推断类型的,可使用any,可是失去了类型提示。其实可使用is来挽回一点局面:
// 若是是canvas标签,使用canvas标签的方法
function isCanvas(ele: any): ele is HTMLCanvasElement {
return ele.nodeName === 'canvas'
}
function query(selector: string) {
const e = document.querySelector(selector)
if (isCanvas(e)) {
e.getContext('2d')
}
}
复制代码
使用ts基本语法和关键字,能够实现一些高级的特性(如Partial,Required,Pick,Exclude,Omit等等),增长了类型复用性。按住cmd,再点击这些类型进入ts源码里面(lib.es5.d.ts)的看到一些内置类型的实现:
// 所有变成可选
type Partial<T> = {
[P in keyof T]?: T[P];
};
type t = Partial<{ a: string, b: number }> // { a?: string, b?: number }
// 所有变成必填
type Required<T> = {
[P in keyof T]-?: T[P];
};
type p = Required<{ a: string, b?: number }> // { a: string, b: number }
// 所有变成只读
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// 从T里面挑几个key
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
type p = Pick<{ a: string, b?: number }, 'a'> // { a: string }
// K extends keyof,说明第一个泛型参数是key。也就是对于给定key,都是T类型
type Record<K extends keyof any, T> = {
[P in K]: T;
};
type p = Record<'a' | 'b', string> // { a: string, b: string }
// Record版本的Readonly和Required,应该怎么实现,也很明显了
// 返回T里面除了U的
type Exclude<T, U> = T extends U ? never : T;
type p = Exclude<'a' | 'b', 'a'> // 'b'
// Exclude反向做用
type Extract<T, U> = T extends U ? T : never; // 'a'
// Pick的反向做用,从T里面选取非K里面的key出来
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
type p = Omit<{ a: string, b?: number }, 'b'> // { a: string }
// 过滤null和undefined
type NonNullable<T> = T extends null | undefined ? never : T;
type p1 = NonNullable<{ a: number, b: string }> // { a: number, b: string }
type p2 = NonNullable<undefined> // never
// 把函数的参数抠出来
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
type op = Parameters<(a: string, b: number) => void> // [string, number]
复制代码
有时候,咱们真的是不知道结果是什么,而后就上了any。好比querySelector,选择了一个什么结果都是一个未知数,下面的代码就标红了:
function commonQuery(selector: string) {
const ele = document.querySelector(selector)
if (ele && ele.nodeName === 'DIV') {
console.log(ele.innerHTML)
} else if (ele && ele.nodeName === 'CANVAS') {
console.log(ele.getContext)
}
}
复制代码
此时咱们可使用is来补救一下:
function isDiv(ele: any): ele is HTMLDivElement {
return ele && ele.nodeName === 'DIV'
}
function isCanvas(ele: any): ele is HTMLCanvasElement {
return ele && ele.nodeName === 'CANVAS'
}
function commonQuery(selector: string) {
const ele = document.querySelector(selector)
if (isDiv(ele)) {
console.log(ele.innerHTML)
} else if (isCanvas(ele)) {
// 不会报错,且有代码提示
console.log(ele.getContext)
}
}
复制代码
关注公众号《不同的前端》,以不同的视角学习前端,快速成长,一块儿把玩最新的技术、探索各类黑科技