typescript之defaultProps


React 之 Default Prop Values

React 官方文檔 - Default Prop Values

方式一: Class 類名.屬性名

通過組件的 defaultProps 屬性可為其 Props 指定默認值。

class Greeting extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

// Specifies the default values for props:
Greeting.defaultProps = {
  name: "Stranger",
};

// Renders "Hello, Stranger":
ReactDOM.render(<Greeting />, document.getElementById("example"));

方式二: static 屬性名

如果編譯過程使用了 Babel 的 transform-class-properties 插件,你可以在 React 類組建中定義一個靜態變量 defaultProps

class Greeting extends React.Component {
  static defaultProps = {
    name: "stranger",
  };

  render() {
    return <div>Hello, {this.props.name}</div>;
  }
}

加入 TypeScript

方式一: Class 類名.屬性名(報錯)

interface IProps {
  name?: string;
}

class Greeting extends React.Component<IProps, {}> {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

Greeting.defaultProps = {
  name: "Stranger",
};

此時不支持直接通過類訪問 defaultProps 來賦值以設置默認屬性,因為 React.Component 類型上並沒有該屬性。

// ?Property 'defualtProps' does not exist on type 'typeof Greeting'.ts(2339)
Greeting.defualtProps = {
  name: "stranger",
};

方式二: static 屬性名

上面雖然實現了通過 defaultProps 來指定屬性的默認值,但 defaultProps 的類型是不受約束的,和 Props 沒有關聯上。以至於我們可以在 defaultProps 里面放任何值,顯然這是不科學的。

class Greeting extends React.Component<IProps, {}> {
  static defaultProps = {
    name: "stranger",
    // 並不會報錯
    foo: 1, // +
    bar: {}, // +
  };
  // ...
}

同時對於同一字段,我們不得不書寫兩次代碼。一次是定義組件的 Props,另一次是在 defaultProps 里。如果屬性有增刪或名稱有變更,兩個地方都需要改。

Partial<typeof defaultProps>

為了后面演示方便,現在給組件新增一個必填屬性 age:number。

interface IProps {
  age: number;
  name?: string;
}

class Greeting extends React.Component<IProps, {}> {
  static defaultProps = {
    name: "Stranger",
  };

  render() {
    const { name, age } = this.props;
    return (
      <h1>
        Hello, {name}, my age is {age}
      </h1>
    );
  }
}

通過可選屬性抽取出來,利用 typeof 獲取其類型和必傳屬性結合來形成組件的 Props 可解決上面提到的兩個問題。

所以優化后的代碼成了:

const defaultProps = {
    name: 'Stranger'
};

type IProps {
    age: number;
} & Partial<typeof defaultProps>;


class Greeting extends React.Component<IProps, {}> {
    static defaultProps = defaultProps;

    render() {
        const { name, age } = this.props;
        return (
        <h1>Hello, {name}, my age is {age}</h1>
        );
    }
}

注意我們的 Props 是通過和 typeof defaultProps 組合而形成的,可選屬性中的 name 字段在整個代碼中只書寫了一次

當我們更新了 defaultProps 時整個組件的 Props 也同步更新,所以 defaultProps 中的字段一定是組件所需要的字段

默認值的判空檢查優化

如果屬性提供了默認值,在使用時,可不再需要判空,因為其一定是有值的。但 TypeScript 在編譯時並不知道,因為有默認值的屬性是被定義成可選的 ?。

比如我們嘗試訪問 name 屬性的長度,

class Greeting extends React.Component<IProps, {}> {
  static defaultProps = defaultProps;

  render() {
    const { name } = this.props;
    return (
      <div>
        {/* ?Object is possibly 'undefined'.ts(2532) */}
        name length is {name.length}
      </div>
    );
  }
}

因為此時我們的 Props 實際上是:

type Props = {
  age: number,
} & Partial<typeof defaultProps>;
// 相當於:
type Props = {
  age: number,
  name?: string,
};

非空判定符

- name length is {name.length}

* name length is {name?.length}

這意味着每一處使用的地方都需要做類似的操作,當程序復雜起來時不太可控。但多數情況下應付日常使用,這樣已經夠了。

✨ 類型轉換

因為組件內部有默認值的保證,所以字段不可能為空,因此,可對組件內部使用非空的屬性類型來定義組件,而對外仍暴露原來的版本。

const Greeting = class extends React.Component<
-  IProps,
+  IProps & typeof defaultProps,
  {}
> {
  static defaultProps = defaultProps;

  render() {
    const { name } = this.props;
    return (
      <div>
-        name length is {name!.length}
+        name length is {name.length}
      </div>
    );
  }
-};
+} as React.ComponentClass<IProps>;

通過 as React.ComponentClass<Props> 的類型轉換,對外使用 Greeting 時屬性中 name 還是可選的,但組件內部實際使用的是 Props & typeof defaultProps,而不是 Partial<T> 版本的,所以規避了字段可能為空的報錯。

通過高階組件的方式封裝默認屬性的處理

通過定義一個高階組件比如 withDefaultProps 將需要默認屬性的組件包裹,將默認值的處理放到高階組件中,同樣可解決上述問題。

function withDefaultProps<P extends object, DP extends Partial<P>>(
  dp: DP,
  component: React.ComponentType<P>,
) {
  component.defaultProps = dp;
  type RequiredProps = Omit<P, keyof DP>;
  return (component as React.ComponentType<any>) as React.ComponentType<
    RequiredProps & DP
  >;
}

然后我們的組件則可以這樣來寫:

const defaultProps = {
  name: "stranger",
};

interface Props {
  name: string;
  age: number;
}

const _Greeting = class extends React.Component<Props, {}> {
  public render() {
    const { name } = this.props;
    return <div>name length is {name.length}</div>;
  }
};

export const Greeting = withDefaultProps(defaultProps, _Greeting);

這種方式就比較通用一些,將 withDefaultProps 抽取成一個公共組件,后續其他組件都可使用。但此種情況下就沒有很好地利用已經定義好的默認值 defaultProps 中的字段,書寫 Props 時還需要重復寫一遍字段名。

方法組建 defaultProps

interface IProps {
  name?: string;
}

function Greeting(props: IProps) { {
   return <h1>Hello, {props.name}</h1>;
}

Greeting.defaultProps = {
  name: "Stranger",
};

或者

interface IProps {
  name?: string;
}

function Greeting({ name = 'Stranger' }: IProps) { {
   return <h1>Hello, {name}</h1>;
}

理解 Partial

type Partial<T> = {
    [P in keyof T]?: T[P];
};

假設我們有一個定義 user 的接口,如下

interface IUser {
  name: string
  age: number
  department: string
}

經過 Partial 類型轉化后得到

type optional = Partial<IUser>;

// optional的結果如下
type optional = {
  name?: string,
  age?: number,
  department?: string,
};

那么 Partial<T> 是如何實現類型轉化的呢?

1.遍歷入參 T ,獲得每一對 key, value

2.將每一對中的 key 變為可選,即添加 ?

3.希望得到的是由 key, value 組成的新類型

以上對應到 TypeScript 中是如何實現的呢?

keyof、in、T[P]

對照最開始 Partial 的類型定義,能夠捕捉到以下重要信息

  • keyof 是干什么的?

  • in 是干什么的?

  • [P in keyof T]中括號是干什么的?

  • ? 是將該屬性變為可選屬性

  • T[P] 是干什么的?

keyof

keyof,即 索引類型查詢操作符,我們可以將 keyof 作用於泛型 T上來獲取泛型 T 上的所有 public 屬性名構成的 聯合類型

注意:"public、protected、private"修飾符不可出現在類型成員上
例如:

type unionKey = keyof IUser

// unionKey 結果如下,其獲得了接口類型 IUser 中的所有屬性名組成的聯合類型
type unionKey = "name" | "age" | "department"

in

我們需要遍歷 IUser ,這時候 映射類型就可以用上了,其語法為 [P in Keys]

  • P:類型變量,依次綁定到每個屬性上,對應每個屬性名的類型
  • Keys:字符串字面量構成的聯合類型,表示一組屬性名(的類型),可以聯想到上文 keyof 的作用
    上述問題中 [P in keyof T] 中括號是干什么的?這里也就很清晰了。

T[P]

我們可以通過 keyof 查詢索引類型的屬性名,那么如何獲取屬性名對應的屬性值類型呢?

這里就用到了 索引訪問操作符,與 JavaScript 種訪問屬性值的操作類似,訪問類型的操作符也是通過 [] 來訪問的,即 T[P],其中”中括號“中的P[P in keyof T] 中的 P 相對應。

例如

type unionKey = keyof IUser // "name" | "age" | "department"

type values = IUser[unionKey] // string | number 屬性值類型組成的聯合類型

最后我們希望得到的是由多個 key, value 組成的新類型,故而在 [P in keyof T]?: T[P]; 外部包上一層大括號。

到此我們解決遇到的所有問題,只需要逐步代入到 Partial 類型定義中即可。

相關資源


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM