Effective Typescript:使用 Typescript 的 n 個技巧

主要整理了 effective typescript 裏列出的一些技巧,列出了一些本身以爲有用的,有的簡單的就一筆帶過。更加友好的排版見 blog.staleclosure.com/effective-t…javascript

Typescript 入門

理解Typescript與Javascript區別

Typescript編譯選項影響類型檢查(建議開啓strict)

代碼生成與類型檢查是獨立的

  • 類型報錯也不影響代碼生成
  • 運行時不進行類型檢查
  • 類型斷言不影響運行時類型
  • 運行是類型和定義的類型可能不一致
function setLightSwitch(value:boolean){
  switch(value){
    case true: 
       turnOn();
       break;
    case false: 
       turnOff();
       break;
    default: 
        console.log('dead code')
  }
}
複製代碼

上述代碼的default條件在TS裏會被標記爲dead code,但在運行時仍然可能執行,如setLightSwitch('boom'),因此 代碼裏不能徹底依賴於類型檢查,必要時仍是須要進行防護性編程java

  • 不支持靜態重載

如不支持以下靜態重載react

function add(a:number,b:number): number { 
  return a + b;
}
function add(a:string,b:string): string{
  return a + b + '!'
}
複製代碼

只能支持函數簽名加函數實現的方式重載c++

function add(a:number,b:number):number;
function add(a:string,b:string):string;
function add(a:any,b:any){
  if(typeof a === 'string'){
    return a + b + '!'
  }else{
    return a + b
  }
}
複製代碼
  • Typescript的類型聲明不影響運行時性能

習慣結構化類型(Structual Typing)

下述代碼,即便vector3D不是vector2D的子類,仍然不會報錯, 由於Typescript不是經過繼承來實現子類型,而是經過structual typing來實現子類型, 即雖然vector3D不是vector2D的子類可是是其子類型。web

class vector2D{
  constructor(public x:number, public y: number){
    this.x = x;
    this.y = y;
  }
}
class vector3D{
  constructor(public x:number,public y:number,public z:number){
    this.x = x;
    this.y = y;
    this.z = z;
  }
}
function calculateLength(v:vector2D){
  return Math.sqrt(v.x*v.x + v.y*v.y)
}
const point = new vector3D(0,1,2)
const dist = calculateLength(point) 
複製代碼

限制any的使用

  • any至關於放棄了類型檢測
  • any破壞了自動補全
  • any對重構代碼不友好
  • any掩蓋了你的類型設計
  • 盡你所能避免any

Typescript的類型系統

  • 充分使用編輯器的language service功能(類型提示,類型檢查,類型推倒,自動補全,類型定義跳轉)
  • 把類型當作值的集合思考
type A= 'A' // 單值集合 { 'A' }
type B= 'B' // 單值集合 { 'B' }
type AB = 'A' | 'B'  // 集合的並集 { 'A', 'B' }
type twoInt =  2 | 4 | 5 ... // 無限元素集合 { 1,2,3,4}
type threeInt = 3 | 6 | 9 // 無限集合
type twoIntersectThreeInt = twoInt & threeInt // 無限集合的交集
type twoUnionThreeInt = 2| 3 | 4 | 6 ... // 無限集合的並集
keyof (A&B) = (keyof A) | (keyof B)
keyof (A|B) = (keyof A) & (keyof B)
複製代碼
  • 術語和集合術語對照表
Typescript術語 集合術語
never 空集
literal type 單值集合
value 可賦值給 T value ∈T
T1 assignable to T2 T1是T2的子集
T1 extends T2 T1是T2的子集
T1 T2
T1 & T2 T1 和T2的交集
unknown universal set

區分類型空間(type space)仍是值空間(value space)

  • TS中的一個符號能夠屬於Type space或者 value Space,也能夠同時屬於type spacevalue space
  • classenum同時屬於type spacevalue space

以下的左邊的Cylinder是實例的類型,而右邊的Cylinder是construtortypescript

class Cylinder {
  radius = 1;
  height = 1
}
const instance: Cylinder = new Cylinder();
複製代碼

而且typeof Cylinder並不是是Cylinder類型,而InstanceType<typeof Cylinder>纔是Cylinder類型 這裏着重說一下class,class其實是兩類對象的合體,一類是做爲構造函數及其原型屬性一類是類對象自己 考察以下的classexpress

class Test {
  constructor(x:number){
    this.instanceMember = x;
  }
  static staticMember = 1;
  instanceMember = 2;
  static staticMethod1(){

  }
  static staticMethod2(){
    this.staticMethod1();
  }
  instanceMethod1(){

  }
  instanceMethod2(){
    this.instanceMethod1()
  }
}
複製代碼

實際上能夠將Test拆分爲兩部分編程

class Test {
   instanceMember = 1;
   instanceMethod1(){

   }
   instanceMethod2(){

   }
}

object Test {
  new(x:number): Test{

  }
  staticMethod1(){

  }
  staticMethod2(){

  }
  staticMember = 1
}
複製代碼

這裏的object Test在scala中被稱爲伴生對象,而這裏的class Test實際是用來生成實例對象的 伴生對象和實例對象經過構造函數關聯 咱們能夠從伴生類型中獲取實例類型,也能夠從實例類型獲取伴生類型api

const test = new Test()
type instanceType = typeof test; // 獲取實例對象的類型即這裏class Test定義的類型
type companionType = typeof Test // 獲取伴生對象的類型即這裏的object Test定義的類型
type c = InstanceType<companionType> // 根據伴生類型推倒實例類型
複製代碼

雖然能夠經過實例的__proto__來獲取伴生對象可是Typescript並無提供支持數組

  • 還有不少東西在兩個spaces下有不一樣的意義
    • constvalue space修飾變量時表示變量不能從新賦值,而as const修飾則修改字面量的類型推導
    • extends 能夠用來定義繼承關係(class A extends B)或者定義子類型(interface A extends B)或者定義泛型約束Generic<T extends number>
    • in可用於檢測屬性存在key in object也能夠用於mapped type({[key in keyof T]:string})

優先使用類型聲明而非類型斷言

避免使用裝箱類型(String, Number, Boolean, Symbol, BigInt)

const a = new String('ss');
const b: string = a; // String沒法賦值給string
const c:String = '123' // string能夠賦值給String
複製代碼

多餘屬性檢查(Excess Property Checking)的侷限

當將對象字面量賦值給變量時會觸發額外的屬性檢查,以保證沒有傳入多餘的屬性

interface Point {
  x: number;
  y: number;
}
const point : Point = {
  x:1,
  y:2,
  z:3 // 報錯,多餘的屬性
}
複製代碼

這個按照strutual typing的設計是不合理的,有幾種繞過Excess Property Checking方式 這裏是Typescript對對象字面量額外添加的檢查,

  • 引入臨時變量
interface Point {
  x: number;
  y: number;
}
const tmp = {
  x:1,
  y:2,
  z:3 
}
const point:Point= tmp; // 不報錯
複製代碼
  • 類型斷言
interface Point {
  x: number;
  y: number;
}
const point : Point = {
  x:1,
  y:2,
  z:3 
} as Point
複製代碼

儘量對整個函數表達式進行類型標註

  • 提取出公共的函數類型
function add(a:number,b:number){
  return a+b;
}
function sub(a:number,b:number){
  return a-b;
}
function mult(a:number,b:number){
  return a*b;
}
function div(a:number,b:number){
  return a/b;
}
複製代碼

提取出公共的函數類型,可簡化以下

type Binary = (a:number,b:number) =>number;
const add : Binary = (a,b) => a+b;
const sub: Binary = (a,b) => a-b;
const mult: Binary = (a,b) => a*b;
const div: Binary= (a,b) => a-b;
複製代碼
  • 使用typeof fn來標註加強的函數類型
const checked: typeof fetch = (...args) => {
  return fetch(...args).then(resp=> {
    if(!resp.ok){
      throw new Error('failed')
    }
  })
}
checked('/api') // 能夠繼續獲取類型檢查
複製代碼

瞭解type和interface的區別

  • 絕大部分狀況下,type和interface都能等價轉換
// 普通對象
type TState = {
  name: string;
  capital: string;
}
interface TState {
  name: string;
  capital: string;
}
// index signature
type TDict = {[key:string]: string }
interface IDict {
  [key:string]: string
}
type TFn = (x:number) => string;
interface IFn {
  (x:number):string;
}
// function with props
type TFnWithProps = {
  (x:number):number;
  prop: string;
}
interface IFnWithProps {
  (x:number):number;
  prop: string;
}
// constructor
type TConstructor = new(x:number) => {x:number}
interface IConstructor{
  new(x:number): {x:number}
}
// generic
type TPair<T>= {
  first: T;
  second: T;
}
interface IPair<T> {
  first: T;
  second: T;
}
// extends
type TStateWithProps = IState & { population : number}
interface IStateWithProp extends TState {
  population: number;
}
// implements
class StateT implements TState {
  name = '';
  capital = '';
}
class StateI implements IState {
  name='';
  capital = ''
}
複製代碼
  • type和interface亦有所區別
    • inteface沒法應用於union type | intersection type | conditional type | tuple
type AorB = 'A' | 'B'
type NamedVariable = (Input | Output) & { name: string}
type Pair = [number,number]
複製代碼
  • interface 能夠augumented,而type不能夠
// inner
interface IState {
  name :string;
  capital: string;
}
// outer
interface IState {
  population: number
} 
const wyoming: IState = {
  name: 'Wyoming',
  capital: 'Cheyenne',
  population: 500_000
}
複製代碼

充分利用泛型和類型運算避免冗餘類型標記

  • 使用泛型提取公共的util type,簡化類型編寫
interface ButtonProps {
  type: string;
  size: 'large' | 'middle'| 'small'
}
interface ButtonPropsWithChildren{
  type: string;
  size: 'large' | 'middle'| 'small',
  children: React.ReactNode
}
複製代碼

使用PropsWithChildren簡化

import { PropsWithChildren } from 'react';
type ButtonPropsWithChildren = PropsWithChildren<ButtonProps>
複製代碼
  • 使用index type | mapped type | keyof 等進行類型傳遞
interface State {
  userId: string;
  pageTitle: string;
  recentFiles: string[]
  pageContents: string;
}
interface TopNavState {
  userId: string;
  pageTitle: string;
  recentFiles: string[]
}
複製代碼

上述代碼可經過lookup type簡化

interface TopNavState = {
  userId: State['userId'];
  pageTitle: State['pageTitle']
  recentFiles: State['recentFiles']
}
複製代碼

使用mapped type 可進一步簡化

type TopNavState = {
  [k in 'userId' | 'pageTitle' | 'recentFiles'] : State[k]
}
複製代碼

再使用工具類進一步簡化

type TopNavState = Pick<State, 'userId', 'pageTitle', 'rencentFiles'>
複製代碼

咱們也能夠利用typeof來進行類型傳遞

function getUserInfo(userId:string){
  return {
    userId,
    name,
    age,
    height,
    weight,
    favoriteColor
  }
}
type UserInfo = ReturnType<typeof getUserInfo>
複製代碼
  • 編寫utility type時,多多使用generic constraint保證明例化時的類型安全
interface Name {
  first: string;
  last: string
}
type Pick1<T, K>{
  [k in K]: T[k]
}
type FirstLast = Pick1<Name, 'first'| 'last'>
type FirstMiddle = Pick1<Name, 'first', 'middle'> // 應該報錯但沒報錯
type Pick2<T, K extends keyof T> = { // 添加泛型約束
  [k in K]: T[K]
}
type FirstMiddle = Pick2<Name, 'first', 'middle'> // 正確的報錯了
複製代碼

使用Index signature來表示動態數據

  • 對於只有在運行期才能獲取的屬性,能夠經過index signature來建模,如從csv或者遠程加載數據
function parseCSV(input:string): {[columnName:string]: string}[]{
  // xxx
}
複製代碼

能夠經過Record簡化

function parseCSV(input:string): Record<string,string>[] 複製代碼
  • 對於動態數據,其屬性的值的類型應添加undefined類型,確保安全訪問
function safeParseCSV(input:string): Record<string,string|undefined>[] const result =safeParseCSV('input') for(const x of result){
  console.log(x.name?.toUpperCase()) // 應該使用optiona chain訪問,防止屬性不存在
}
複製代碼
  • 儘量對index signatures 進行細化以保證類型安全
interface Row1 { [column:string]: number} // 太寬泛了,容許訪問不該該容許的屬性了
interface Row2 { a:number, b?:number, c?:number, d?:number} // 不容許訪問不存在的屬性了
interface Row3 = | {a:number} | { a: number; b:number } | {a:number;b:number;c:number} | {a: number; b: number; c:number; d:number} 
// 更細化了,不容許{ a:1, c: 2}這種不容許的對象
複製代碼

優先使用 Arrays、Tuple、ArrayLike而非number index signatures

  • 數組其實是對象,其keys也是string而非number,Typescript裏使用number index signature是爲了進行更多的類型檢查

即便以下代碼x[0]和x['0']的行爲在運行時徹底一致,可是隻有x[0]才能正確的推倒出類型。

let a : string[] = []
let x = a[0] // x類型爲string
let y = a['0'] // 可是y類型爲any
複製代碼

使用readonly來避免mutation形成的錯誤

  • 聲明參數爲readonly來避免在函數實現裏修改參數

以下所示,當聲明一個函數的參數爲readonly時

  • Typescript會檢查函數實現裏是否對參數進行了修改
  • 調用者能夠確保實現沒有修改參數
  • 調用者能夠傳遞一個readonly 的數組
function arraySum(arr:readonly number[] ){
  let sum=0,num = 0;
  // check error
  while((num = arr.pop()) !== undefined){
    sum += num;
  }
  return sum;
}
複製代碼

若是一個函數沒有聲明一個函數參數爲readonly,那麼將沒法傳遞一個readonly的數組, 即便函數實現沒有修改參數

function arraySum2(arr: number[]) {
  
}
const arr: readonly number[] = [];
arraySum2(arr)
複製代碼

因此爲了保證函數能夠同時接受readonly和非readonly的數組,應儘可能聲明參數爲readonly(這裏和c++的const reference 和reference的限制很相似)

  • 區別constreadonly
  • const 用於修飾變量,表示變量不可從新賦值
  • readonly用於修飾值,表示值的不可變(雖然在Typescript只能限制最外一層)

使用Mapped Type來實現值和類型的同步

假若有一天你實現了一個組件,而且實現了shouldComponentUpdate來進行性能優化

class App extends React.Component<{
  x: number,
  y: number
}> {
  shouldComponentUpdate(props){
    return props.x !== this.props.x  || props.y !== this.props.y
  }
}
複製代碼

忽然有一天你的組件添加了個新的z props,雖然你擴展了你的props類型,可是你忘記修改了 shouldComponentUpdate,致使組件該從新渲染的時候沒從新渲染,此時Typescript並不會 幫你作檢查

type AppProps = {
  x: number,
  y: number,
  z: number,
  onClick: () => {} // 不須要檢查它
}
class App extends React.Component< AppProps> {
  shouldComponentUpdate(props){
    return props.x !== this.props.x  || props.y !== this.props.y
  }
}
複製代碼

經過Mapped Type咱們能夠創建這種檢查,下面的[k in keyof AppProps]保證了 每次添加新的屬性,都須要在REQUIRED_UPDATE進行添加

{
  x: number,
  y: number,
  z: number,
  onClick: () => {} // 不須要檢查它
}

const REQUIRED_UPDATE: {[k in keyof AppProps]: boolean} = {
  x: true,
  y: true,
  z: true
  onClick: false,
}
class App extends React.Component<AppProps> {
  shouldComponentUpdate(props){
    for(const k in this.props){
      if(this.props[k] !== props[k] && REQUIRED_UPDATE[k]){
        return true;
      }
    }
    return false;
    
  }
}
複製代碼

類型推導

避免濫用類型推導

  • 避免對簡單能夠推導的類型進行標註
const a: number = 10; // 不建議
const a = 10 // 可自行推導

const obj: {name: string, age: number} = {name:'yj', age: 20} // 不建議
const obj = { name: 'yj', age: 20} // 自動推導
複製代碼
  • 對於函數儘可能顯示的標明返回類型,而非依賴類型推導

避免將一個變量重複賦值爲其餘類型

以下代碼雖然在javascript裏是合法的,可是在typescript裏會報錯

let id = '123456';
id = 123456; // 123456 not assignable to string
複製代碼

解決方式1: 聲明類型爲union

let id: string| number = '123456';
id = 123456 // works
複製代碼

雖然上述代碼能經過類型檢查,但更好的方式是避免使用union, 而是從新定義一個新的變量

let id= '123456';
let idInt = 123456;
複製代碼

進一步的能夠將變量聲明爲const

const id= '123456';
let idInt = 123456;
複製代碼

理解 Type widening

當你使用一個常量初始化一個變量而且沒提供類型標註時, typescript須要爲你的變量肯定一個類型,這個過程就叫widening 考慮以下代碼

const mixed = ['x', 1]
複製代碼

上述代碼中的mixed的應該被推導爲何類型

  • ('x'|1)[]
  • ['x',1]
  • [string,number]
  • readonly [string,number]
  • (string|number) []
  • readonly (string|number)[]
  • [any,any]
  • any[]

上述答案彷佛都合理,事實上Typescript只能根據你的使用狀況 進行猜想推導,並不能徹底知足你的需求。

const mixed = ['x',1]
//使用方式1
mixed.push(1) //(string|number)[] 更爲合理
//使用方式二
function test(a:string,b:number){
}
test(...mixed) // [string,number] 更爲合理
//使用方式三
function test2(a:'x',b:1){
}
test2(...mixed) // ['x',1] 更合理
複製代碼

咱們發現不一樣的使用場景須要的類型是不一樣的,事實上Typescript只能根據 大部分使用場景進行類型推斷

literal widening

當發生literal widening 時,'foo',1,ColorEnum.RED等unit type會被視爲其base type即string,number, ColorEnum 觸發literal widening的條件爲

  • mutable location會觸發(如let)
let x = 3 // widening,類型爲number
const x = 3 // 不觸發weidening,類型爲3
複製代碼

對於primitive type,const可以控制其不會觸發widening,可是 對於object和array這些複合對象,const並不能控制屬性的widening

const obj = {
  name: 'yj'
} // 推導類型爲 { name: string}

const arr = [1,'x'] // 推導類型爲(string|number)[]
複製代碼

上述類型推導大部分狀況下是合理的

const obj = {
  name
}
obj.name = 'zrj' // 後續修改props

const arr = [1,'x']
arr.push(3) 
複製代碼

但有時候咱們須要進一步的控制屬性的widening,此時有兩種方式

  • 顯示的類型標註
const arr1: [1,'x'] = [1,'x'] // 類型爲[1,'x']
arr[0] = 2; // check error

const arr: readonly [number, string] = [1, 'x']

arr.push(3); // check error
複製代碼
  • as const
const arr = [1,'x'] as const
arr.push(3) // check error
複製代碼

理解Type Narrowing

type narrowing與type widening相反,其負責收窄類型

  • 對於大部分類型使用內置的類型收窄便可,支持的類型收窄操做包括

    • Array.isArray
    • instanceof
    • key in object
    • typeof
    • falsy 判斷
  • 對於更加複雜的對象則須要使用自定義類型收窄和tagged union來支持類型收窄

    • tagged union
interface Point {
  type: 'point',
  x: number,
  y: number
}
interface Radius{
  type: 'radius',
  r: number
}
function distance(p: Point | Radius){
  if(p.type === 'point'){
    return Math.sqrt(p.x*p.x + p.y*p.y)
  }else {
    return p.r
  }
}
複製代碼

對於更加的複雜的類型,可使用自定義類型判斷

function isInputElement(el: HTMLElement): el is HTMLInputElement {
  return 'value' in el;
}
function getElementContent(el: HTMLElement){
  if(isInputElement(el)){
    return el.value // el 爲HTMLInputElement類型
  }else {
    return el.textContent // el爲HTMLElement類型
  }
}
複製代碼

一次性的定義好對象

考慮以下代碼,雖然是合法的js代碼,可是TS仍然會報錯

const pt = {}
pt.x = 3; // check error 
pt.y = 4
複製代碼

這是由於pt定義是類型被推導爲{} 正確的作法應該是

const pt = {x :3, y: 4}
複製代碼

若是須要經過一系列對象構造出新對象,應儘可能使用spread 操做, 能夠保證生成的對象類型安全

const pt =  { x:3,y:4}
const id = {name: 'point'}
const namedpoint = {}
Object.assign(namedpoint, pt, id)
namedpt.name // check error 
複製代碼

正確的作法應該是

const pt = { x:3, y: 4}
const id = { name: 'point'}
const namedpoint = {...pt, ...id}
namedpoint.name // 正常
複製代碼

若是是須要合併部分屬性,則須要配合Partial使用

const pt = { x:3, y: 4}
const id = { name: 'point'}
function merge<T extends object, U extends object>(x: T, y: U): T & Partial<U> {
  return {...x,...y}
}
const p = merge(pt, id)
p.name // 類型爲string | undefined
複製代碼

使用alias時保持一致

當對變量進行narrowing時,並不會同步的對其alias進行narrowing

interface Test {
  name?: string;
}
const obj: Test = {}
const name = obj.name
if (obj.name) {
  obj.name.toLowerCase(); // ok
  name.toLowerCase(); // check error
}
複製代碼

雖然這裏的name和obj.name是一致的可是,name並不受obj.name影響, 所以當使用alias和narrow時得注意保持一致

異步處理時使用async函數替換callback

充分利用函數庫(如lodash)來簡化代碼裏的類型處理,和避免顯式的類型標註

類型設計

避免同時使用多個變量來建模狀態,而是使用單一變量來區分不一樣的狀態

考慮下面的組件

const App = () =>  {
  const [content,setContent] = useState('')
  const [loading, setLoading] =useState(false);
  const [error, setError] = useState(null);
  function load(){
    setLoading(true);
    try {
      const resp = await fetch(getUrlForPage());
      if(!resp.ok){
        throw new Error('unable to load')
      }
      const text = await resp.text();
      setLoading(false);
      setContent(text);
    }catch(e){
      setError(e);
    }
  }
 
  if(error){
      return 'Error';
    }else if(loading){
      return 'loading'
    }
    return <h1>{content}</h1>
  }
}
複製代碼

上面的代碼明顯存在一些問題

  • 請求失敗時忘記重置loading狀態
  • 忘記狀況error狀態
  • 從新拉接口時,狀態容易錯亂

因爲Error, Loading,Content等狀態其實是互斥的,所以能夠用一個變量經過tagged union來建模狀態 重構代碼以下

interface RequestPending {
  state: 'pending'
}
interface RequestError {
  state: 'error',
  error: string;
}
interface RequestSuccess {
  state: 'ok',
  content: string;
}
type RequestState = RequestError | RequestPending | RequestSuccess

const App = () =>  {
  const [state, setState] = useState<RequestState>({
    state: 'ok',
    content: ''
  })
  function load(){
    setState({
      state: 'pending'
    })
    try {
      const resp = await fetch(getUrlForPage());
      if(!resp.ok){
        throw new Error('unable to load')
      }
      const text = await resp.text();
      setState({
        state: 'ok',
        content: text
      })
    }catch(error){
      setState({
        state: 'error',
        error
      })
    }
  }
  switch(state.type){
    case 'pending':
        return 'pending',
    case 'error':
        return state.error
    case 'ok':
        return <h1>{state.content}</h1>
  }
}
複製代碼

此時就徹底避免了上面存在的幾個問題,並且後續每次增長新的狀態 Typescript均可以幫咱們進行類型檢查

對入參款寬鬆對出參嚴格

不要在jsdoc裏記錄類型信息

/** * Return a string with the backgroudColor * */
function getBackgroundColor(){
  // return 'red' // 老代碼
  return (255,255,255); // 重構後的代碼
}
複製代碼

重構時會忘記更改文檔裏的類型信息,致使不一致,對於量綱的信息, 因爲難以使用類型進行標記,因此能夠在文檔裏標註

/** * duration: timeMs 表示ms * */
function sleep(duration: number){

}
複製代碼

儘可能減少null|undefined的影響區域(儘可能開啓strictNullCheck檢查)

考慮下面代碼,實現了一個肯定數組範圍的函數

function extent(nums: number[]) {
  let min, max;
  for (const num of nums) {
    if (!min) {
      min = num;
      max = num;
    } else {
      min = Math.min(min, num);
      max = Math.max(max, num);
    }
  }
  return [min, max];
}
複製代碼

上述代碼存在一些問題

  • 若是數組裏含有0,0會被排查出區間範圍(if(!min)的判斷致使 0 | null | undefined都被排除,可是0

的排除非咱們本意)

  • 若是數組爲空,結果爲[undefined,undefined]

當開啓了strictNullCheck下上述代碼會報錯

function extent(nums: number[]) {
  let min, max;
  for (const num of nums) {
    if (!min) {
      min = num;
      max = num;
    } else {
      min = Math.min(min, num);// number|undefined is not assignable to 'number'
      max = Math.max(max, num);
    }
  }
  return [min, max];
}
複製代碼

重構上述代碼以下

function extent(nums:number[]){
  let result: [number,number] | null = null;
  for(const num of nums){
    if(!result){
      result = [num, num]
    }else {
      result = [Math.min(num,result[0]), Math.max(num,result[1])]
    }
  }
  return result;
}
複製代碼

上述代碼解決了以前的問題,其最大的區別在於,保證了循環裏的 result[0]和result[1]都不含有undefined|null,防止其影響了 正常的代碼判斷

優先使用 union of interface而非 interfaces of unions

考慮下述類型定義

interface Layer {
  layout: FillLayout | LineLayout | PointLayout;
  paint: FillPaint | LinePaint | PointPaint
}
複製代碼

這樣設計的類型很難關聯layout和對應的paint,重構以下

interface FillLayer {
  type: 'fill',
  layout: FillLayout,
  paint: FillPaint
}
interface LineLayer {
  type: 'line',
  layout: LineLayout,
  paint: LinePaint
}
interface PointLayer {
  type: 'paint',
  layout: PointLayout,
  paint: PointPaint
}

type Layer = FillLayer | LineLayer |PointLayer
複製代碼

這實際上就是tagged union,能夠經過type進行narrowing操做

function drawLayer(layer: Layer) {
  if (layer.type === 'fill') {
    const {paint} = layer;  // Type is FillPaint
    const {layout} = layer;  // Type is FillLayout
  } else if (layer.type === 'line') {
    const {paint} = layer;  // Type is LinePaint
    const {layout} = layer;  // Type is LineLayout
  } else {
    const {paint} = layer;  // Type is PointPaint
    const {layout} = layer;  // Type is PointLayout
  }
}
複製代碼

使用更細化的string類型,優先考慮使用string literal union

相比不許確的類型考慮使用不完備的類型

使用brands來模擬nominal typing

考慮下面的case,Point是使用直角座標表示的點, 而RadiusPoint則是使用極座標表示的點

interface Point {
  x: number,
  y: number
}
interface RadiusPoint{
  x: number // radius
  y: number // theta
}
function PointDistance(p:Point){
  return Math.sqrt(p.x**2 + p.y**2)
}
let p1: Point;
let p2: RadiusPoint

PointDistance(p1);
PointDistance(p2); // 應該報錯但不報錯
複製代碼

雖然這裏的PointDistance要求類型是Point,但因爲是Typescript使用的是structual typing,致使 實際上能夠將RadiusPoint類型的變量也能夠傳遞進去,致使計算錯誤 咱們能夠經過添加一個brand標記區分二者

interface Point {
  _brand: 'point',
  x: number,
  y: number
}
interface RadiusPoint{
  _brand: 'radius',
  x: number // radius
  y: number // theta
}
function PointDistance(p:Point){
  return Math.sqrt(p.x**2 + p.y**2)
}

PointDistance(p1);
PointDistance(p2); // 正常報錯
複製代碼

處理any

縮小any的影響範圍

function f1(){
  const x: any = expressionReturningFoo(); // 不建議,後續的x都是any了
  processBar(x)
}

function f2(){
  const x = expressionReturningFoo();
  processBar(x as any) // 建議,只有這裏是any
}
複製代碼

使用更細化的any

function getLengthBad(arr:any){
  return array.length; // 不推薦
}
function getLength(array:any[]){
  return array.length //推薦
}

const numArgsBad = (...args:any) => args.length //Return any 不推薦
const numArgs = (...args: any[]) => args.length // Return number 推薦
複製代碼

函數簽名和實現想分離:安全的簽名不安全的實現

有時候不使用any想編寫一個徹底類型安全的實現並不是易事,可是通常對於使用者 並不關心內部的實現是否安全,只關心對外暴露的簽名是否安全,此時咱們能夠將函數簽名和 函數實現相分離,以簡化內部的類型實現。這個技巧充分利用了當使用重載時,只有函數簽名對外可見, 而函數實現對外不可見 use-immer裏即便用了該技巧

// 類型安全的簽名
export function useImmer<S = any>( initialValue: S | (() => S) ): [S, (f: (draft: Draft<S>) => void | S) => void];
// 沒那麼安全的實現
export function useImmer(initialValue: any) {
  const [val, updateValue] = useState(initialValue);
  return [
    val,
    useCallback(updater => {
      updateValue(produce(updater));
    }, [])
  ];
}
複製代碼

理解進化的any

Typescript中的any並非一成不變的,會隨着用戶的操做,Typescript會猜想更加合理的類型

const output = [] // any[]
output.push(1) 
output // number[]
output.push('2')
output // (number|string)[]
複製代碼

優先使用unknown而非any

考慮下述代碼

function parseYAML(yaml:string):any{

}

const book = parseYAML(` name: effective typescript author:yj `)
console.log(book.title) // no error
book('read') // no error
複製代碼

咱們發現上述代碼在該報錯的地方並無報錯, 更加安全的是使用unknown和配合自定義type guide

function parseYAML(yaml:string):unknown{

}

const book = parseYAML(`name: effective typescript author:yj`)
console.log(book.title) // 報錯 
book('read') // 報錯
interface Book {
  name: string;
  author: string;
}
function isBook(val:unknown): val is Book {
  return (
    typeof val === 'object' && val !== null && 'name' in val && 'author' in val
  )
}
if(isBook(booke)){
  console.log(book.title)
}
複製代碼

同時須要區分{}和object和unknown

  • {}: 包含除了null和undefined以外的全部值
  • object: 包含了全部的非primitive類型,即不包含12,'test'等基本類型

在引入unknown以前,多使用{},在引入unknown以後,基本上不須要再使用{}類型

使用type-coverage測試type的覆蓋率

type聲明和@types

將@types的依賴放在devDependencies裏

將公用API裏使用的類型也一併導出

使用TSDOC去註釋導出的函數,class,types

爲callback提供this的類型

考慮下面函數

class C {
  vals = [1, 2, 3];
  logSquares() {
    for (const val of this.vals) {
      console.log(val * val);
    }
  }
}

const c = new C();
c.logSquares();
const c2 = new C();
const method = c.logSquares;
method(); // check ok, 可是運行時報錯
複製代碼

上面的method函數調用Typescript並未檢查到其錯誤使用,致使其在運行時報錯, 咱們能夠爲logSquares提供this的類型杜絕錯誤的使用

class C {
  vals = [1, 2, 3];
  logSquares(this: C) { // 顯示代表要求的this類型
    for (const val of this.vals) {
      console.log(val * val);
    }
  }
}

const c = new C();
c.logSquares();
const c2 = new C();
const method = c.logSquares;
method(); // check ok, 可是運行時報錯
複製代碼

儘可能避免用戶對@types的依賴,不要強制web用戶依賴NodeJS的types

編寫代碼最佳實踐

優先考慮使用Javascript的語言特性而非Typescript獨有的語言特性

Typescript獨有的一些語言特性包括

  • Enums
enum Color {
  RED,
  BLUE
}
複製代碼
  • Parameter Properties
class Person {
  constructor(public name: string)
}
複製代碼
  • Namespaces 和 triple-slash imports
namespace foo {
  function bar(){}
}
/// <reference path="other.ts">
foo.bar()
複製代碼
  • Decorators
class Greeter {
  @logged
  greet(){
    return 'hello'
  }
}
複製代碼

使用Object.entries去遍歷對象

interface ABC{
  a:string;
  b:string;
  c:string;
}
function foo(abc:ABC){
  for(const [k,v] of Object.entries(abc)){
    console.log(k,v)
  }
}
複製代碼

理解DOM的層級關係,瞭解Node,Element,HTMLElement,EventTarget,Event等的區別

private在運行時並不能阻止外部用戶訪問

使用sourcemap去debug Typescript 程序

相關文章
相關標籤/搜索