Objects
Objects are the fundamental way in which data and functions are grouped together and passed around.
An object is a comma-separate list of key-value pairs, enclosed in curly braces ({
and }
). A key is a unique identifier, which can be used to efficiently look up a particular value.
To improve a program's readability, the key-value pairs will often be written on separate lines. We can use the dot operator (.
) to retrieve the value corresponding to a particular key
with obj.key
.
In the example above, the person
object has an implicit typing (i.e. one that TypeScript figured out) where the name
and age
key correspond
to string
and number
. However, the TypeScript compiler will become especially useful when you formally tell it about the
structure of objects being used in a program.
Interfaces
An interface
declaration defines the key and value types of an object.
The main purpose of an interface is to tell the TypeScript compiler about how an object is structured. All interface declarations are removed by the compiler when it generates JavaScript code.
key: value
key2: value2
}
You can also use type
to define the structure of an object - both examples are shown below.
Structural Compatibility
TypeScript uses structural typing to determine whether two types are compatible. This means that compatibility is based on the shape of the types — the properties they have and how they're typed — rather than their explicit declarations.
A Point3D
object can be used wherever a Point2D
is expected, because it has at least the required properties x
and y
. We also don't need to explicitly set the type of pt
to Point3D
- as long as the type inferred by TypeScript for pt
matches Point3D
, it is allowed to be used as a Point3D
argument.
This flexibility allows for safer, more adaptable programs. However, it also means that TypeScript does not enforce nominal typing (i.e., type compatibility based on name), which can be surprising for developers coming from other statically typed languages.
Type Composition
The ampersand operator (&
) can be used to construct an intersection type which contains all the members of two or more types.
Notice that the &
actually joins the two types. More precisely, A & B
creates a new type C
such that every object that matches either A
or B
will fit the description of C
. This is often useful for constructing a safe interface for operating with multiple other interfaces.
This also means that two overlapping keys that map to different value types will produce a resulting value of never
.
The type of never
indicates to any operations that depend on values of type C
that the foo
property should not be accessed. Consider the exact same example as above, but with the union type operator (|
) instead.
If you played with the example above, you might have noticed that the union type will only contain keys that are in both object types.
Type composition
Let's look at a more practical example to understand the implications of all this. Suppose we have two functions for sending emails, and we want to merge them into one.
We have a roughly equivalent choice here of using Employee | Customer
or Employee & Customer
as the first parameter of our new composed function if we
are only interested in the name
and email
field.
The difference is that by choosing a union type (|
) instead, we being more restrictive with the information that the function has available to operate on. There are certainly cases where &
is more appropriate, but in this case, we are telling the sendEmail
function that a Recipient
may have a badge
and birthday
property that can also be accessed.
Also notice what happens if we update the Customer
type to include a required key. We can no longer send emails with Employee
objects, because the intersection type also contains a required key that Employee
objects do not have.
Indexed Types
The value type for a specific key can be extracted with an indexed access type.
The indexing type is itself a type, so we can use unions keyof
, or other types.
You will even get an error if you try to access a property which doesn't exist.
Optional properties
Add a ?
after a property name to mark it as optional.
When a property is optional, it either has a value of undefined
or is not present in an object's keys. Keep in mind that undefined
is different from null
, so you would need to specifically include null
to include it in the type.
If a key is not marked as optional, it is required. An object that is missing a required key will generate a TypeScript error.
Partial and Required
The Partial
type produces a new object type where all properties are optional, and Required
produces an object type where all properties are required.
The Partial<T>
specification is often used when working with a subset of an object's properties. In the example below, a simple updateUser
function allows a partial set of user properties to be updated.
We can combine Partial
and Required
to build a type where some keys are optional, and others are required. Suppose we want to store all color definitions properties in a single Color
interface:
Readonly
A readonly
property can only be initialized once.
Index signatures
Index signatures allow you to define the shape of objects that may have unknown or dynamically named properties.
This is useful when dealing with objects where property names are not predetermined but share a common value type.
The
It is
Extending and Merging Object Types
An interface can inherit the property definitions of another interface with the extends
keyword.
Excess property checks
Mapped types
A mapped type iterates through a set of keys to create a new type.
You can apply a ?
modifier during mapping to make the mapped values optional, or use a -?
to make the mapped value required.
If you are using a more recent version of TypeScript, you can re-map keys in mapped types with an as
clause.
You can also filter out keys by conditionally producing the never
type.
You can map over arbitrary unions, not just string | number | symbol
.
Mapped types can be useful in enforcing compiler guarantees based on the data and constants in your programs.
Object from tuples
The Object.fromEntries
function produces an object from an array of tuples, where
the first element of each tuple is the key and the second element is the value.
Note that Object.fromEntries
requires your TypeScript compiler target to be set to ES2019
or later.