Typescript Core Features
Objects
Caveat for Excess Property Checking
Checks if an object literal contains properties that are not present in the target type. If a property does not exist in the target type, TypeScript will raise an error.
Excess property checks only trigger for object literals being created in locations that are declared to be an object type. Providing an existing object literal bypasses excess property checks. This is because TypeScript assumes that the object has already been created and any excess properties are intentional.
Optional Properties
Optional properties using ?
can either exist or not exist in the object.
There is a difference between optional properties and properties whose type happens to include undefined
in a type union.
A property declared as optional with ?
is allowed to not exist.
A property declared as required and | undefined
must exist, even if the value is undefined.
Inferred Object-Type Unions
If a variable is given an initial value that could be one of multiple object types, TypeScript will infer its type to be a union of object types. That union type will have a constituent for each of the possible object shapes. Each of the possible properties on the type will be present in each of those constituents, though they’ll be ? optional types on any type that doesn’t have an initial value for them.
This poem value always has a name property of type string, and may or may not have pages and rhymes properties:
Explicit Object-Type Unions
Most notably, if a value’s type is a union of object types, TypeScript’s type system will only allow access to properties that exist on all of those union types.
Accessing names
is allowed because it always exists, but pages
and rhymes
aren’t guaranteed to exist.
Narrowing Object Types
TypeScript’s type narrowing will apply to objects if you check their shape in code.
Note that TypeScript won’t allow truthiness existence checks like if (poem.pages). Attempting to access a property of an object that might not exist is considered a type error, even if used in a way that seems to behave like a type guard:
Discriminated Unions
Another popular form of union typed objects in JavaScript and TypeScript is to have a property on the object indicate what shape the object is. This kind of type shape is called a discriminated union, and the property whose value indicates the object’s type is a discriminant. TypeScript is able to perform type narrowing for code that type guards on discriminant properties.
Intersection Types
Intersection types are a way to combine multiple types into one type using the &
operator.
This creates a new type that has all the properties and methods of each individual type.
We can combine Intersection types with union types.
There are dangers of using intersection types:
Long assignability errors
: Intersection types can make the resulting type more complex and difficult to read, especially if the types being intersected are already complex.Conflicting types
: If two types being intersected have conflicting properties, it can lead to unexpected behavior. For example, if one type has a property that's optional and another has the same property as required, the resulting type will have the property as required, which may not be what you intended.type NotPossible = number & string;
The never
keyword and type is what programming languages refer to as a bottom type, or empty type. A bottom type is one that can have no possible values and can’t be reached. No types can be provided to a location whose type is a bottom type.
Functions
Optional Parameters
Optional parameters are not the same as parameters with union types that happen to include | undefined
.
Parameters that aren’t marked as optional with a ?
must always be provided, even if the value is explicitly undefined.
Any optional parameters for a function must be the last parameters. Placing an optional parameter before a required parameter would trigger a TypeScript syntax error:
Rest Parameters
The ... spread
operator may be placed on the last parameter in a function declaration
to indicate any “rest” arguments passed to the function starting at that
parameter should all be stored in a single array. We can use []
syntax to
indicate it's an array of arguments.
Void returns
The void
keyword is used to declare the return type of a function that doesn't return anything.
This is useful for functions that only perform some action, like logging, and don't need to return a value indicating that any returned value from the function would be ignored.
It's important to note that void
is not the same as undefined
. void
means that
the return type of a function will be ignored,
while undefined
is a literal value that can be returned.
Trying to assign a value of type void
to a value whose type includes undefined
will result in a type error.
For example built-in forEach
method on arrays takes in a callback that returns void
. Functions provided to forEach
can return any value they want.
Return value will be ignored.
Never returns
Some functions not only don’t return a value, but aren’t meant to return at all. Never-returning functions are those that always throw an error or run an infinite loop.
If a function is meant to never return, adding an explicit : never
type annotation indicates that any code after a call to that function won’t run.
In below example, the convertNumberToString
function takes in a number and returns a string.
However, if the input number is negative, the function throws an error and never returns a string value.
To indicate this in the function's signature, we can use the never
type like this:
By adding | never
to the return type, we're indicating that the function will never actually return a string value if the input number is negative.
never
is not the same as void
. void
is for a function that returns nothing. never
is for a function that never returns.
Arrays
Array Inference
The following firstCharAndSize function is inferred as returning (string | number)[]
,
not [string, number]
, because that’s the type inferred for its returned array literal.
It assumes a flexible size array rather than a fixed size tuple.
Explicit tuple type
Tuple types may be used in type annotations. If the function is declared as returning a tuple type and returns an array literal, that array literal will be inferred to be a tuple instead of a more general variable-length array
Const asserted tuples
TypeScript provides an as const
operator known as a const assertion
that can be placed after a value.
Const assertions tell TypeScript to use the most literal, read-only possible form of the value when inferring its type.
If one is placed after an array literal, it will indicate that the array should be treated as a tuple:
Note that as const assertions go beyond switching from flexible sized arrays to fixed size tuples: they also indicate to TypeScript that the tuple is read-only and cannot be used in a place that expects it should be allowed to modify the value.
In practice, read-only tuples are convenient for function returns. Returned values from functions that return a tuple are often destructured immediately anyway, so the tuple being read-only does not get in the way of using the function.
Interfaces
Interfaces are another way to declare an object shape with an associated name. Interfaces are in many ways similar to aliased object types but are generally preferred for their more readable error messages, speedier compiler performance, and better interoperability with classes.
Key differences between interfaces and type aliases:
-
Declaration merging: Interfaces can "merge" together, which allows you to combine multiple interface declarations into a single definition.
-
Type checking class declarations: Interfaces can be used to type check the structure of class declarations while type aliases cannot.
-
Speed: Interfaces are generally speedier for the TypeScript type checker to work with. They declare a named type that can be cached more easily internally, rather than a dynamic copy-and-paste of a new object literal the way type aliases do.
-
Readablity of errors: Because interfaces are considered named objects rather than an alias for an unnamed object literal, their error messages are more likely to be readable i n hard edge cases.
Read-Only Properties
TypeScript allows you to add a readonly modifier before a property name to indicate that once set, that property should not be set to a different value. These readonly properties can be read from normally, but not reassigned to anything new.
Noote that they’re a type system construct only and don’t exist in the compiled JavaScript output code. They only protect from modification during development with the TypeScript type checker.
Functions and Methods
TypeScript provides two ways of declaring interface members as functions:
- Method syntax: declaring that a member of the interface is a function intended
to be called as a member of the object, like
member(): void
. - Property syntax: declaring that a member of the interface is equal to a
standalone function, like
member: () => void
The two declaration forms are an analog for the two ways you can declare a JavaScript object as having a function. There are some differences but let's not get into that it’ll rarely impact your code.
Call Signatures
Interfaces and object types can declare call signatures, which is a type system description of how a value may be called like a function.
Interfaces can be used to define not only the shape of objects but also the shape of function types, including call signatures. Call signatures are used to describe the parameters and return type of a function type.
Index signatures
Index signatures allow you to define the types of properties that are not known ahead of time.
In JavaScript, you can use bracket notation to access properties of an object.
For example, you can use myObj["myProp"]
to access the value of a property
named myProp
on an object named myObj
.
In TypeScript, you can use index signatures to define the type of these unknown
properties. An index signature has the following syntax: [propertyName: type]: valueType
Interface Extensions
TypeScript allows an interface to extend another interface, which declares it as copying all the members of another. This means that the new interface will inherit all the properties and methods of the base interface, and you can add new properties or methods to it.
Overridden Properties
Derived interfaces may override, or replace, properties from their base interface by declaring the property again with a different type. This is useful when you want to customize or extend the behavior of an existing interface.
Interface Merging
One of the important features of interfaces is their ability to merge with each other. Interface merging means if two interfaces are declared in the same scope with the same name, they’ll join into one bigger interface under that name with all declared fields.
Interface merging isn’t a feature used very often in day-to-day TypeScript development. I would recommend avoiding it when possible, as it can be difficult to understand code where an interface is declared in multiple places.
However, interface merging is particularly useful when working with third-party libraries, as it enables developers to extend the functionality of existing interfaces without having to modify their source code. For example, when using the default TypeScript compiler options, declaring a Window interface in a file with a myEnvironmentVariable property makes a window.myEnvironmentVariable available:
Member Naming Conflicts
If there are member naming conflicts between the interfaces, TypeScript will raise an error.