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

Why does TypeScript accept value as a data type?

为什么 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
    }

  • Why does TypeScript allow a value as a data type?
  • How does JavaScript handle these at compile time?
  • How does it differ from readonly and constant?

readonly error= 'test'; 对比 error: 'test' = 'test';

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

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

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

似乎扩展了字符串文字类型功能以允许其他类型的值。也许有更好的文档示例,但我只能找到手册中的字符串文字类型部分 here.

Why does TypeScript accept a value as a data type?

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

How does JavaScript handle these at compile time?

它是纯 typescript 创建的,不会影响结果 javascript。

How does it differ from readonly and constant?

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

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
    }
}

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

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

就他们可以取的值的多样性而言,类型往往属于 3 组中的 1 组。

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

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

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

考虑:

function getValue(): never {
  throw Error();
}

在上面的例子中,函数 getValue 有一个 return 类型的 never 因为它 never return 是一个值,它总是抛出。因此,如果我们写

const value = getValue();

value 也将是 never 类型。

现在开始第一个问题:

Why typescript allow value as a data type?

有很多很多原因,但有几个特别引人注目的是

根据传递给它们的值对函数的行为进行建模。想到的一个例子是函数 document.getElementsByTagName。此函数始终采用 string 类型的值,并且始终 return 是包含 HTMLElementNodeList。然而,根据传递给它的实际字符串值,它会 return 列表中完全不同类型的东西。这些元素之间唯一的共同点是它们都派生自 HTMLElement.

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

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

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

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

那我们能做什么呢?我们可以 "cast" 所有 returned 元素到 HTMLInputElement 但这很丑陋,容易出错(我们必须记住所有类型名称以及它们如何映射到它们的标签名称),冗长,有点迟钝,我们知道得更多,我们知道得更多静态

因此,需要对 tagname 之间的关系建模,这是 getElementsByTagName 的参数和它的元素类型实际上 returns.

输入字符串文字类型:

字符串字面量类型是一种更细化的字符串类型,它是单例类型,就像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.

,则会立即出现错误

因此文字类型及早捕获错误,同时能够精确描述荒野中现有的 APIs。

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

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 语句是无法访问的,实际上是无意义的代码,表明我们的程序中存在错误,逻辑错误(毕竟我们只是人类)。

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

And how Javascript handle those on compile time?

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

And How it will differ from readonly and constant?

与所有 TypeScript 类型一样,您所说的类型,即表示特定值的类型,与 constreadonly 修饰符交互。这种交互有些复杂,可以像我将在此处做的那样浅层处理,也可以很容易地包含一个 Q/A 本身。

可以这么说,constreadonly 对可能的值有影响,因此对变量或 属性 实际可以随时保存的可能类型有影响,因此使文字类型,即特定值类型的类型,更易于传播、推理,也许最重要的是为我们推断。

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

const x = 'a'; 

推断 x 的类型为 'a' 因为它不能被重新分配。

let x = 'a';

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

现在你可以写了

let x: 'a' = 'a';

在这种情况下,尽管它是可变的,但它只能被分配一个 'a'.

类型的值

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

在上面的 if else if 示例中可以观察到其他机制在起作用,该示例表明该语言还有另一层,即控制流分析层,它跟踪可能的值类型通过条件、赋值、真值检查和解构赋值等其他结构缩小范围。


现在让我们详细检查您问题中的 class,属性 by 属性:

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 文件中,您会注意到,在不添加任何类型注释的情况下,它能够了解程序的很多结构。文字类型进一步增强了这种能力。

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