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)
}
}
複製代碼
關注公衆號《不同的前端》,以不同的視角學習前端,快速成長,一塊兒把玩最新的技術、探索各類黑科技