Typescript theory
Type hierarchy
Explained in this article about unknown and never:
- Top type
unknown - Bottom type
never - Top & Bottom type
any
T | never ⇒ T
T & unknown ⇒ Tneveris the identity with respect to unions (T | never ⇒ T)unknownis the identity with respect to intersections (T & unknown ⇒ T)
(but number & string is of type never)
Difference between any and unknown
unknownis I don’t know;anyis I don’t care
Taken from this StackOverFlow thread about the difference between any and unknown
TypeScript Type Variance
- TypeScript is covariant in shapes (example)
- TypeScript is structurally typed
interface Animal {
name: string;
}
// This works also if Bird does not extend Animal,
// but just gets defined as { name: string; wingCount: number; }
interface Bird extends Animal {
wingCount: number;
}
const myAnimal: Animal = { name: "snake" };
const myBird: Bird = { name: "pigeon", wingCount: 2 };
const getName = (obj: Animal) => obj.name;
// TypeScript is covariant in shapes
// 'Bird' allowed although 'Animal' required
getName(myBird);Function types and contravariance
function types are covariant in their return type but contravariant in their parameter types
interface Foo {
name: string;
}
interface Bar {
name: string;
displayName: string;
}
function getName(obj: Foo): '1' | '2' {
return "1";
}
const getName2: (obj: Bar) => string = getName;Here the parameter of getName2 is a subtype of the parameter of getName but the return type is a supertype of the return type of getName.
Corresponding TS Playground
Covariance & Contravariance (again)
In general
- TypeScript shapes are covariant in their property types.
- Every complex type is covariant in its members and function return types.
- Functions are covariant in their return types (the return type has to be a subtype of the other function’s return type)
-
Functions are contravariant in their parameter and
thistypes.- In other words: For a function to be a subtype of another function, each of its parameters and its
thistype must be a supertype of its corresponding parameter in the other function.
- In other words: For a function to be a subtype of another function, each of its parameters and its
const test = (arg: "a" | "b"): "a" | "b" | "c" => {
return "c";
};
const test2 = (arg: "a" | "b" | "c"): "a" | "b" | "c" | "d" => {
return "c";
};
const useTest = (arg: typeof test) => {
//
}
// Error: Since TS is contravariant on function parameter values but not on function return values this throws a compile error
useTest(test2);See this TS Playground
See also this discussion I had in StackOverFlow comments about Variance and Covariance
Assignability and Type Annotation
Assignability: TypeScript’s rules for whether or not you can use a type A where another type B is required.
If type A extends type B then type A is assignable to type B. You can say that
Example
type Book = { name: string; isbn: string; author?: string }
let Book: Book = { name: 'Test' }Examples:
"hello" extends stringstring extends number | string- nothing extends
never - everything (besides
never) extendsunknown
Type Assertion
If type B extends type A then type A is asserted to be type B
Top and bottom types
-
unknownis a top type - the root of the hierarchy.- Any value is assignable to something typed as
unknown(e.g.let a: unknown = 5;is possible, butlet a: unknown = 5; let b = 6; b = a;is not) (because everything inherits from it) - This e.g. works:
let Book: unknown = { name: 'Test' } - But elements with type
unknownare assignable to nothing else (because you inherit from nothing) - This doesn’t work:
let Sth: unknown; const New: Book = Sth;. You e.g. get “Type ‘unknown’ is not assignable to type ‘Book’” unknownis an empty type: It has no properties of its own.
- Any value is assignable to something typed as
-
neveris a bottom type- Nothing is assignable to elements with type
never - This e.g. doesn’t work:
const Book: never = { name: 'Test' };. You get “Type ‘string’ is not assignable to type ‘never’.” - But the
nevertype is assignable to everything.
- Nothing is assignable to elements with type
Usages of never
Be safe against future changes to types, e.g. change from
// former type
const stringOrNum = string | number = 'Hello world!';
// new type
const stringOrNum = string | number | boolean = 'Hello world!';Exhaustive check: TypeScript would throw a compile error.
if (typeof stringOrNum === 'string') {
// ...
} else if (typeof stringOrNum === 'number') {
// ...
} else {
const _exhaustiveCheck: never = stringOrNum
throw new Error(`Unknwon type ${_exhaustiveCheck}`)
}any
-
It’s both, a bottom and a top type.
- It’s assignable to everything
- Everything is assignable to it.
unkown vs. any
unknown is I don’t know; any is I don’t care.
Type Widening and Narrowing
In case of a type assertion, e.g. const B: foo as A it will be allowed if A extends B or if B extends A.
-
If
A extends Byou widenAtoBwhich is relatively safe and acts similarly to a type annotation.Example:
string extends number | string -
If
B extends Ayou narrow A to B which is unsafe.Example:
number | string extends string
Type widening
Type widening is the default behavior for many literal types. So e.g. a string foo becomes the string type.
- If you widen
AtoB,A extends Bholds. e.g. when doing a type assertionlet foo = bar as Awherebaris of a type which “extends” B, i.e. is a superset ofA.
Marius Schulz writes why non-widening literal types might be useful.
Links
- This article is based on this blog post.
Control flow analysis
Discuss on Twitter ● Improve this article: Edit on GitHub