Basic Types
TypeScript's type system has quite a few advanced types that go beyond the basics of string
, number
, or boolean
. We introduced the any
type in the basic primer on types - now we will explore the behavior of special types like unknown
and never
, along with the TypeScript operators like extends
which allow us to construct types dynamically.
This section expands on the very basics of types with a more rigorous approach to type theory. This is where things get really interesting.
Types vs data
Types are meta-information - they only exist for the sake of enforcing how actual information is processed. Consider the simple program below.
In the JavaScript produced by the TypeScript compiler, there are no types - only the instructions that operate on data. Attempting to use a type as a value will produce an error.
Type assertions
The as
keyword is used to make a type assertion that a value is of a specified type. For example, consider the example below where we assert that y
has a number
type.
Clearly, y
is not actually holding a number
value. TypeScript can detect several situations where an obviously incorrect type assertion is taking place, but it is very easy to misuse this keyword when attempting to resolve compiler errors.
One common use for the as
keyword is with the as const
syntax, which indicates that a value should be inferred as a literal type rather than a general type.
Another common use of the as
keyword is to explicitly tell the TypeScript compiler that an array value is actually a tuple.
You can also use as
to represent a value as a less specific type.
Enums
Enums (short for "enumerations") are a set of named constants. They make your code more readable by replacing magic numbers or strings with meaningful names, and can be used as both a type and a value.
Each value in an enum can be associated with a unique number or string.
Consider the example below, where enum values are dynamically constructed in the getEvent
function.
Notice that the as
keyword allowed us to construct an enum value "spread_bread"
which is not in the original enum. As you can see, the responsibility of asserting types correctly inevitably falls to the programmer.
Type constraints
The satisfies
keyword ensures that a value matches a specified type without changing the value's inferred type. Unlike as
, it does not assert a type, but rather validates that some data matches a particular type during compilation.
The satisfies
operator was added to TypeScript in version 4.9. If your TypeScript compiler does not recognize it, you are likely running an outdated version.
Parameter contravariance
In type theory, contravariance means you can substitute a parameter type with a more general type (called a super type). For example, a function that accepts string | number
can be safely substituted for one that only accepts string
.
We can use satisfies
to check contravariant function types, such as the function valueChecker
which can be constrained to a StringChecker
type.
The parameter value
can either be string
or number
in valueChecker
, but also satisfies the more specific function type of StringChecker
. This is called parameter contravariance.
The inverse of this is called parameter covariance, where a parameter type can be substituted for a more specific type. TypeScript does allow parameter covariance, and will produce an error for the example below.
Applying to constants
Definitions of static data with as const
can be augmented with the satisfies
keyword to type check the constant value.
Conditional types
A conditional type is produced as a result of evaluating a ternary expression.
- todo: syntax
It is important to note in the example above that T
does not refer to an actual string, but rather a type of string. Here's a more practical example using arrays and tuples.
Conditional type distribution
When a conditional extends
is applied to a union type (e.g. T extends string | number
), TypeScript distributes the condition over each member of the union.
Conditional type distribution only happens if T
is a naked type parameter, which means it isn't wrapped in a tuple or other type construct.
The "never" type
This is a consequence of the distributive property of conditional types.
Essentially, (S | T) extends A ? B : C is equivalent to (S extends A ? B : C) | (T extends A ? B : C), i.e. the conditional type distributes over the union in the extends clause. Since every type T is equivalent to T | never, it follows that
T extends A ? B : C is equivalent to (T | never) extends A ? B : C, Which in turn is equivalent to (T extends A ? B : C) | (never extends A ? B : C), This is the same as the original type unioned with never extends A ? B : C. So a conditional type of the form never extends A ? B : C must evaluate to never, otherwise the distributive property would be violated.
The "unknown" type
Forced casting with unknown
I can't think of any realistic situation where it is advisable to do this. Since certain TypeScript error messages suggest using this approach for resolving type assertion errors, it has been included for the sake of reference.
If you find yourself forced casting as a means of resolving a TypeScript compilation error, you are most likely contradicting TypeScript's own automated inference and defeating the benefits of using TypeScript over JavaScript.
Type inference
The infer
keyword allows you to instruct the TypeScript compiler to infer a more specific type.