typescript叫anyscript?不存在的

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

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一些組件,僅僅有幾種值:

  • Button的size有"large", "default", "small"
  • Button的type有"default", "primary", "ghost", "dashed", "danger", "link"

若是咱們想寫一個類型邏輯:是那幾種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 as any

有時候,咱們想給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;
    };
}
複製代碼
  • 看命名,數據應該就放dataSource、表格有哪些列就配置一下columns
  • 注意到ColumnProps,而T是泛型接口TableProps來的,TableProps的T又是Table組件的泛型參數。ok,這就肯定了dataSource的數據結構了
  • 數據還沒加載完使用loading,照顧到小屏還可使用scroll控制距離多大才滾動
  • 看onChange的參數pagination,翻頁的回調實錘了,那pagination確定是配置翻頁的
  • onRowClick?顧名思義,這就是我要的點擊某行要彈出修改的效果呀
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了
複製代碼

自己沒有d.ts文件的庫怎麼辦

// 隨便找一個比較冷門且小型的庫
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
複製代碼

dom選擇器不知道是什麼類型

像這種狀況是真的不能在寫代碼的時候推斷類型的,可使用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]
複製代碼

is

有時候,咱們真的是不知道結果是什麼,而後就上了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)
  }
}
複製代碼

總結

  • 首先須要主動積極維護項目代碼的類型系統,遇到稍微麻煩的狀況要先嚐試能不能有解決方案,而不是立刻妥協使用any
  • 若是不知道一個外部的庫、框架的類型系統,能夠點進去看他的d.ts源碼。若是沒有d.ts文件,能夠本身去看一下文檔和源碼,本身給它定義類型
  • learn by doing,step by step

關注公衆號《不同的前端》,以不同的視角學習前端,快速成長,一塊兒把玩最新的技術、探索各類黑科技

相關文章
相關標籤/搜索