查看原文
其他

为初学者解答 Stack Overflow 上最热门的 TypeScript 问题

luojiyin freeCodeCamp 2022-09-21


“我讨厌 Stack Overflow 网站”——从未有开发者这样说过。

虽然在谷歌上搜索答案很有帮助,但更需要的是真正理解你遇到的偶发问题的解决方案。

在这篇文章中,我将探讨七个 Stack Overflow 上最常见的 TypeScript 问题。

我花了几个小时来研究这些问题。

我希望你对在 TypeScript 中可能面临的常见问题有更深入的了解。

如果你刚刚学习 TypeScript,这也是有意义的——有什么比熟悉你未来遇到的问题更好的方法呢?

让我们马上进入正题。

目录

  1. TypeScript 中的接口(interfaces)与类型(Types)之间有什么区别?
  2. 在 TypeScript 中,什么是 ! 操作符?
  3. TypeScript 中的 “.d.ts” 文件是什么?
  4. 如何在 TypeScript 中明确设置 window 的新属性?
  5. 强类型函数作为参数在 TypeScript 中是否可行?
  6. 如何修复无法找到模块的声明文件?
  7. 如何在 TypeScript 中为对象动态分配属性?

注意: 你可以得到这份手册的 PDF 或 ePub 版本,以方便参考或在你的 Kindle 或平板电脑上阅读。


这是 PDF 或 Epub 版本的手册下载地址

TypeScript 中的接口(interfaces)与类型(Types)之间有什么区别?

   Typescript 中的 Interfaces(接口) 与 Types(类型) 的对比

interfaces vs types(技术上来说,是 type alias)是一个充满争议的问题。

在开始使用 TypeScript 时,你可能会发现有选择上很困惑。这篇文章消除这种困惑,并帮助你选择适合你的方式。

TL;DR

在许多情况下,你可以交替使用 interface 或者 type alias。

interface 的大部分功能可以通过 type aliases 来实现, 只是你不能通过重新声明类型(re-declaring)来给它增加新的属性(properties)。你必须使用一个交叉类型(intersection type)。

为什么开始时会对类型(Types)与接口(Interfaces)产生混淆?

每当我们面临多种选择时,大多数人都会开始面对选择悖论(paradox of choice)。

在这种情况下,只有两个选项。

这有什么好困惑的?

好吧,这里的主要混淆源于这两个选项在大多数方面是如此势均力敌

这使得你很难做出一个明确的选择,尤其是当你刚刚开始使用 Typescript 的时候。

类型别名(Type Alias)与接口(Interface)的一个基本例子

让我们通过 interface 和 type alias 的例子快速了解一下。

考虑下面的 Human type 的写法:

// type 
type Human = {
  name: string 
  legs: number 
  head: number
}

// interface 
interface Human {
  name: string 
  legs: number 
  head: number
}

这些都是表示 Human type,通过 type alias 或者 interface。

类型别名(Type Alias)和接口(Interfaces)之间的区别

以下是 type alias 和 interface 的主要区别:

关键区别:接口(Interfaces)只能描述 object shapes。类型别名(Type aliases)可用于其他类型(other types),如 primitives、unions 和 tuples。

type alias 可以表示的数据类型中是相当灵活的。从 basic primitives(基本的基元)到 复杂的 unions(联合)和 tuples(元组),如下所示:

// primitives 
type Name = string 

// object 
type Male = {
  name: string
}

type Female = {
  name: string 
}

// union
type HumanSex = Male | Female

// tuple
type Children = [Female, Male, Female]

不像 type aliases,你只能用一个 interface 来表示 object types(对象类型)。

关键区别:一个接口可以通过多次声明来进行扩展

请思考以下例子:

interface Human {
  name: string 
}

interface Human {
  legs: number 
}

上面的两个声明将变成:

interface Human {
  name: string 
  legs: number 
}

Human将被视为一个单一的 interface:两个声明的成员的组合。

type Humans 中需要 legs 这个属性(Property)

   

看这个 TypeScript 演示代码

这种情况不适合使用 type aliases。

使用 type aliases,以下将导致错误:

type Human = {
    name: string 
}
  
type Human =  {
    legs: number 
}

const h: Human = {
   name: 'gg',
   legs: 5 
}  

重复标识符(Duplicate identifier)Human 错误


看这个 TypeScript 演示代码

使用 type aliases 的话,你就不得不使用一个交叉类型(intersection type):

type HumanWithName = {
    name: string 
}
  
type HumanWithLegs =  {
    legs: number 
}

type Human  = HumanWithName & HumanWithLegs

const h: Human = {
   name: 'gg',
   legs: 5 
}  

看这个 TypeScript 演示代码。

小小的区别:类型别名和接口都可以被扩展,但语法不同

对于 interfaces,你使用extends关键字。对于 types,你必须使用一个交叉(intersection)。

思考一下下面的例子:

类型别名扩展了一个类型别名


type HumanWithName = {
  name: string 
}

type Human = HumanWithName & {
   legs: number 
   eyes: number 
}

类型别名扩展了一个接口

interface HumanWithName {
  name: string 
}

type Human = HumanWithName & {
   legs: number 
   eyes: number 

接口扩展了一个接口

interface HumanWithName {
  name: string 
}

interface Human extends HumanWithName {
  legs: number 
  eyes: number 
}

接口扩展了一个类型别名

type HumanWithName = {
  name: string
}

interface Human extends HumanWithName {
  legs: number 
  eyes: number 
}

正如你所看到的,这并不是选择一个而不是另一个的特别理由。然而,语法是不同的。

小区别:类只能实现静态已知的成员

class 可以同时实现 interfaces 或者 type aliases。然而,不能实现(implement)或扩展(extend)一个联合类型(union type)。

请看下面的例子:

类实现了一个接口

interface Human {
  name: string
  legs: number 
  eyes: number 
}

class FourLeggedHuman implements Human {
  name = 'Krizuga'
  legs = 4
  eyes = 2
}

类实现了一个类型别名

type Human = {
  name: string
  legs: number 
  eyes: number 
}

class FourLeggedHuman implements Human {
  name = 'Krizuga'
  legs = 4
  eyes = 2
}

这两段代码作都没有任何错误。然而,下面的情况却失败了:

类实现了一个联合类型

type Human = {
    name: string
} | {
    legs: number
    eyes: number
}

class FourLeggedHuman implements Human {
    name = 'Krizuga'
    legs = 4
    eyes = 2
}

class 只能实现(implement)一个对象类型(object type)或具有静态已知成员的对象类型(intersection of object types with statically known members)的交叉(intersection)

查看 TypeScript playground

类型别名与接口的总结

你的想法可能不同,但只要有可能,我都坚持使用 type aliases,因为它们的灵活性和语法更简单。也就是说,除非我特别需要一个接口(interface)的功能,否则我选择 type aliases。

在大多数情况下,你也可以根据你的个人偏好来决定,但要与你的选择保持一致,至少在一个特定的项目中。

我必须补充一点,在需要考虑性能的情况下,interface 比较检查可能比 type aliases 更快。但我还没有发现使用 type aliases,导致性能问题。

在 TypeScript 中,什么是 ! 操作符?

TypeScript 中的 bang 运算符是什么

TL;DR

这个 在技术上被称为 非空的断言操作符(non-null assertion operator)。如果 TypeScript 编译器警告一个值是 nullundefined,你可以使用操作符来断言该值不是 nullundefined

个人观点:尽可能避免这样做。

什么是非空断言操作符(Non-Null Assertion Operator)?

nullundefined 是有效的 JavaScript 值。

上面的声明对所有 TypeScript 应用程序也是如此。

然而,TypeScript 更进一步。

nullundefined 是同样有效的类型。例如,考虑下面的情况:

// 明确为 null
let a: null 

a = null
// 以下代码将产生错误
a= undefined 
a = {}


// 明确为 undefined
let b: undefined 
// 以下代码将产生错误
b = null 
b = {}

Error:除了 null 和 undefined 之外,其他的值不能被分配


查看 TypeScript playground

在某些情况下,TypeScript 编译器无法判断某个值是否被定义,即不是 nullundefined

例如,假设你有一个值Foo

Foo!产生一个类型为Foo的值, 不能为 nullundefined

   Foo! 从 Foo 的类型中排除了 null 和 undefined

你基本上是在对 TypeScript 编译器说:_我确信 Foo不会是 nullundefined_。

让我们来探讨一个简单的例子。

在标准的 JavaScript 中,你可以用 .concat 方法将两个字符串连接起来:

const str1 = "Hello" 
const str2 = "World"

const greeting = str1.concat(' ', str2)
// Hello World

编写一个简单的产生重复字符串函数,以自己为参数调用 .concat

function duplicate(text: string | null) {
  return text.concat(text);
}

注意,参数 text 被打成了 string | null

在严格模式下,TypeScript 会在这里警告,因为用 null 调用 concat 会导致意外的结果。

用 null 调用 concat 的结果



TypeScript 将报错:Object is possibly 'null'.(2531)

Typescript error: Object is possibly null(对象可能为空)   


反过来说,一个相当懒的方法是,使用非空的断言操作符来消除编译器的错误:

function duplicate(text: string | null) {
  return text!.concat(text!);
}

注意text变量后面的感叹号——text!

text类型代表string | null

text! 只代表string,也就是把nullundefined 从变量类型中删除。

结果是什么?你已经消除了 TypeScript 的错误。

然而,这是一个愚蠢的修复。

duplicate确实可以在 null 的情况下被调用,这可能会导致意外的结果。

请注意,如果text是一个可选的属性,下面的例子也是成立的:

// text could be "undefined"
function duplicate(text?: string) {
  return text!.concat(text!);
}

运算符的陷阱(以及如何不使用它)

当作为一个新用户使用 TypeScript 时,你可能觉得自己在打一场会失败的仗。

这些错误对你来说毫无意义。

你的目标是消除错误,尽可能迅速地继续你的生活。

然而,你应该小心使用非空断言操作符(!)。

消除一个 TypeScript 错误并不意味着消除一个潜在的问题。

正如你在前面的示例中看到的那样,你会失去所有相关的 TypeScript 安全性,以防止 nullundefined 的错误用法。

那么,你应该怎么做?

如果你写 React,考虑一个你可能熟悉的例子:

const MyComponent = () => {
   const ref = React.createRef<HTMLInputElement>();
 
   //compilation error: ref.current is possibly null
   const goToInput = () => ref.current.scrollIntoView(); 

    return (
       <div>
           <input ref={ref}/>
           <button onClick={goToInput}>Go to Input</button>
       </
div>
   );
};

在上面的示例中(对于那些不编写 React 的人),在 React 心智模型(mental mode)中,ref.current 在用户单击按钮时肯定会可用。

ref 对象在 UI 元素被渲染后不久就会被设置。

TypeScript 不知道这一点,所以你可能被迫在这里使用非空断言操作符。

实际上,开发者对 TypeScript 编译器说,我知道我在做什么,你(TypeScript 编译器)不知道。

const goToInput = () => ref.current!.scrollIntoView();

注意感叹号

这 "修复" 了这个错误。

但是,如果将来有人从输入中删除了 ref,而又没有自动测试来捕捉这一点,你现在就有一个错误。

// before
<input ref={ref}/>

// after
<input />

TypeScript 将无法发现以下一行的错误:

const goToInput = () => ref.current!.scrollIntoView();

通过使用非空(non-null)断言操作符,TypeScript 编译器将认为 nullundefined对于相关的值来说是不可能的。在这种情况下,ref.current

解决方案 1:寻找替代修复方法

你应该对第一行找到一个替代的修复方法。

例如,通常你可以像这样明确地检查 nullundefined 值。

// before 
const goToInput = () => ref.current!.scrollIntoView();

// now 
const goToInput = () => {
  if (ref.current) {
   //Typescript会认为 ref.current肯定是 
   //可以在这个条件下中使用
     ref.current.scrollIntoView()
  }
};

//  或者(使用逻辑与运算符)
const goToInput = () => ref.current && ref.current.scrollIntoView();

众多工程师会争论这个事实,即使代码更啰嗦。

这是对的。

但是你应该选择详细而不是可能破坏代码,并将代码推送到生产环境。

这是个人喜好。你选择的道路可能会有所不同。

解决方案 2:明确地抛出一个错误

在其他修复方法不能解决问题的情况下,非空断言运算符似乎是唯一的解决方案,我通常建议你在这样做之前抛出一个错误。

下面是一个例子(用伪代码):

function doSomething (value) {
   // 出于某种原因,TS认为该值可能是  
   // null 或者 undefined,但你不同意
   
  if(!value) {
    // 明确地断言这就是情况 
    // 抛出一个错误或在某个地方记录这个错误,你可以追踪
    throw new Error('uexpected error: value not present')
  } 

  // 继续使用非空的断言运算符
  console.log(value)
}

我发现自己有时会这样做的一个实际案例是在使用Formik时。

除了事情发生了变化,我确实认为Formik在许多情况下是不好的类型。

如果你已经完成了 Formik 的验证,并确定你的值存在,那么这个例子可能会有类似的效果。

这里有一些伪代码:

<Formik 
  validationSchema={...} 
  onSubmit={(values) => {
   // 你确定 values.name 应该存在,因为你有验证了
   // 但TypeScript不知道这一点

   if(!values.name) {
    throw new Error('Invalid form, name is required')  
   } 
   console.log(values.name!)
}}>


</Formik>

在上面的伪代码中,values 可以这样定义:

type Values = {
  name?: string
}

但是在你点击onSubmit之前,你已经添加了一些验证,显示一个 UI 表单错误,让用户在继续提交表单之前输入一个name

还有其他方法可以解决这个问题。但如果你确定一个值存在,但又不能完全传达给 TypeScript 编译器,可以使用非空断言操作符。

但也通过可以添加你自己的断言来抛出一个你可以追踪的错误。

隐性断言(Implicit Assertion)是什么

尽管运算符的名字是非空断言运算符,但实际上没有断言(assertion)。

你主要是在断言(作为开发者)这个值存在。

TypeScript 编译器并没有断言这个值存在。

所以,如果你必须这样做,你可以继续添加你的断言(例如,在前面的章节中讨论的)。

另外,请注意,使用非空断言运算符不需要更多的 JavaScript 代码来发出(emitted)事件。

如前所述,TypeScript 在这里没有做断言。

因此,TypeScript 不会通过一些代码来检查这个值是否存在。

如果这个值存在,JavaScript 代码会发出(emitted)一个事件。

总结

TypeScript 2.0 发布了 non-null assertion operator (非空断言操作符)。是的,它已经存在了一段时间(发布于 2016 年)。在撰写本文时,TypeScript 的最新版本是 v4.7

如果 TypeScript 编译器警告一个值是 nullundefined,你可以使用 操作符来断言上述值不是 nullundefined

只有在你确定是这样的情况下才这样做。

甚至更好的是,继续添加你自己的断言,或尝试找到一个替代的解决方案。

有些人可能会说,如果你每次都需要使用 non-null assertion operator(非空断言操作符),那就说明你通过 TypeScript 控制你的应用程序状态的能力很差。

我同意这个观点。

TypeScript中的 “.d.ts” 文件是什么?

d.ts 文件是什么  


TL;DR

.d.ts文件被称为类型声明文件。它们的存在只有一个目的:描述一个现有模块(module)的类型特征(shape),它们只包含用于类型检查的类型信息。

TypeScript 中的.d.ts文件介绍

学习了 TypeScript 的基础知识后,你就可以获得超能力。

至少我是这么认为的。

你会自动得到潜在错误的警告,并在你的代码编辑器中得到自动完成的功能。

虽然看起来很神奇,但计算机没有使用魔法。

那么,TypeScript 的诀窍是什么呢?

用更清晰的语言,TypeScript 怎么知道这么多?它如何判断哪个 API 正确与否?在某个对象或类上哪些方法可用,哪些不可用?

答案不是魔法。

TypeScript 靠的是类型(type)。

有时,你不编写这些类型(types),但它们存在。

它们存在于称为声明文件的文件中。

这些是以 .d.ts 结尾的文件。

一个关于".d.ts "文件的简单例子

想一下下面的 TypeScript 代码:

// valid 
const amount = Math.ceil(14.99)

// error: Property 'ceil' does not exist on type 'Math'.(2339)
const otherAmount = Math.ciil(14.99)

参考 TypeScript playground(训练场)。

第一行代码是完全有效的,但第二行则不尽然。

TypeScript 很快就发现了这个错误:Property 'ciil' does not exist on type 'Math'.(2339)

Typescript 发现访问属性 ciil 的错误   


TypeScript 是如何知道 Math 对象上不存在 ciil 的?

Math 对象不是我们实现的一部分。这是一个标准的内置对象。

那么,TypeScript 是如何解决这个问题的呢?

答案是有 declaration files 来描述这些内置对象。

将声明文件视为包含与某个模块相关的所有类型信息。它不包含具体实现,仅包含类型信息。

这些文件有一个 .d.ts 结尾。

你的实现文件将有.ts.js结尾,代表 TypeScript 或 JavaScript 文件。

这些声明文件没有实现。他们只包含类型信息,并且有一个.d.ts文件结尾。

内置类型定义

在实践中理解这一点的一个好方法是建立一个全新的 TypeScript 项目,并探索顶级对象的类型定义文件,如 Math

让我们这样做。

创建一个新的目录,并将其命名为任何合适的名字。

我创建一个新文件夹 dts

cd dts

现在初始化一个新的项目:

npm init --yes

初始化 TypeScript:

npm install TypeScript --save-dev

安装 TypeScript

  

这个目录应该包含 2 个文件和一个子目录:

安装后的文件  


在你喜欢的代码编辑器中打开该文件夹。

如果你去查看 node_modules中的TypeScript目录,你会发现一堆开箱即用的类型声明文件。


TypeScript 目录中的类型声明文件,是在安装 TypeScript 后出现的。

默认情况下,TypeScript 将包括所有 DOM API 的类型定义,例如认为windowdocument

当你检查这些类型声明文件时,你会注意到命名惯例是很简单的。

它是这样的:lib.[something].d.ts

打开lib.dom.d.ts声明文件,查看所有与浏览器 DOM API 相关的声明。


DOM 声明文件,正如你所看到的,这是个相当巨大的文件。

DOM 中的所有 API 也是如此。

真棒啊!

现在,如果你看一下lib.es5.d.ts文件,你会看到Math对象的声明,包含ceil属性。

声明文件中的 Math 对象

下次你想,哇,TypeScript 真了不起。请记住,这种美妙的很大一部分是归功于鲜为人知的英雄:类型声明文件。

这不是魔术,是类型声明文件。

TypeScript 的外部类型定义

那些没有内置的 API 怎么办?

有许多npm包可以做任何你想做的事情。

有没有办法让 TypeScript 也能理解上述模块的相关类型关系?

嗯,答案是肯定的。

通常有两种方式,一个库的作者可以做到这一点。

类型打包

在这种情况下,库的作者已经将类型声明文件作为包的一部分打包(Bundled)在一起。

你通常不需要做任何事情。

你只需继续在你的项目中安装库,从库中导入所需的模块,看看 TypeScript 是否会自动为你解析类型

记住,这不是魔术。

库的作者已经将类型声明文件包含在包的分发中。

DefinitelyTyped(@types)

想象一下,一个公共资源库(central public repository)为成千上万的库托管声明文件?

这个资源库已经存在了。

DefinitelyTyped repository是一个集中式的仓库,存储了成千上万个库的声明文件。

说实话,绝大多数常用的库都在 DefinitelyTyped 上有声明文件。

这些类型定义文件会自动发布到npm下的@types范围。

例如,如果你想安装reactnpm 包的类型文件,你可以这样做:

npm install --save-dev @types/react

如果你发现自己使用的模块的类型不是 TypeScript 自动解析的,可以尝试直接从 DefinitelyTyped 安装类型。

看看那里是否存在这些类型。比如说:

npm install --save-dev @types/your-library

你以这种方式添加的定义文件将被保存到 node_modules/@types 目录下。

TypeScript 会自动找到这些。所以,你不需要采取额外的步骤。

如何编写你自己的声明文件

在不常见的情况下,如果一个库没有捆绑它的类型,并且在 DefinitelyTyped 上没有类型定义文件,你可以编写你自己的声明文件。

深入了解编写声明文件超出了本文的范围,但你可能会遇到的一个情况是在没有声明文件(declaration file)的情况下,如何消除关于某个特定模块(particular module)的错误。

声明文件(Declaration files)都有一个.d.ts结尾。

所以要创建你的声明文件,就要创建一个以.d.ts结尾的文件。

例如,假设我在项目中安装了库untyped-module

untyped-module没有引用的类型定义文件,所以 TypeScript 在我的项目中对此进行警告。

为了消除这个警告,我可以在我的项目中创建一个新的untyped-module.d.ts文件,内容如下:

declare module "some-untyped-module";

这将声明该模块为any类型。

我们不会得到任何 TypeScript 对该模块的支持,但你已经消除了 TypeScript 的警告。

理想的下一步包括在模块的公共资源库中打开一个问题,包括一个 TypeScript 声明文件,或者自己写一个合适的文件。

总结

下次你想,哇,TypeScript 真了不起。请记住,这种了不起的成就有很大一部分是由于幕后的英雄:类型声明文件(type declaration files)。

现在你明白它们是如何工作的了吧!

如何在 TypeScript 中明确设置 window 的新属性?

在 window 对象上设置一个新属性(new property)?



TL;DR

Window对象扩展(extend)现有的接口声明。

TypeScript 中的 window 简介

知识建立在知识之上。

这是对的。

在本节中,我们将建立在前两节的知识基础上:

  • TypeScript 中的接口与类型对比
  • 什么是 TypeScript 中的 d.t.s 文件??

准备好了吗?

首先,我必须说,在我使用 TypeScript 的早期,这是一个我在谷歌上一遍又一遍搜索的问题。

我从来没有理解它。我也懒得理会,我只是在网上搜索。

这绝不是掌握一门学问的正确心态。

让我们来讨论一下这个问题的解决方案。

理解问题

这里的问题实际上很容易推理。

思考以下 TypeScript 代码:

window.__MY_APPLICATION_NAME__ = "freecodecamp"

console.log(window.__MY_APPLICATION_NAME__)

TypeScript 会很快让你知道__MY_APPLICATION_NAME__不存在于 Window & typeof globalThis 类型。

该属性不存在于 Window 上的报错   


查看 TypeScript playground

好吧,TypeScript。

我们明白了。

仔细观察,记得上一节关于声明文件(declaration files),所有现有的浏览器 API 都有一个声明文件。这包括内置对象,如window

默认的 Window 接口声明

 

如果你看看lib.dom.d.ts声明文件,你会发现Window接口的描述。

用通俗的话说,这里的错误是:Window接口描述了我对window对象及其用法的理解。该接口没有指定某个__MY_APPLICATION_NAME__属性。

如何修复该错误

在类型(types)与接口(interface)部分,我解释了如何扩展一个接口。

让我们在这里应用这些知识。

我们可以扩展(extend)Window 接口声明 (interface declaration),使其知道__MY_APPLICATION_NAME__属性。

下面是方法:

// before
window.__MY_APPLICATION_NAME__ = "freecodecamp"

console.log(window.__MY_APPLICATION_NAME__)

// now 
interface Window {
  __MY_APPLICATION_NAME__: string
}

window.__MY_APPLICATION_NAME__ = "freecodecamp"

console.log(window.__MY_APPLICATION_NAME__)

没有错误了!

解决问题的方案   


查看 TypeScript playground

记住,类型(types)和接口(interfaces)的一个关键区别是,接口可以通过多次声明来扩展(extended)。

我们在这里所做的是再一次声明了 Window接口,因此扩展了(extending)接口声明。

一个现实世界的解决方案

我在 TypeScript playground 解决了这个问题,向你展示了最简单的解决方案,这就是关键所在。

不过,在现实世界中,你不会在你的代码中扩展接口。

那么,你应该怎么做呢?

也许,给它一个猜测?

是的,你很接近……,也可能是正确的。

创建一个类型定义文件!

例如,创建一个window.d.ts文件,内容如下:

interface Window {
  __MY_APPLICATION_NAME__: string
}

然后就可以了。

你已经成功地扩展了Window接口并解决了问题。

如果你继续给 __MY_APPLICATION_NAME__ 属性分配了错误的值类型,你现在已经启用了强类型检查。

对新定义的属性进行错误的赋值,将会有警告。

查看 TypeScript playground

总结

在旧的 Stack Overflow 帖子,你会发现基于旧的 TypeScript 版本会方案更复杂。

在新版 TypeScript 中,该解决方案更容易。

现在你知道了。😉

强类型函数作为参数在 TypeScript 中是否可行?

TL;DR

这个问题不需要过多的解释。简短的回答是肯定的。

函数可以被强类型化--甚至作为其他函数的参数。

简介

我必须说,与本文的其他部分不同,在我早期的 TypeScript 时代,我从未真正发现自己在寻找这个。

然而,这并不是最重要的。

这是一个经过精心研究的问题,所以让我们来回答它吧!

如何在 TypeScript 中使用强类型的函数参数

这个 Stack Overflow 帖子上的公认答案在一定程度上是正确的。

假设你有一个函数: speak

function speak(callback) {
  const sentence = "Hello world"
  alert(callback(sentence))
}

它接收一个 callback,在内部用一个 string 来调用。

为了打这个字,继续用一个函数类型的别名来表示 callback

type Callback = (value: string) => void

并敲下 speak 函数,如下所示:

function speak(callback: Callback) {
  const sentence = "Hello world"
  alert(callback(sentence))
}

另外,你也可以将该保持类型内联(type inline):

function speak(callback: (value: string) => void) {
  const sentence = "Hello world"

  alert(callback(sentence))
}

查看 TypeScript playground

这就是了!

你使用了一个强类型的函数作为参数。

如何处理没有返回值的函数

例如,参考的堆栈溢出帖子中接受的答案说  回调参数的类型必须是 "function that accepts a number and returns type any. (接收数字并返回任何类型的函数)"

这部分是正确的,但是返回类型不一定是any

事实上,不要使用 any

如果你的函数返回一个值,请继续并适当地输入它:

// 回调返回一个对象
type Callback = (value: string) => { result: string }

If your callback returns nothing, use void not any:

// 回调不返回一个对象
type Callback = (value: string) => void

注意,你的函数类型的签名(signature of your function type)应该是:

(arg1: Arg1type, arg2: Arg2type) => ReturnType

其中Arg1type是参数arg1的类型,Arg2type是参数arg2的类型,而ReturnType是你的函数的返回类型。

总结

JavaScript 中传递数据的主要手段是函数。

TypeScript 不仅允许你指定函数的输入和输出,而且你还可以将函数作为其他函数的参数。

去吧,放心地使用它们。

如何修复无法找到模块的声明文件......?

对于 TypeScript 初学者来说,这是一个常见的挫折来源。

然而,你知道如何解决这个问题吗?

是的,你知道!

我们在 what is d.ts 一节中看到了这个问题的解决方案。

TL;DR

创建一个声明文件,例如 untyped-module.d.ts,其内容如下:declare module "ste-untyped-module"; 注意,这将明确地将模块类型为any

解释解决方案

如果你不记得如何解决这个问题,你可以重新阅读写声明文件一节。

根本原因,你有这个错误是因为相关的库没有它的类型,并且在 DefinitelyTyped 上没有一个类型定义文件。

这就给你留下了一个解决方案:编写你自己的声明文件。

例如,如果你在项目中安装了库untyped-moduleuntyped-module没有引用的类型定义文件,所以 TypeScript 会警告。

为了消除这个警告,在你的项目中创建一个新的untyped-module.d.ts文件,内容如下:

declare module "some-untyped-module";

这将声明该模块为 any 类型。

你不会得到任何 TypeScript 对该模块的支持,但你已经消除了 TypeScript 的警告。

下一步包括在模块的公共仓库中开一个 issue,以包括一个 TypeScript 声明文件,或者自己写一个像样的文件(超出本文的范围)。

如何在TypeScript中为对象动态分配属性?

在 Typescript 中动态地给对象分配属性

 

TL;DR

如果你不能在声明时定义变量类型,请使用 Record utility 类型或对象索引签名。

简介

请思考以下例子:

const organization = {}

organization.name = "Freecodecamp"
                                                                                                            

这段看似无害的代码在动态分配nameorganization对象时抛出一个 TypeScript 错误。

动态添加新属性时出现的 Typescript 错误



查看 Typescript playground

如果你是 TypeScript 的初学者,会感到困惑,也许是有道理的,那就是看似简单的东西在 TypeScript 中怎么会成为问题?

理解问题

一般来说,TypeScript 在声明变量时确定其类型,并且这个确定的类型不会改变,也就是在你的应用程序中应该保持不变。

在考虑类型缩小或使用 any 类型时,此规则有例外,但这是要记住的一般规则。

在前面的示例中,organization 对象声明如下:

const organization = {}

没有为 organization 变量指定明确的类型,所以 TypeScript 根据声明推断 organization 的类型为 {},即字面的空对象(iteral empty object)。

例如,如果你添加一个类型别名(type alias),你可以在 organization 的类型上进行尝试:

type Org = typeof organization

探索字面对象类型



查看 TypeScript playground

当你试图在这个空对象的字面意义上引用name prop 时:

organization.name = ...

TypeScript 大喊。

{}类型上不存在 name 属性。

当你理解了这个问题后,这个错误确实该发生。

让我们来解决这个问题。

如何解决该错误

有许多方法可以解决这里的 TypeScript 错误。让我们考虑一下这些:

1. 在声明时明确地输入对象

这是最容易推理的解决方案。

在你声明对象的时候,去输入它。此外,给它分配所有相关的值。

type Org = {
    name: string
}

const organization: Org = {
    name: "Freecodecamp"
}

查看 TypeScript playground

这就消除了所有的警告。

有时,对象的属性确实需要在声明时之后添加。

然而,如果必须动态地添加对象的属性,这并不总是可行的。

2. 使用一个对象索引签名

偶尔,对象的属性确实需要在比声明时更晚的时间被添加。

在这种情况下,你可以利用对象索引签名,如下所示:

type Org = {[key: string] : string}

const organization: Org = {}

organization.name = "Freecodecamp"

查看 TypeScript playground

在声明 organization 变量时,你继续并将其显式键入到以下 {[key: string] : string}

为了进一步解释语法,你可能习惯于具有固定属性类型的对象类型:

type obj = {
  name: string
}

但你也可以用 name 代替 variable type(变量类型)

例如,如果你想在 obj 上定义任何字符串属性:

type obj = {
 [key: string]: string
}

请注意,语法类似于你在标准 JavaScript 中使用变量对象属性的方式:

const variable = "name" 

const obj = {
   [variable]: "Freecodecamp"
}

TypeScript 等效项(equivalent)称为对象索引签名。

另外,请注意,你可以使用其他原语(other primitives)键入 key

// number 
type Org = {[key: number] : string}

// string 
type Org = {[key: string] : string}

//boolean
type Org = {[key: boolean] : string}

3. 使用 Record utility type

这里的解决方案非常简洁:

type Org = Record<stringstring>

const organization: Org = {}


organization.name = "Freecodecamp"

除了使用类型别名(type alias),你还可以内联类型(inline the type)。

const organization: Record<stringstring> = {}

使用 Record utility type



查看 TypeScript playground

Record utility type 有以下签名:Record<Keys, Type>

它允许你约束一个对象类型,其属性是Keys,属性值是Type

在我们的例子中,Keys 代表 stringType 也代表string

总结

除了原语(primitives)之外,你必须处理的最常见的类型可能是对象类型。

如果你需要动态构建对象,请利用 Record utility type 或使用对象索引签名来定义对象上允许的属性。

请注意,你可以获得 PDF 或 ePub,该手册的版本以便于参考,或在你的 Kindle 或平板电脑上阅读。

谢谢你阅读本文!

想要一本免费的 TypeScript 书籍吗?

《构建强类型多态的 React 组件》书籍

 

点击这里免费获取这本书



原文链接:https://www.freecodecamp.org/news/the-top-stack-overflowed-typescript-questions-explained/

作者:Emmanuel Ohans

译者:luojiyin


在线贡献者交流会预告
在线贡献者交流会将于北京时间 2022 年 9 月 11 日周日上午 10:00 - 11:00 开展(每两周一次,都在这个时间段开展)。
欢迎大家添加小助手微信 fcczhongguo,加入会议室。
开源公益社区 freeCodeCamp.org 自 2014 年成立以来,以“帮助人们免费学习编程”为使命,创建了大量免费的编程教程,包括交互式课程、视频课程、文章等。我们正在帮助全球数百万人学习编程,希望让世界上每个人都有机会获得免费的优质的编程教育资源,成为开发者或者运用编程去解决问题。
✨ ✨ ✨年度总结丨2021 年世界各地的开发者在 freeCodeCamp 学习 21 亿分钟(4000 年)freeCodeCamp 教育公益下一个目标:免费提供大学计算机科学学士学位参与 freeCodeCamp 开源社区翻译协作,帮助全世界人们用母语免费学习编程

点击“阅读原文”
在 freeCodeCamp 专栏了解更多

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存