为什么TypeScript接受值作为数据类型? [英] Why does TypeScript accept value as a data type?

查看:142
本文介绍了为什么TypeScript接受值作为数据类型?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

为什么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';

解决方案

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

通常,类型表示一组0或多个值.这些价值观可以被认为是该类型的成员或居民.

就它们可以采用的多种值而言,类型往往属于3组之一.

第1组:例子:string类型.所有字符串值都使用string类型.由于字符串可以有效地任意长,因此本质上是string类型成员的无限数值.属于该类型的成员的值的集合是所有可能的字符串的集合.

第2组:例子:undefined类型. undefined类型只有一个值,即undefined值.因此,这种类型通常称为单例类型,因为其成员的集合只有一个值.

第3组:例子:never类型. never类型没有成员.根据定义,不可能具有类型为never的值.在专业人士中阅读它时,似乎有些困惑,但是有一个小的代码示例可以对其进行解释.

考虑:

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

在上面的示例中,

函数getValue的返回类型为never,因为它从不返回始终返回的值.因此,如果我们写

const value = getValue();

value的类型也将为never.

第一个问题:

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

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

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

现在,让我们考虑一下如何写下该函数的类型签名.我们的第一个刺可能是类似的

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

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

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

那我们该怎么办?我们可以将所有返回的元素投射"到HTMLInputElement,但这很丑陋,容易出错(我们必须记住所有类型名称以及它们如何映射到标签名称),冗长且有点晦涩,我们知道得更好,而且我们对静态地了解得更多.

因此,有必要对tagname(它是getElementsByTagName的参数)与其实际返回的元素类型之间的关系进行建模.

输入字符串文字类型:

字符串文字类型是一种更精细的字符串类型,它是单例类型,就像undefined一样,它只有一个值,即文字字符串.一旦有了这种类型,我们就可以 overload getElementsByTagName的声明,使其更加精确和有用

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

我认为这清楚地展示了具有特殊的字符串类型的实用性,该字符串类型是从一个值派生的,并且仅被该单个值所占用,但是拥有它们的原因还有很多,因此我将进一步讨论.

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

鉴于此,另一个动机是 precision .有许多采用特定值的JavaScript API,根据其具体含义,它们可能会做一些非常不同的事情,什么都不做,或者很难失败.

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

在我们可以使用离散值作为类型之前,此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文件中,您会注意到,无需添加任何类型注释,它就能弄清楚程序的结构.文字类型进一步增强了此功能.

关于文字类型,还有很多可以说的,为了说明起见,我已经删除并简化了某些内容,但是请放心,它们很棒.

Why does TypeScript accept value as a data type?

These scenarios below are accepting and non-acceptable declarations.

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'; vs. error: 'test' = 'test';

解决方案

First, some informal background and information that will help us discuss the questions you have posed:

In general a type represents a set of 0 or more values. Those values can be thought of as the members or inhabitants of that type.

In terms of this multiplicity of values that they can take, types tend to fall in 1 of 3 groups.

Group 1: Case in point: the string type. The string type is inhabited by all string values. Since a string can effectively be arbitrarily long, there are essentially an infinite number values that are members of the string type. The set of values that are members this type is the set of all possible strings.

Group 2: Case in point: the undefined type. The undefined type has exactly one value, the undefined value. This type is thus often referred to as a singleton type because the set of its members has only 1 value.

Group 3: Case in point: the never type. The never type has no members. It is not possible, by definition, to have a value that is of type never. This can seem a little confusing when you read about it in pros but a small code example serves to explain it.

Consider:

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

in the example above, the function getValue has a return type of never because it never returns a value, it always throws. Therefore if we write

const value = getValue();

value will be of type never as well.

Now on to the first question:

Why typescript allow value as a data type?

There are many, many reasons but a few particularly compelling ones are

To model the behavior of functions that behave differently depending on the values that are passed to them. One example that comes to mind is the function document.getElementsByTagName. This function always takes a value of type string, and always returns a NodeList containing HTMLElements. However, depending on the actual string value it is passed, it will return completely different types of things in that list. The only thing in common between these elements is that they all derive from HTMLElement.

Now, let us think about how we would write down the type signature of this function. Our first stab might be something like

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

This is a correct, but it is not particularly useful. Imagine we want to get the values of all HTMLInput elements on a page so we can send them to our server.

We know that, getElementsByTagName('input'), actually returns only the input elements on the page, just what we want, but with our definition above, while we of course get the right values (TypeScript does not impact JavaScript runtime behavior), they will have the wrong types. Specifically they will be of type HTMLElement, a supertype of HTMLInputElement that does not have the value property that we want to access.

So what can we do? We can "cast" all the returned elements to HTMLInputElement but this is ugly, error prone (we have to remember all of the type names and how they map to their tag names), verbose, and sort of obtuse, we know better, and we know better statically.

Therefore, it becomes desirable to model the relationship between the value of tagname, which is the argument to getElementsByTagName and the type of elements it actually returns.

Enter string literal types:

A string literal type is a more refined string type, it is a singleton type, just like undefined it has exactly one value, the literal string. Once we have this kind of type, we are able to overload the declaration of getElementsByTagName making it superiorly precise and useful

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

I think this clearly demonstrates the utility of having specialized string types, derived from a value and only inhabited by that single value, but there are plenty of other reasons to have them so I will discuss a few more.

In the previous example, I would say ease of use was the primary motivation, but remember that TypeScript's #1 goal is to catch programming errors via compile time, static analysis.

Given that, another motivation is precision. There are many, many JavaScript APIs that take a specific value and depending on what it is, they may do something very different, do nothing at all, or fail hard.

So, for another real-world example, SystemJS is an excellent and widely used module loader that has an extensive configuration API. One of the options you can pass to it is called transpiler and depending on what value you specify, non-trivially different things will happen and, furthermore, if you specify an invalid value, it will try to load a module that does not exist and fail to load anything else. The valid values for transpiler are: "plugin-traceur", "plugin-babel", "plugin-typescript", and false. We want to not only have these 4 possibilities suggested by TypeScript, but also to have it check that we are using only 1 of these possibilities.

Before we could use discrete values as types, this API was hard to model.

At best, we would have to write something like

transpiler: string | boolean;

which is not what we want since there are only 3 valid strings and true is not a valid value!

By using the values as types, we can actually describe this API with perfect precision as

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

And not only know which values we can pass but immediately get an error if we mistakenly type 'plugin-tsc' or try to pass true.

Thus literal types catch errors early while enabling precise descriptions of existing APIs in the Wilds.

Another benefit is in control flow analysis which allows the compiler to detect common logic errors. This is a complex topic but here is a simple example:

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');
}

The code above contains a relatively simple logic error, but with complex conditionals, and various human factors, it is surprisingly easy to miss in practice. The third if is essentially dead code, its body will never be executed. The specificity that literal types granted us to declare the possible compass direction as one of 'N', 'S', 'E', or 'W' enabled the compiler to instantly flag the third if statement as unreachable, effectively nonsensical code that indicates a bug in our program, a logic error (We are only Human after all).

So again, we have a prime motivating factor for being able to define types that correspond to a very specific subset of possible values. And the best part of that last example was that it was all in our own code. We wanted to declare a reasonable but highly specific contract, the language provided the expressiveness for us to do so, and then caught us when we broke our own contract.

And how Javascript handle those on compile time?

The exact same way as all other TypeScript types. They are completely erased from the JavaScript emitted by the TypeScript compiler.

And How it will differ from readonly and constant?

Like all TypeScript types, the types you speak of, those that indicate a specific value, interact with the const and readonly modifiers. This interaction is somewhat complex and can either be addressed shallowly, as I will do here, or would easily comprise a Q/A in its own right.

Suffice it to say, const and readonly have implications on the possible values and therefore the possible types that a variable or property can actually hold at any time and therefore make literal types, types that are the types of specific values, easier to propagate, reason about, and perhaps most importantly infer for us.

So, when something is immutable it generally makes sense to infer its type as being as specific as possible since its value will not change.

const x = 'a'; 

infers the type of x to be 'a' since it cannot be reassigned.

let x = 'a';

on the other hand, infers the type of x to be string since it is mutable.

Now you could write

let x: 'a' = 'a';

in which case, although it is mutable it can only be assigned a value of type 'a'.

Please note that this is somewhat of an oversimplification for expository purposes.

There is additional machinery at work as can be observed in the if else if example above which shows that the language has another layer, the control flow analysis layer, that is tracking the probable types of values as they are narrowed down by conditionals, assignments, truthy checks, and other constructs like destructuring assignment.


Now let us examine the class in your question in detail, property by property:

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 is all about type inference, the more inference the better, as it is able to propagate type information that comes from values, such as expressions, and use that to infer even more precise types.

In most languages, the type system starts with types but in TypeScript, and this has pretty much always been the case, the type system starts with values. All values have types. Operations on these values yield new values with new types allowing for type interference to propagate further into the program.

If you paste a plain JavaScript program into a TypeScript file, you'll notice that, without the addition of any type annotations, it's able to figure out much about the structure of your program. Literal types further enhance this capability.

There is a lot more that can be said about literal types, and I have elided and simplified certain things for explanatory purposes but rest assured they are awesome.

这篇关于为什么TypeScript接受值作为数据类型?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆