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
andconstant
?
readonly error= 'test';
vs. error: 'test' = 'test';
Variable Declaration in TypeScript The type syntax for declaring a variable in TypeScript is to include a colon (:) after the variable name, followed by its type. Just as in JavaScript, we use the var keyword to declare a variable. Declare its type and value in one statement.
any. ❌ Don't use any as a type unless you are in the process of migrating a JavaScript project to TypeScript. The compiler effectively treats any as “please turn off type checking for this thing”. It is similar to putting an @ts-ignore comment around every usage of the variable.
Primitive data types like string, number, boolean, undefined and the null object is passed in by value in TypeScript.
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 HTMLElement
s. 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With