为啥 TypeScript 接受值作为数据类型?

Posted

技术标签:

【中文标题】为啥 TypeScript 接受值作为数据类型?【英文标题】:Why does TypeScript accept value as a data type?为什么 TypeScript 接受值作为数据类型? 【发布时间】:2017-11-17 13:31:22 【问题描述】:

为什么 TypeScript 接受值作为数据类型?

以下这些场景是可接受和不可接受的声明。

export class MyComponent
        error: 'test' = 'test'; // accept
        error: 'test' = 'test1'; // not accept
        error: Boolean = true || false; // accept
        error: true | false = true; // not accept
        error: true = true; // accept
        error: true = false; // not accept
        error: Boolean; //accept
        error: true; // accept
        error: 1 = 1;   //accept
        error: 1 = 2; // not accept
    

为什么 TypeScript 允许将值作为数据类型? javascript 在编译时如何处理这些? 它与readonlyconstant 有何不同?

readonly error= 'test';error: 'test' = 'test';

【问题讨论】:

【参考方案1】:

首先,一些非正式的背景和信息将帮助我们讨论您提出的问题:

一般来说,一个类型表示一组 0 个或多个值。这些值可以被认为是该类型的成员或居民。

就它们可以采用的多种值而言,类型往往属于 3 组中的 1 组。

第 1 组: 举个例子:string 类型。 string 类型包含所有字符串值。由于字符串实际上可以任意长,因此基本上有无限数量的值是string 类型的成员。作为该类型成员的值的集合是所有可能的字符串的集合。

第 2 组: 举个例子:undefined 类型。 undefined 类型只有一个值,undefined 值。因此,这种类型通常被称为单例类型,因为它的成员集只有 1 个值。

第 3 组: 举个例子:never 类型。 never 类型没有成员。根据定义,不可能有never 类型的值。当您在专业人士中阅读它时,这似乎有点令人困惑,但一个小的代码示例可以解释它。

考虑:

function getValue(): never 
  throw Error();

在上面的示例中,函数getValue 的返回类型为never,因为它从不返回值,它总是抛出。因此,如果我们写

const value = getValue();

value 的类型也是 never

现在开始第一个问题:

为什么打字稿允许值作为数据类型?

原因有很多很多,但有一些特别令人信服的原因

根据传递给它们的值对行为不同的函数的行为进行建模。想到的一个例子是函数document.getElementsByTagName。此函数始终采用string 类型的值,并始终返回包含htmlElements 的NodeList。但是,根据传递的实际字符串值,它将在该列表中返回完全不同类型的事物。这些元素之间唯一的共同点是它们都派生自HTMLElement

现在,让我们考虑如何写下这个函数的类型签名。我们的第一次尝试可能是这样的

declare function getElementsByTagName(tagname: string): NodeList<HTMLElement>;

这是一个正确的,但它不是特别有用。假设我们想要获取页面上所有 HTMLInput 元素的值,以便将它们发送到我们的服务器。

我们知道,getElementsByTagName('input') 实际上只返回页面上的输入元素,这正是我们想要的,但是根据我们上面的定义,我们当然会得到正确的值(TypeScript 不会影响 JavaScript 运行时行为),他们将有错误的类型。具体来说,它们将是 HTMLElement 类型,这是 HTMLInputElement 的超类型,它没有我们想要访问的 value 属性。

那么我们能做些什么呢?我们可以将所有返回的元素“转换”为HTMLInputElement,但这很丑陋、容易出错(我们必须记住所有类型名称以及它们如何映射到它们的标签名称)、冗长且有点迟钝,我们知道得更好,而且我们静态地知道得更好。

因此,有必要对tagnamegetElementsByTagName 的参数)与它实际返回的元素类型之间的关系进行建模。

输入字符串文字类型:

字符串字面量类型是一种更精细的字符串类型,它是单例类型,就像undefined 它只有一个值,字面量字符串。一旦我们有了这种类型,我们就可以重载getElementsByTagName 的声明,使其更加精确和有用

declare function getElementsByTagName(tagname: 'input'): NodeList<HTMLInputElement>;
declare function getElementsByTagName(tagname: string): NodeList<HTMLElement>;

我认为这清楚地表明了使用专门的字符串类型的实用性,从一个值派生并且仅由该单个值占据,但还有很多其他原因需要它们,所以我将讨论更多。

在前面的示例中,我会说易用性是主要动机,但请记住,TypeScript 的第一目标是通过编译时、静态分析来捕获编程错误。

鉴于此,另一个动机是精度。有许许多多的 JavaScript API 采用特定的值,根据具体的值,它们可能会做一些非常不同的事情,什么也不做,或者失败。

因此,对于另一个现实世界的示例,SystemJS 是一个出色且广泛使用的模块加载器,具有广泛的配置 API。您可以传递给它的选项之一称为transpiler,根据您指定的值,会发生非常不同的事情,此外,如果您指定无效值,它将尝试加载一个不存在并且无法加载其他任何内容。 transpiler 的有效值为:"plugin-traceur""plugin-babel""plugin-typescript"false。我们不仅要拥有 TypeScript 建议的这 4 种可能性,还要让它检查我们是否只使用了其中一种可能性。

在我们可以使用离散值作为类型之前,这个 API 很难建模。

我们最多只能写类似

transpiler: string | boolean;

这不是我们想要的,因为只有 3 个有效字符串,true 不是有效值!

通过使用值作为类型,我们实际上可以完美地描述这个 API

transpiler: 'plugin-traceur' | 'plugin-babel' | 'plugin-typescript' | false;

不仅知道我们可以传递哪些值,而且如果我们错误地输入'plugin-tsc' 或尝试传递true,则会立即得到错误。

因此,字面量类型可以尽早发现错误,同时支持对 Wilds 中现有 API 的精确描述。

另一个好处是控制流分析,它允许编译器检测常见的逻辑错误。这是一个复杂的话题,但这里是一个简单的例子:

declare const compass: 
  direction: 'N' | 'E' | 'S' | 'W'
;

const direction = compass.direction;
// direction is 'N' | 'E' | 'S' | 'W'
if (direction === 'N')  
  console.log('north');
 // direction is 'E' | 'S' | 'W'
else if (direction === 'S') 
  console.log('south');
 // direction is 'E' | 'W'
else if (direction === 'N')  // ERROR!
  console.log('Northerly');

上面的代码包含一个比较简单的逻辑错误,但是条件复杂,人为因素多,在实践中很容易漏掉。第三个if 本质上是死代码,它的主体永远不会被执行。文字类型赋予我们将可能的罗盘direction 声明为“N”、“S”、“E”或“W”之一的特殊性,使编译器能够立即将第三个 if 语句标记为无法访问、实际上是无意义的代码这表明我们的程序中有一个错误,一个逻辑错误(毕竟我们只是人类)。

同样,我们有一个主要的激励因素来定义与可能值的非常特定子集相对应的类型。最后一个例子最好的部分是它都在我们自己的代码中。我们想声明一个合理但高度具体的合同,该语言为我们提供了表达能力,然后在我们违反自己的合同时抓住了我们。

以及 Javascript 如何在编译时处理这些问题?

与所有其他 TypeScript 类型完全相同。它们完全从 TypeScript 编译器发出的 JavaScript 中删除。

它与只读和常量有何不同?

像所有 TypeScript 类型一样,您所说的类型,表示特定值的类型,与 constreadonly 修饰符交互。这种交互有点复杂,既可以像我在这里做的那样浅显地处理,也可以很容易地组成一个 Q/A 本身。

我只想说,constreadonly 对可能的值有影响,因此变量或属性可以在任何时候实际持有的可能类型,因此产生文字类型,即特定类型的类型价值,更容易传播,推理,也许最重要的是为我们推断。

因此,当某些东西是不可变的时,通常推断其类型尽可能具体是有意义的,因为它的值不会改变。

const x = 'a'; 

推断x 的类型为'a',因为它无法重新分配。

let x = 'a';

另一方面,推断x 的类型为string,因为它是可变的。

现在你可以写了

let x: 'a' = 'a';

在这种情况下,虽然它是可变的,但它只能被分配一个'a'类型的值。

请注意,出于说明目的,这有点过于简单化了。

在上面的if else if 示例中可以观察到,还有其他机制在起作用,这表明该语言还有另一层,即控制流分析层,用于跟踪可能的值类型,因为它们被条件限制了、赋值、真值检查和其他构造,如解构赋值。


现在让我们逐个属性地详细检查您问题中的类:

export class MyComponent 
  // OK because we have said `error` is of type 'test',
  // the singleton string type whose values must be members of the set 'test'
  error: 'test' = 'test';
  // NOT OK because we have said `error` is of type 'test',
  // the singleton string type whose values must be members of the set 'test'
  // 'test1' is not a member of the set 'test'
  error: 'test' = 'test1';
  // OK but a word of Warning:
  // this is valid because of a subtle aspect of structural subtyping,
  // another topic but it is an error in your program as the type `Boolean` with a
  // capital "B" is the wrong type to use
  // you definitely want to use 'boolean' with a lowercase "b" instead.
  error: Boolean = true || false;
  // This one is OK, it must be a typo in your question because we have said that 
  // `error` is of type true | false the type whose values must
  // be members of the set true, false and true satisfies that and so is accepted
  error: true | false = true;
  // OK for the same reason as the first property, error: 'test' = 'test';
  error: true = true;
  // NOT OK because we have said that error is of type `true` the type whose values
  // must be members of the set true
  // false is not in that set and therefore this is an error.
  error: true = false;
  // OK this is just a type declaration, no value is provided, but
  // as noted above, this is the WRONG type to use.
  // please use boolean with a lowercase "b".
  error: Boolean;
  // As above, this is just a type, no value to conflict with
  error: true;
  // OK because we have said `error` is of type 1 (yes the number 1),
  // the singleton number type whose values must be members of the set 1
  // 1 is a member of 1 so we are good to go
  error: 1 = 1;
  // NOT OK because we have said `error` is of type 1 (yes the number 1),
  // the singleton number type whose values must be members of the set 1
  // 2 is NOT a member of 1 so this is an error.
  error: 1 = 2;


TypeScript 是关于类型推断的,推断越多越好,因为它能够传播来自值(例如表达式)的类型信息,并使用它来推断更精确的类型。

在大多数语言中,类型系统从类型开始,但在 TypeScript 中,几乎总是如此,类型系统从值开始。所有值都有类型。对这些值的操作会产生具有新类型的新值,从而允许类型干扰进一步传播到程序中。

如果您将纯 JavaScript 程序粘贴到 TypeScript 文件中,您会注意到,在不添加任何类型注释的情况下,它能够了解您程序的结构。文字类型进一步增强了这种能力。

关于字面量类型还有很多要说的,为了解释的目的,我省略并简化了某些内容,但请放心,它们很棒。

【讨论】:

真的是我期待的好答案:+1 但她我有一个疑问And how Javascript handle those on compile time?。它编译普通的javascript意味着编译器如何知道这是一个常量变量或值类型变量? 我想我明白了。这不是它们不同的问题,而是当某些东西是不可变的时,通常将其类型推断为尽可能具体是有意义的,因为它的值不会改变。就像const x = 'a'; 一样,将x 的类型推断为'a',因为它不能被重新分配。另一方面,let x = 'a';x 的类型推断为string,因为它是可变的。现在你可以写let x: 'a' = 'a'; in which case, although it is mutable it can only be assigned a value of type 'a'`了。 我从你身边得到了一切:)。我稍后会给我的赏金。我还有一个与 void 和 never 相关的问题。我会问一个问题,然后我会打电话给你 @RameshRajendran 很公平,无论如何我都计划向这个问题添加更多信息,但可能要到明天。所以提出问题!【参考方案2】:

为什么 TypeScript 接受一个值作为数据类型?

这是字符串字面量类型的扩展,这个 PR 解释它:literal types

JavaScript 在编译时如何处理这些?

它是纯粹的打字稿创建,不会影响生成的 javascript。

它与只读和常量有何不同?

嗯 - 它不会是只读的。它只允许一个值。检查这个例子:

export class MyComponent

    readonly error = 1;
    error1: 1 = 1;

    public do()
    
        this.error = 1; //Error. The field is readonly
        this.error1 = 1; //No error - because the field is not readonly
        this.error1 = 2; //Error. Type mismatch
    

【讨论】:

是的!同意只读答案。但是我们不能更改该变量的任何其他值!那么 it5 是否像只读一样工作?对不对? 技术上 - 是的。但通常你用readonly 标记一些东西,以确保没有人会因为某些业务逻辑限制而试图改变它的值。认为您的代码可能会随着时间的推移而更改,并且类型限制将被取消或扩展。但readonly 仍将发挥作用。而这种骇人听闻的方法将失败。 另外,在 --strictNullChecks 关闭(默认关闭)的情况下,您始终可以为文字类型的变量分配两个附加值:nullundefined。跨度> 我没有收到 Its pure typescript creation, that will not affect resulting javascript. 。 ts 代码将编译为 javascript 代码。那么这种数据类型要怎么编译呢? 请注意,如果您还希望在运行时保持只读性,例如面对甚至可能没有您的类型声明的任意消费者,那么只读修饰符虽然有用是不够的。在这种情况下,您可以仅使用 get 属性或冻结您的对象。我意识到这与问题无关,但值得一提。【参考方案3】:

一个原因是要为同一个变量处理多种类型。这就是 typescript 允许您为类型使用特定值的原因。

let x: true | false | 'dog';
x = true; // works
x = false; // works
x = 'cat'; // compilation error

在这种情况下,let x: true 只是一种特殊情况,其中只有一种类型。

看起来字符串文字类型功能已被扩展以允许其他类型的值。也许有一个更好的文档示例,但我只能找到手册 here 中的字符串文字类型部分。

【讨论】:

但我没有定义任何多种数据类型,请查看我更新的问题:) typescript 允许您使用一些值作为变量的类型,仅此而已。主要是这样你就可以创造出这种东西。如果您选择仅设置一个值 typescript 不会阻止您。 :)

以上是关于为啥 TypeScript 接受值作为数据类型?的主要内容,如果未能解决你的问题,请参考以下文章

为啥类型 `Record<string,unknown>` 不接受具有已定义键的对象作为值

为啥人们将 typescript 的类型作为依赖项存储在 package.json(而不是 devDep)中? [复制]

为啥 TypeScript 中的 'instanceof' 会给我错误“'Foo' 仅指一种类型,但在这里被用作值。”?

如何键入 Typescript 数组以仅接受一组特定的值?

为啥 Array.prototype.reduce() 不接受 Map 对象作为初始值?

为啥 TypeScript 接受被覆盖方法的正确返回,即使它不可访问?