As TypeScript applications get more complex so do the types required to describe it. There are a number of built-in types that can be used for this purpose or combined to create more specialised types.
What I term modifying types such as Partial
and Required
are included in the language and I will quickly cover these first to warm up for the deeper types we’ll address later.
This article will quickly move on to focus on the slightly more advanced types beginning with Extract
. You can see the source of the various types by looking at the lib.es5.d.ts declaration file inside TypeScript.
Partial
This generic type takes a single argument, an object type, and returns a new type where all the properties are defined as optional.
With the application of the Partial
type TypeScript will interpret User
as the following type where all properties are now optional.
You can also get a little creative and keep some properties required when applying partial.
Whilst this works and the name
property is now mandatory there are easier ways to do this that will become apparent further into this article.
Required
Much like partial this type takes a single argument of an object type and returns a new type where all the properties are required.
interface ComputerRecord {
clockSpeed?: number
ram?: number
}
type Computer = Required<ComputerRecord>
Creates a new type with the following form when interpreted by TypeScript.
Readonly
Pretty much what it says on the tin; this type marks all the properties of an object type as readonly.
Revealing a new TypeScript type that takes the following shape.
Record
This type is a little different to the three types that we’ve already reviewed so far; it takes two arguments. A union of keys and a type. With this information TypeScript will construct a new type that includes each of these keys set to the supplied type.
Which TypeScript will expand into the following type when it is interpreted.
Again, you can get a little creative with this type and do some things like this.
Will create a type when interpreted that looks a lot like what you might write as:
Another neat trick is to use Record
to create types that include properties of multiple types.
That will create a type that will be interpreted into the following:
Extract (better known as intersection)
Set notation: A∩B
Items that exist in both the first and second arguments to the type are kept, but unique items from either side are dropped. This type essentially fills the role of an intersection between two types.
Describing the same operation in TypeScript code this type could be written using the in-built Array.prototype.filter()
function.
If you have a two union types and you want to the find the intersection then Extract
is very useful.
Exclude (better known as difference)
Set notation: A – B
Calculates the difference between two types (important to note that this is not the symmetrical difference). Everything that exists in the first argument excluding all items that appear in the second argument will be included in the resultant type.
// keep everything from the left excluding any from the right
type T2 = Exclude<
'a' | 'b' | 'x',
'a' | 'b' | 'z'
> // 'x'
This can also be described by the following TypeScript implementation code.
Exclude is used to narrow union types back down again. I am including the following code as a demonstration, but it is not production ready code and in some ways takes the form of pseudocode.
enum ConfigType {
INI,
JSON,
TOML
}
interface ConfigObject {
name: string
port: number
}
type JSONConfig = string
type TOMLConfig = string
type INIConfig = string
type ENVConfig = ConfigObject
type Config = JSONConfig | TOMLConfig | INIConfig | ENVConfig
type UnparsedConfig = Exclude<Config, ENVConfig | ConfigObject>
type ParsedConfig = Exclude<Config, UnparsedConfig>
function loadJsonConfig(cfg: JSONConfig): ParsedConfig {
return JSON.parse(cfg)
}
function loadTomlConfig(cfg: TOMLConfig): ParsedConfig {
return TOML.parse(cfg)
}
function loadIniConfig(cfg: INIConfig): ParsedConfig {
return INI.parse(cfg)
}
function loadConfig(cfg: UnparsedConfig): ParsedConfig {
if (isType(ConfigType.JSON, cfg)) {
return loadJsonConfig(cfg)
} else if (isType(ConfigType.TOML, cfg)) {
return loadTomlConfig(cfg)
} else if (isType(ConfigType.INI, cfg)) {
return loadIniConfig(cfg)
}
}
Pick
Set notation: A∩B
Similar to an intersection, but it is based on the keys defined in the first type argument. The second argument is a list of the keys to copy into the new type.
Here is a very contrived example of a possible use for Pick
:
interface Config {
host: {
uri: string
port: number
}
authentication: {
oauth: {
uri: string
}
}
}
// these get config functions could be loading from the environment
// or different files etc in a real application. Here they are hard
// coded for demonstration purposes.
const getHostConfig = (): Pick<Config, 'host'> => ({
host: {
uri: 'http://example.org',
port: 1337
}
})
const getAuthConfig = (): Pick<Config['authentication'], 'oauth'> => ({
oauth: {
uri: 'http://example.org'
}
})
const main = (cfg: Config) => {
// this is where you application code would probably be
}
// assemble the final config object by piecing together
// the various parts that were loaded up from the env etc.
// and start the application
main({ ...getHostConfig(), authentication: getAuthConfig() })
Omit
Set notation: A – B
Again, this type is similar to the Exclude
type, but it takes an object type and a list of keys. The keys indicate which properties should be dropped from the new object type.
This has recently been added to the set of types that come with TypeScript by default in 3.5, but older code will need to implement this manually using code like the follow.
Notice how it builds upon two types that we’ve already looked at - Exclude
and Pick
.
Using the same example types as Pick
we could have a function something like the following:
const startServer = (cfg: Omit<Config, 'authentication'>) => {
http.listen(cfg.host.port, () => {
console.log('Started...')
})
}
Difference (symmetrical)
Set notation: ’(A∩B) or (A∪B) - (A∩B)
Providing types for symmetrical difference is a little more difficult. This is where values that are unique from both the left and right should be included in the resultant type. Essentially this will lead to a final type that will be used in the following way.
type T5 = Difference<
{ a: number; b: number; x: number },
{ a: number; b: number; z: number }
>; // { x: number; z: number }
As I mentioned this is a fair bit more difficult than it sounds and there are a number of steps required so hang in there.
To produce this we must first workout the difference between the keys in each of the input types. We’ll first write a key differencing type - AMinusB
. This will take two object types and keep all the keys of A
that do not exist in B
.
export type AMinusB<A extends keyof any, B extends keyof any> = ({
[P in A]: P
} &
{ [P in B]: never } & { [x: string]: never })[A];
The set notation for this is A - B (as you would expect) and that makes this type is very similar to one that we’ve just explored - Omit
. AMinusB
is a little different in that it can take any two objects and calculate the keys that exist in A, but not in B. Omit on the other hand dictates that the keys it is supplied are on the object it is given.
To get the symmetrical difference of the keys we can execute the AMinusB
type twice and join them in a sum type.
export type SymmetricalKeyDiff<A extends object, B extends object> =
| AMinusB<keyof A, keyof B>
| AMinusB<keyof B, keyof A>;
Note that the key lists are flipped between the two calls to AMinusB
so as to get key difference both ways - thus powering the “symmetrical” part of this difference type.
With these two key types we can now create the final differencing type that will take the keys and apply them to an object type. Given what we’ve already learned about the inbuilt types we know that Pick
takes an object type and a list of keys and will return a new object type with just the specified properties/keys.
So, given SymmetricalKeyDiff
and Pick
we can create a symmetrical difference type. The input object for Pick
is the union of A
and B
and the list of keys is the SymmetricalKeyDiff
of A
and B
.
export type SymmetricalDiff<A extends object, B extends object> = Pick<
A & B,
SymmetricalKeyDiff<A, B>
>;
Putting this type into action looks something like this:
type T5 = SymmetricalDifference<
{ a: number; b: number; x: number },
{ a: number; b: number; z: string }
>; // { x: number; z: string }
Intersection
Using the same basic underlying types it is also possible to get the intersection of two object types.
export type Intersection<A extends object, B extends object> = Omit<
A & B,
SymmetricalKeyDiff<A, B>
>;
Put into practice this type can be used in the following way:
type T6 = Intersection<
{ a: number; b: number; x: number },
{ a: number; b: number; z: string }
>; // { a: number; b:number }
So, there you have it - some reasonably complicated types defined in TypeScript. Hopefully, you’ve been able to follow along until the end and you get some use out of what you’ve learnt here.