🏃♂️ TypeScript 以及一些理解和技巧
Banner from typescript
!本篇文章过于久远,其中观点和内容可能已经不准确,请见谅!~
真正的面向接口编程,更自然完备的模块和逻辑抽象,让前端开发的思维方式从怎么实现结果,变成了更高质量的需求交付,去思考接口的输入输出,去设计模块和代码,更何况仅仅是代码提示和对接联调就足够让你爽翻天了。
复杂模块之间的集成从来不是那么顺利,功能的升级重构、项目的交接一直是开发很重要的一部分,这块经常会出现问题,因为并不是每个人、每次都能清楚的记住项目的接口、参数、类型等,这块出现的错误在前端占了很大一个比例,所以很多的测试才能覆盖这块的质量保证。可是,这对于静态语言其实并不是问题。
之前的前端代码提示依赖于 document 注释,这其实对很多人是一种负担,很多人只使用内置的官方提示,不能享受类型思维的好处。TS 使用一些标记和类型声明能够很好的实现完整的代码提示,带有代码提示的编程体验能让效率和质量更好,至少代码质量里面静态检查没有问题了。
这都是 Typescript 带来的重要特性,TS 能帮我们梳理清不同的接口、天生的让你知道这个接口应该怎么样的,在运行时才能看到的问题在开发的时候就很明显的暴露了。
TL;DR: 内部使用只要程序一致,都可以,type 更短更灵活。提供第三方库声明的时候请使用 interface。
刚接触的时候都会纠结到底是用哪个来定义类型,官网的说明中一般是这么叫的
interface = interface types
接口类型,type = type aliases
类型别名。但是官网很多表述现在有点问题,目前可以参考 Interface vs Type alias in TypeScript 2.7。命名上一个强调接口,一个强调别名,所以按照我的理解:
- interface 作为一个整体来描述一个实体接口。
- type 作为别名使用,一般较多的表示集合概念。
- 二者在表示常见的结构时,可以互相转换。
比如
interface
强调 People
人的数据结构,完整的类型。type
强调一个有姓名、年龄等属性的集合体,别名和集合。我们平时使用的时候,不需要刻意强调哪个是对的,因为大部分时候无论声明 interface
还是 type
对于程序逻辑都是有意义可推断的,大部分时候也是可以互相转换的。interface People {name: string;age: number;}
大部分情况可以写成:
type People = {name: string;age: number;}
二者之间一些不同的语法,比如只有
interface
可以有声明合并 declaration merging
一般多人协作或者第三方接口扩展比较有用。接触 TS 刚开始,很多的类型声明认为没必要,可以省略,所以添加了很多的 any 到代码里面,但是这是很不好的编程实践。
除非第三方代码,不要使用 any 来仅仅为了去除红线提示,可以使用自动推断来减少 any 的使用。
- any 是一个代码 bug 隐患
- 代码的每一个角落都需要类型声明,不要以为模块简单、非核心就直接 any 了事
- 代码的重构如果没有类型声明支持将会很不可靠
- 没有自动补全支持
尽可能的将不能自动推断的类型手动标注类型声明,这是编程的一部分。
// 明确类型的值初始化,能够自动推倒变量类型let someVal = 10;const someObj = { key1: 'val1', key2: 100 }// ======== 但是不能完备初始化的除外,例如下面就会出错const someObj2 = {}someObj2.x = 3;someObj2.y = 4
- any 表示“这个数据可能是任何类型,不用管”
- unknow 表示“这个数据不知道是什么类型,需要进一步检查”
let a: any = '123';a.say(); // 静态检查不会提示问题let a: unknow = '123';a.say(); // 静态检查会提示 "Object is of type 'unknown'"// 自动推断后收窄类型,允许静态检查通过if (typeof a.say === 'function') {a.say();}
unknown 类型要比 any 安全得多,强制我们必须进行检查才能进一步执行,能够避免运行时的类型错误。但是可能这个强制性有些工作量,所以可能很多人还是 any。
保护性类型、兜底类型、底部类型、零类型或空类型等叫法,比如下面一个常见的需求:
type Foo = string | number;function runBar(foo: Foo) {if(typeof foo === "string") {// 这里 foo 被收窄为 string 类型} else if(typeof foo === "number") {// 这里 foo 被收窄为 number 类型}}
这段代码是没有问题的,接收的值有不同的类型有不同的处理,静态检查、运行时都能通过。
但是如果协作的时候出现问题:
- type Foo = string | number;+ type Foo = string | number | boolean;function runBar(foo: Foo) {if(typeof foo === "string") {// 这里 foo 被收窄为 string 类型} else if(typeof foo === "number") {// 这里 foo 被收窄为 number 类型}}
这个时候静态检查不会报错,运行时也不会报错,但是逻辑上就是有问题的,所以刚开始应该使用 never 来控制类型的流动。
type Foo = string | number;function runBar(foo: Foo) {if(typeof foo === "string") {// 这里 foo 被收窄为 string 类型} else if(typeof foo === "number") {// 这里 foo 被收窄为 number 类型} else {// foo 在这里是 neverconst check: never = foo;}}
别人改动之后编译阶段就能发现这里的问题。
interface ComponentProps {prop1: string;}interface ComponentPropsWithChildren{prop1: string;children: React.ReactNode}// =========== 可以转化为import { PropsWithChildren } from 'react';interface ComponentPropsWithChildren = PropsWithChildren<ComponentProps>
用 Pick 操作简化类型声明:
interface Adult {name: string;age: number;job: string;}interface Child {name: string;age: number;}// =========== 可以转化为type Child = {[k in 'name' | 'age'] : Adult[k]}// =========== 也可以转化为type Child = Pick<Adult, 'name', 'age'>
type SomeObject = { [ key: string ]: string }// =========== 也可以转化为type SomeObject = Record<string,string>
interface UserLoginType {username: string;id: number;avatar: string;token: string;}// 用 Omit 剔除属性type UserInfo = Omit<UserLoginType, 'token'>
可以避免一些重复的代码
TS 的泛型是很强大的概念,在很多框架中都能看到身影,是静态类型推断很重要的一个地方。ts 的 lib 声明文件中比如
lib.es5.d.ts
有很多有意思的辅助泛型类型。每一个都很简洁,都是抽象出来的类型操作,比如上面说的
Pick
:class Model<S> {state: Readonly<S>;setState = <K extends keyof S>(state: Pick<S, K>) => {this.state = { ...this.state, ...state };};}// ========= 很简单的用 Pick 来只允许目标类型的 key 的输入type TodoState = {list: string[];lastUpdate: string;}const todoModel = new Model<TodoState>();todoModel.setState({list: ['a1', 'b2'],lastUpdate: '12:01',})todoModel.setState({unknowKey: '123' // 类型“{ unknowKey: string; }”的参数不能赋给类型“Pick<TodoState, "list" | "lastUpdate">”的参数。 ts(2345)})
/*** Extracts the type of the 'this' parameter of a function type, or 'unknown' if the function type has no 'this' parameter.*/type ThisParameterType<T> = T extends (this: infer U, ...args: any[]) => any ? U : unknown;/*** Removes the 'this' parameter from a function type.*/type OmitThisParameter<T> = unknown extends ThisParameterType<T> ? T : T extends (...args: infer A) => infer R ? (...args: A) => R : T;/*** Make all properties in T optional*/type Partial<T> = {[P in keyof T]?: T[P];};/*** Make all properties in T required*/type Required<T> = {[P in keyof T]-?: T[P];};/*** Make all properties in T readonly*/type Readonly<T> = {readonly [P in keyof T]: T[P];};/*** From T, pick a set of properties whose keys are in the union K*/type Pick<T, K extends keyof T> = {[P in K]: T[P];};/*** Construct a type with a set of properties K of type T*/type Record<K extends keyof any, T> = {[P in K]: T;};/*** Exclude from T those types that are assignable to U*/type Exclude<T, U> = T extends U ? never : T;/*** Extract from T those types that are assignable to U*/type Extract<T, U> = T extends U ? T : never;/*** Construct a type with the properties of T except for those in type K.*/type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;/*** Exclude null and undefined from T*/type NonNullable<T> = T extends null | undefined ? never : T;/*** Obtain the parameters of a function type in a tuple*/type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;/*** Obtain the parameters of a constructor function type in a tuple*/type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;/*** Obtain the return type of a function type*/type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;/*** Obtain the return type of a constructor function type*/type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any;/*** Marker for contextual 'this' type*/interface ThisType<T> { }
类型声明很好用,但是没办法加注释,注释的添加还是依靠 JSDoc 等工具,但是不需要写类型(避免类型与 ts 不一致)。
interface People {/*** 用户名*/name: string;}/*** uid: user id 用户的 id*/function callSomeOne(uid: number){// # ...}
这个并不是 ts 的特性,很多时候看到了不同的代码风格,比如下面的两种:
class DemoCom extends React.Component<{}, TDemoState> {state = {statusPool: {}}action = () => {this.state}};
class DemoCom extends React.Component<{}, TDemoState> {constructor(){super();this.state = {statusPool: {}}}action = () => {this.state}};
之前我一直很好奇后面这个是要干嘛,这岂不是很奇怪的做法吗?明明静态的赋值就能初始化,为什么还要在 constructor 里面运行时初始化呢?
现在明白了,因为 React.Component 这个父类已经在初始化的时候拿到了 state 的泛型,所以如果直接使用 this.state 能够看到代码提示。
方法一里面的属性,覆盖了父组件的属性,对于 ts 来说,这个 state 的泛型就失效了,当然 setState 方法的提示还是支持的,但是直接 this.state 访问就变成了当前子类的 state 的类型推断了,除非显式再指定类型。
而方法二没有用新属性覆盖父类的属性,而是在父类上赋值,所以代码提示依然用的是父类的。
class DemoCom extends React.Component<{}, TDemoState> {// 使用这种赋值方法会覆盖掉父组件的 state 类型声明,代码提示就会变成这个变量// state = {// statusPool: {}// }// 使用这种赋值方法会覆盖掉父组件的 state 类型声明,但是显式指定又重新指定了// state: IStatusBarState = {// statusPool: {}// }constructor(){super();// 使用赋值法,可以避免声明覆盖父组件的 state 类型声明,省去了显式指定this.state = {statusPool: {}}}action = () => {this.state}};
感谢您的阅读,本文由 Ubug 版权所有。如若转载,请注明出处:Ubug(https://ubug.io/blog/typescript)