Once upon a time, there was a developer that had an issue with TypeScript. He wanted to share his issue with the community, but he didn't know how to properly write the title that would best describe his problem. He struggled to find the appropriate words as he didn't know how to name "this behavior" or "that kind of type mechanism". After hours of reflexion, he ended up posting an issue with a pretty awkward title.
This story encouraged me to start writing a glossary of TypeScript. Hopefully it will help you if you have to write an issue or communicate with other TypeScript developers.
Disclaimer: it was me, I was the developer that struggled. I still struggle though, but this means I still have things to learn, which is great!
You may not agree with some definitions or examples in this document. Please, feel free to leave a comment and let me know, only together can we improve this glossary! Also, if the stars are aligned and this gist gets more and more attention, I might move it into its own repository so we can all contribute via issues and pull-requests! 🚀
- String literal type
- Union type
- Discriminated union
- Intersection type
- Type guard
- Type narrowing
- Mapped type
- Conditional type
- Diagnostic message
String literal types were introduced in TypeScript v1.8. It is meant to expect only a specific set of strings, instead of "any string".
type Scheme = 'http' | 'https'
const prependWithScheme = (scheme: Scheme, domain: string, path: string): string =>
`${scheme}://${domain}/${path}`
prependWithScheme('http')
prependWithScheme('https')
prependWithScheme('')Here, no TypeScript error is raised for prependWithScheme('http') and prependWithScheme('https'). Even better, the IDE knows which values are available for autocompletion. However, an error is raised for prependWithScheme('') as the empty string '' is not availalbe in the string literal type Scheme.
Union types were introduced in TypeScript v1.4. It's a way of expressing mulitple possible types for a given value.
declare const value: string | number | booleanHere, value type can be either string, number or boolean.
Discriminated unions were introduced in TypeScript 2.0 as "tagged union types".
interface Square {
kind: 'square'
size: number
}
interface Rectangle {
kind: 'rectangle'
width: number
height: number
}
interface Circle {
kind: 'circle'
radius: number
}
type Shape = Square | Rectangle | CircleHere, Shape is the discriminated union, where the discriminant - also known as singleton property or tag - is the kind property.
You can also check the official documentation section on discriminated unions.
Intersection types were introduced in TypeScript v1.6 as a complement to union types. This allows us to type a value that is both a A and a B.
interface WithName {
name: string
}
interface WithAge {
age: number
}
type User = WithName & WithAgeThe User type is computed as { name: string, age: number }, which is the intersection of both types WithName and WithAge.
The intersection of types that have nothing in common results in the never type, introduced in TypeScript v2.0.
type A = 'a' & 'b'
type B = { name: string, age: number } & { name: number, adult: boolean }Here, A is never, because the string literals 'a' and 'b' have nothing in common.
For B, its type results in { name: never, age: number, adult: boolean } because string and number have nothing in common. As one of its properties type is never, there's no way to create a value which type is B, because no value can be assigned to the never type.
A type guard, whether it's built into the TypeScript language or provided by the developer, allows for a type to be narrowed in a conditional branch.
declare const value: string | number
const getNumberOfChars = (s: string): number => s.length
const isGreaterThan = (n: number, bound: number): boolean => n > bound
if (typeof value === 'string') {
console.log(`Number of chars in ${value}: ${getNumberOfChars(value)}.`)
} else if (typeof value === 'number') {
console.log(`Is ${value} greater than 20? ${isGreaterThan(value, 20) ? 'Yes' : 'No'}.`)
} else {
throw 'Impossible, perhaps the archives are incomplete'
}By checking the type of value with typeof, TypeScript knows that in the if branch, value must have that type and not the others:
- In the
if (typeof value === 'string') { ... }branch,valuetype is narrowed fromstring | numbertostring. - In the
if (typeof value === 'number') { ... }branch,valuetype is narrowed fromstring | numbertonumber. - In the
else { ... }branch, since all the possible types have been checked in the previousifconditions,valuetype is narrowed fromstring | numbertonever(it "can't" happen since all the possible cases have been checked already).
The developer can also create its own type guards thanks to the is syntax.
interface User {
name: string
age: number
}
// You can ignore this function for the sake of this example
const hasOwnProperty = <A extends {}, B extends PropertyKey>(obj: A, prop: B): obj is A & Record<B, unknown> =>
obj.hasOwnProperty(prop)
const isUser = (v: unknown): v is User =>
typeof v === 'object' &&
v !== null &&
hasOwnProperty(v, 'name') &&
typeof v.name === 'string' &&
hasOwnProperty(v, 'age') &&
typeof v.age === 'number'
declare const value: unknown
if (isUser(value)) {
console.log(`User(name = ${value.name}, age = ${value.age})`)
} else {
throw `Invalid user provided: ${JSON.stringify(value)}`
}We created the v is User type guard as the return value of the isUser function, which tells TypeScript that if isUser(value) returns true, then value is a User in the if branch, otherwise keep the initial type, which is unknown here.
You can also check the official documentation about type guards.
It's the ability for the TypeScript language to restrict the type of a value to a subset of that type.
type A =
| { kind: 'a', arg1: 'hey' }
| { kind: 'b', arg1: 'Hello', arg2: 'World' }
| { kind: 'c' }
declare const value: A
if (value.kind === 'a') {
console.log(`Value of kind ${value.kind} has argument ${value.arg1}`)
} else if (value.kind === 'b') {
console.log(`Value of kind ${value.kind} has arguments ${value.arg1} and ${value.arg2}`)
} else {
console.log(`Value of kind ${value.kind} has no argument`)
}- If
value.kind === 'a'is true, thenvaluetype is{ kind: 'a', arg1: 'hey' } - Otherwise, if
value.kind === 'b'is true, thenvaluetype is{ kind: 'b', arg1: 'Hello', arg2: 'World' } - Otherwise, since we've already handled the cases
{ kind: 'a', arg1: 'hey' }and{ kind: 'b', arg1: 'Hello', arg2: 'World' }, only the last one of the discriminated unionAis left, i.e.{ kind: 'c' }
More examples are available in the type guard section.
Mapped types were introduced in TypeScript v2.1. Essentially, mapped types can be used to make uniform type mapping, allowing a developer to transform a type into another type of the same "form":
-
A mapped type can use a string or subset of a string (i.e. string literal type to build an object thanks to the built-in
Recordmapped type:type A = Record<'firstname' | 'lastname' | 'surname', string> /** * { * firstname: string, * lastname: string, * surname: string * } */
-
A mapped type can be a subset of the original type, e.g. using
PickorOmit(cf. example below). -
A mapped type can change the optionality of all the properties of the original type using
Required,NullableorPartial. -
A mapped type can change the visibility of all the properties of the original type using
Readonly.
interface Config {
address: string
port: number
logsLevel: 'none' | 'error' | 'warning' | 'info' | 'debug'
}
type A = Partial<Config>
type B = Omit<Config, 'address' | 'logsLevel'>
type C = Pick<Config, 'logsLevel'>
type D = Readonly<Config>Types A, B, C and D are all uniform mappings of Config thanks to the built-in mapped types Partial, Omit, Pick and Readonly:
-
Ais computed as:{ address?: string | undefined, port?: number | undefined, logslevel?: 'none' | 'error' | 'warning' | 'info' | 'debug' | undefined }
-
Bis computed as:{ port: number }
-
Cis computed as:{ logslevel: 'none' | 'error' | 'warning' | 'info' | 'debug' | undefined }
-
Dis computed ad:{ readonly address: string, readonly port: number, readonly logslevel: 'none' | 'error' | 'warning' | 'info' | 'debug' }
There is a section in the official documentation about mapped types.
Conditional types were introduced in TypeScript v2.8. They can be used to make non-uniform mappings into other types, meaning they can be considered as functions for types, like mapped types but more powerful.
When TypeScript detects an error/warning in your program, its checker (an internal component of the language) generates a diagnostic message.
// { "strict": true }
const value: string = undefinedType 'undefined' is not assignable to type 'string'. (2322)The 2322 number is an ID that could be used to reference the type of diagnostic message when creating an issue for example. More specifically, the 2322 ID relates to the Type '{0}' is not assignable to type '{1}'. diagnostic. You can also check the wiki for more information about these messages.
Thanks for taking the time to put this together. +1