当我开始使用TypeScript时我希望知道的5个技巧

Posted TypeScript中文圈

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了当我开始使用TypeScript时我希望知道的5个技巧相关的知识,希望对你有一定的参考价值。

原作者是一位热衷函数式编程的JS全栈工程师。以下为译文。


我已经在一个重要的项目(trainline.com)中使用一年多的 TypeScript 了。我觉得这是一段愉快的经历,并且当你和很多开发者共用一个代码库时,TypeScript有很多好处。如果你也认为 TypeScript 可以帮助你,在这里我会提供一些建议,这些建议是我认为开始使用这个工具时应该知道的。

1)严格的配置

如果在使用 TypeScript过程中没有开启下面的几个配置项,我想你会错过很多有价值的部分。我建议强制采取更严格的配置并且默认启用,否则,你的类型会很随意,而这正是我们在 TypeScript 中尽量去避免的。  下面就是为了使用严格的配置你需要在 tsconfig.json 中添加的内容:

{     
   "forceConsistentCasingInFileNames": true,    
   "noImplicitReturns": true,      
   "strict": true,    
   "noUnusedLocals": true,
}

上面最重要的配置就是 " strict ": true,它包含了四个可以独立添加的选项,如果你想在现有代码库中逐步引入TypeScript,并且随着时间逐渐变得更严格,可以按照这个顺序来:noImplicitAnynoImplictThisalwaysStrictstrictNullChecks。我将详细说明对我而言最重要的两个:

* strictNullChecks

这个选项会确保你不会访问到任何值为null的属性,请看下面的代码:

interface Foo {     
   bar:string;
}
const fn = ( foo?: Foo ) => foo.bar

上面的代码在 strictNullCheck 模式下不会被编译,因为 foo 是一个可选的参数,当我们试图确认它的值时,它有可能是undefined。这里的问号(可选参数)对于编译器来说和下面定义的函数类型是一样的

const fn = ( foo: Foo | undefined ) => foo.bar;  

如果你不试图欺骗编译器并且仍然在你的程序中保持严格类型,可以预见你将永远不会在运行时报下面的错

TypeError: Cannot read property  ' foo ' of undefined  

理论上来说,你可以在编译时捕获这些错误,对我来说,这就是我使用TypeScript的全部要点。通过在编译时强制执行该操作,你将不得不去提前考虑你定义数据的类型,并且你肯定会停止使用一些你不知道静态类型的写法。

* noImplicitAny

这个参数意味着你必须在程序中定义每一种类型,例如下面的代码段是不会被编译的 :

const fn = ( foo ) = > foo.bar  

但是,如果你不开启 noImplicitAny 这个参数,”foo” 会被隐式地声明为any类型,而这是应该被避免的。当你定义任何类型的东西(隐式或者显式),你都应当确认它是正确的。尽管强类型语言通常不会有如此灵活的类型抽象是any 类型。  

所以正如我所说,我们不想在程序中使用any 类型,并且 noImplicitAny 模式将会禁止所有编译器为我们推断出的隐式类型。在 TypeScript 中,为了使用完全严格类型检查需要使用 TSLint 并且设置下面的规则: 

"no-any" : true  

当你在程序中显式的声明 “ any “ 类型,并且没有设置上面的规则为 false ,这条规则就会失效。这在你和其他很多开发者一起工作时是必须的,这对每个人甚至一些懒惰的同事都会强制执行。尽管你可能会遇到一些阻碍,但是我认为如果你和你的团队花时间去理解静态类型的好处,那么一切就都不是问题了,这个新工具会很受欢迎的。另外,我将在下一节展示一种方法远离所有可能被隐式声明的静态类型。

2)类型推断

处理静态类型语言另一个重要话题是类型推断。不好的类型推断对我来说是个难题。任何现代静态类型语言都应该有一个很好的类型推断系统,TypeScript也在逐渐变得更好。自从TypeScript 2.0推出以来,每个小版本都包含了类型推断的改进。让我们来看一些TypeScript可以处理的很好的实例:  

首先,一个很基础的例子,你不需要定义任何常量的类型: 

const a = 'foo' ;  

这会推断出 “ a “ 是 “ foo “ 的类型,即 “ string literal “ 类型,不是任意的 string 类型,而在其他很多语言中不会有这样的准确性,这些语言的编译器很有可能推断出是 “ string “ 类型。  

你应该始终让编译器去自动推断你的类型并且尽量避免显式声明类型,因为编译器通常比你更好的了解类型到底是什么。如果你想在程序中随时了解类型,好的 IDE(比如 VSCode )会有一个工具提示集成,它会给你提供这个信息。  现在让我们看另一个代码片段:

const a = 'foo'; 
const func = (foo: string) => ({ foo });
const b = func(a);

在这我没有具体指定函数 func 的返回值类型,推断的类型是 { foo : string } ,最后是一个符合下面类型声明的函数。

type func = (foo : string) => {foo: string;}  

因此如果我声明一个 Foo 接口 (interface) 并且将函数 func 的结果指定为Foo 类型,编译器会正确的编译:

interface Foo {     
   foo: string;
}
const b: Foo = func(a);

好了,我想你应该明白了,让我们来看一些更复杂的例子,在某些对象上使用一些 ES6 的特性 Rest/Spread 或者 Destructuring。

interface Hello{     
world: string;    
test: string;
}
const myFunction = ({ test,   ...rest }: Hello) => rest;

const res = myFunction({ world: 'world',test: 'test' });
//  res is of type { world : string }

在上面的代码中,TypeScript 推断出 myFunction 函数中 rest 对象的类型是Hello 接口不包含test 属性(通过析构函数被挑出来了)。所以 TypeScript 可以推断函数本身的返回类型,通过这种方式我们可以得到常量 res 的类型 { world : string; }  

ES6 Spread 也适用于 Typescript :

const first = {     
   one: 1,    
   two: 2
};
const second = {    
   three: 3,    
   four: 4
};
const res2 = {    
   ...first,    
   ...second
};

在这个例子中,res2 和下面的 Res2接口(interface)完全一致:

interface Res2 {     
   one: number;    
   two: number;    
   three: number;    
   four: number;
}

现在假设你有一个参数是 “ string | undefined “ 的结合类型,然后你想得到字符串或者默认的常量,让我们来写一个函数解决这个问题,想法来自Elm语言的 “ Maybe.withDefault “

const  maybeWithDefault = ( str?: string) => (defaultStr: string) => {     
   if(str) {        
       //infer str does not evaluate falsy and is of type string        
       return str;    
   }    
   return defaultStr;
};

在这个例子中,maybeWithDefault 函数返回值类型是 “ string “ 类型,而不是 " string | undefined " ,因为多谢了类型保护(type guards),我们的 “ if “语句不会返回undefined。  

我认为,现在是时候了解类型保护(type guards)了 ,它让类型推断达到了另一个层次,当你想从联合类型(union type)中推断类型时你可以使用它们。下面是一个例子:

interface Fish {     
   fin: number;
}
interface Cat {    
   legs: {        
       number: number;    
   }
}
type Animal = Fish | Cat
const myAnimal: Animal = { legs :{ number : 2 } };
const isFish = (animal: Animal): animal is Fish => (animal).fin !== undefined  
if(isFish( myAnimal )){    
   console.log(myAnimal.fin);
}else{    
   console.log(myAnimal.legs.number);
};

这个例子是TypeScript官方文档中的一个实例,足够说明类型保护(type guards)。这里我们创建了 “ isFish “函数,它会使用 if 语句来推断 myAnimal 的类型。myAnimal 在 if 语句中是 Fish 类型,在 else 语句中是Cat 类型。 

 在操作(operator)和 ES6 的类中通过 instanceof 运算符也可以使用类型保护( type guards)。

class A {     
   hello() {        
       console.log('hello');    
   }
}
class B {    
   bonjour() {        
       console.log('bonjour');    
   }
}
const myFunction2 = (sayHello: A | B) =>{    
   if( param instanceof A) {        
       sayHello.hello();    
   }else {        
       sayHello.bonjour();    
   }
}
myFunction2(new A());

上面的代码会打出 “ hello “ ,这里 TypeScript 可以推断出 “ sayHello “ 在 if 语句中是  A 类型,在else语句中是 B 类型。

3)管理不存在的值

正如 Tony Hoare 在2009年的一次会议上所说: “ 我把1965年空指针的发明称为价值10亿美元的错误。”

现在想象一下javascript中同时有 null 和 undefined 的混乱,而这就是 TypeScript 可以帮助我们的地方。

假设我们在 tsconfig.json 中使用 strictNullCheck 选项来进行严格的配置,现在复杂的是理解 TypeScript 是如何处理 null 或者 undefined 的值,并且我们如何使用一些抽象概念来帮助自己。  

当一个对象的属性值或者一个函数的参数值可以被忽略时,你可以使用TypeScript中可选的 “ ? “来表示。

interface User{     
   firstName: string;    
   secondName: string;
}

理解TypeScript “ ? “运算符不处理 null 是很重要的,我们只用 “ ? “ 来代表一个缺少的属性,对于TypeScript编译器而言,等同于这个属性值是undefined。知道这个,我建议对于不存在的属性值只使用undefined并且禁止在你的程序中使用 null ,可以使用这条 TSLint 规则: 

{ "no-null-keyword": true}  

不幸的是,你不能使用 “ ? “ 运算符去定义一个未定义的变量或者一个函数的返回值类型为undefined,你可能需要一个抽象来解决这个问题。 

type Optional= T | undefined  

这样,如果你的user没有在这定义,因为它要在第一次渲染组件的时候加载,你可以定义user为下面的类型:

let user: Optional  

这与下面定义的类型是一样的: 

let user:User | undefined;  

现在在组件中,我们想知道使用user的属性之前user是否已经存在了。正如我之前所说,typescript 可以使用类型保护( type guards) 来推断出参数的类型,所以我们可以实际使用类型保护来检测我们的定义的user。

const getFirstName = (user?: User) =>{     
   if(user) {        
       return user.firstName;    
   }    
   return 'no name';
}
let user: Optional;  console.log(getFirstName(user));
//'no name'

上面的代码将会打出 " no name " ,因为我们还没有定义user,但这样我们已经安全的处理了在程序中使用一个未定义的user的问题。

在这一点上,你可能认为应该使用 typescript “ ! “ 运算符去解决 undefined 值的问题,但是实际上我想指出在大部分情况下这都是一个不好的做法。

const getFirstName = (user?: User) => {     
   return user!.firstName;
}

TypeScript 感叹号会把你的 T 类型映射成 undefined类型。99%的时间里你不会想这样做,并且你应该处理未定义的用例。让我们来看一个很简单的例子,你可能会使用它: 

["test","hello"].find(el => el ==="hello")!; 

 数组 “ find “操作的类型描述如下(简化过): 

find(predicate: (obj: Array) => boolean): T | undefined; 

在这里,默认情况下,” find “会一直返回 T 类型或者undefined,但是在数组中我们有一系列常量并且我们知道 find 操作符会返回一个值,我认为这是有道理的去假设返回值不会是undefined,因此我们可以使用 TypeScript bang 来解决这个问题。  

你可能认为上面的这些会导致很多潜在的人为错误,你是对的。所以如果你正在寻找一个更强大的方式来解决不存在的值,我建议你去看看纯函数式编程语言是如何使用 Optional或者Maybe monads来处理它。在 TypeScript 中有一些monads 的实现,但如果只推荐一个的话,我建议看看 TsMonad。这些实现会提供比 if … else 语句更复杂的操作来处理不存在的值。  

尽管采用这样的工具代价不小,但是对于很多包括我在内的开发者而言,这都是不寻常的,当你第一次使用这些工具时会花费很多。

4)运行时检查

假设你正在遵循最好的TypeScript实践,并且你的系统在运行时是绝对安全的。现在还有另外一个失败的点是来自程序之外的数据,你需要确保所有数据都和你在编译时所做的假设相匹配。因此我建议添加一个运行时数据类型检查。  

首先你需要识别这些数据,例如这些数据可能来自 URL,你的本地存储,你的cookies 或者通过你定义的API。总之任何从你系统之外得来的数据都应该被视为不可信数据。  

例如假设你有一个前端体系架构的后端,并且遵循OpenAPI规范,我建议你使用具有Yaml模式定义的Swagger来解决这个问题,它可以结合Ajv等工具验证你发送给前端应用程序的数据。我不会详细讲述这一点,但是这有一段代码可以在你的express 层使用中间件实现: 

javascript

不幸的是我真的不太喜欢Ajv这样的工具,因为如果用 { removeAdditional : true }来去除多余的内容(props)时,它会改变你的原始数据。 

运行时验证在Elm中是强制的,你必须通过JSON.decode去确保数据是来自你自己的API,与此同时它会维护运行时验证检查来确保你不会引入任何错误到你的程序中。

5)类型抽象

好了,现在你有了良好的实践并且感谢TypeScript你可以编写在运行时安全的程序了。在这一点上,你可能想使用更多强大的抽象来帮助你。这时就可以使用TypeScript泛型(generics)了,其实我们早就在第三部分使用过泛型了,也就是Optional这个类型定义。我不想解释什么是泛型,我想花一些时间来看看我们在Trainline这个代码库中编写的一些内容,这或许会对你有帮助。 

假设你有一个规范化的前端并且你使用Dictionary (或者 Map ,Object等等)来存储数据,你可以为你的dictionary写一个类型抽象: 

type Dictionary= { [key: string]: T | undefined };  

这里,dictionary中的每个属性都是 T 或者 undefined,为了运行时安全我们永远不能确保实体(entity)可用。所以,你必须确认user存在并且user的属性也存在。  

假设我们有一个像Facebook 一样的app ,它有一个用户实体包含许多用户,我们可以执行下面的操作:

export interface User {     
   readonly id: string;    
   readonly firstName: string;    
   readonly secondName?: string;
}  
export type UsersDict = Dictionary;  

请注意,这里我们使用readonly 修饰符(attribute)来修饰user的每个属性(properties),这将有助于我们强制维持 user 对象的不变性。  

TypeScript也提供了一些有用的类型如 Partial: 

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

上面的代码允许对象T的所有属性是可选的,因此,你可以在你的dictionary中使用Partial而不是返回 “ T| undefined “,Partial有更准确的语义,并且在类型安全方面和” T| undefined “a表现的一致,然后最终你将有如下的Dictionary类型: 

type Dictionary= Partial<{ [key: string]: T }>;  

TypeScript 也暴露了一个全局Readonly类型可以像下面这样使用: 

type Readonly= { readonly [P in keyof T]: T[P]; };  

我们可以在我们的Dictionary类型中使用这个Readonly修饰符来避免一个个的定义” User “中的属性为只读的。然后我们可以得到如下的Dictionary 类型:

  type Dictionary= Partial<{ [key: string]: Readonly}>; 

上面代码的结果是我们有一个安全的Dictionary 有着T的只读属性。  

我建议你在 “ global.d.ts “文件中全局暴露这些有用的类型,这样你就不用在每次需要使用的时候重新导入。

总结

TypeScript 的优点不仅在于提供的静态类型,还包括和它相关的所有工具。 与VSCode 的整合也十分简单,你可以直接跳转到定义,获取合适的自动补全,甚至可以自动 import module。  

社区也在积极致力于让 TypeScript 生态变得更加美好,对我而言,这也是必须要有的” 自动格式化并保存 “选项,不然就需要自己调整代码样式。  

TypeScript 一直在改进中,目前的版本是测试版2.6,并且有一个新的严格选项strictFunctionTypes,会对你的方法参数有一个更严格的类型检查。  

如果你现在仍然没有看到 TypeScript 严格的静态类型和强类型推断的好处,或者你想要更近一步了解,我建议你可以先尝试一下 Elm。Elm 的社区声称运行时的安全性可以达到99%,我从中收获了很多。


原文链接:https://codeburst.io/five-tips-i-wish-i-knew-when-i-started-with-typescript-c9e8609029db

翻译:cjlalala

(PC上建议打开“阅读原文”链接,会有更好的阅读体验)

以上是关于当我开始使用TypeScript时我希望知道的5个技巧的主要内容,如果未能解决你的问题,请参考以下文章

登录服务器,首先用到的5个命令

为啥当我输入一个大数字时我的计算器程序开始闪烁和滚动

typescript 访问修饰符和 javascript 访问修饰符有啥区别?在使用打字稿时我应该更喜欢哪一个?

在 Angular 4/5 和 TypeScript 中重新加载同一页面

TypeScript 中的匿名/内联接口实现

你应该知道的 5 个 Docker 工具