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.

TypeScriptJavaScript
compiling...

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.

Loading TypeScript...

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.

Loading TypeScript...

Each value in an enum can be associated with a unique number or string.

Loading TypeScript...

Consider the example below, where enum values are dynamically constructed in the getEvent function.

Loading TypeScript...

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.

Loading TypeScript...

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

Loading TypeScript...

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.

Inference with template literals

Loading TypeScript...

Was this page helpful?