Typescript
TypeScript is like a highly-advanced linter, able to check documentation and warn when code is not being used as intended.
(taken from this blog article)
tsc (compiler) vs. IDE language service
How does it work?
- Typescript compiler compiles
.tsand.tsxfiles to.jsfiles +js.mapminified files. - Option
"declaration": true,: _ If exporting an own Typescript module, you can offer typescript linting support for it by generating a declaration file. With the help of that, typescript errors are displayed to developers who use functions/methods of it. _ Typescript compiler compiles.tsand.tsxfiles to.d.tsfiles (“Header” files - contains type definitions) +.jsfiles +js.mapminified files. - Typescript holds a recursive tree of type definitions.
tsc (compiler) vs. tslint (form checker)
tsc: Semantics (e.g. unused variables) _ fast _ guarantees to apply rules for ALL files and cases!tslint: Advanced semantics checks _ not as logically rigid astsc_ can selectively turn rules on/off on file/line lev
There’s an extra tool dts-bundle to create one d.ts. file out of all d.ts. files in the dist/ folder.
Type inference
- Each function expects a certain type.
-
Given an expression, if a type is not provided by the programmer, a type can be inferred from the context it is placed in - such an expression is then called a contextually typed expression.
If the contextually typed expression contains explicit type information, the contextual type is ignored.
If you explicitly set a type, you express that you know which type that function/value should expect.
When would you explicitly set a type? - If the contextually inferred type is not helpful/wrong, explicitly set a type.
-
A type cannot always be inferred. Example:
const foo = { a: 3, b: 'string', c: {} }; const bar = _.pick(foo, ['a', 'b']); bar.a; // typescript can't infer the types of `a` and `b`(
_.pickis from the lodash library)
More information can be found in the type inference documentation.
Optional types
interface PersonPartial {
name?: string;
age?: number;
}Type assertions (aka type casts)
Two different ways to cast:
myObject = <TypeA>otherObject; // using <>
// Recommended:
myObject = otherObject as TypeA; // using `as` keyword- Read about why you shouldn’t cast often.
Scenarios where you might want a type assertion
- Sometimes TS can’t automatically infer the correct type. In this case it’s ok to use a type assertion.
- Control flow analysis when assigning to a union type
let baz: string | number = "baz" // this might be a number sometime but for testing it's "baz"
if (typeof baz === "number") {
baz.toFixed(); // error?! toFixed() doesn't exist on type 'never'
}The type is never here because TypeScript narrowed the type to string in the line with the if statement.
With a type assertion there will be no error.
let qux = "qux" as string | number;
if (typeof qux === "number") {
qux.toFixed(); // okay
}TODO: Example
Assure that type is defined
data.name won’t be undefined and thus myName won’t.
const myName = data!.name!;Don’t use it! Eliminates type safety.
Pass on responsibility to type
By explicitly setting any I give away the responsibility to type to the stuff calling my function:
const foo = (bar: string, baz: string): any => ({
...
})Then I can set Function (i.e. make any more concrete):
const curriedFoo: Function = _.curry(foo('hello'));Modules (Import/Export)
Typescript has an extra export = syntax which is used to model the traditional CommonJS and AMD workflow.
Notes:
- AMD is used in
require.jsbecause of these reasons. - Read more about module patterns in Addy Osmani: JS Design Patterns.
Example:
This code is from a twig file layout.html.twig:
<script type="text/javascript">
require(['app/user'], function(user) {
app.init({}, function() {
{% if requireJsApp is defined and requireJsApp %}
app.load('{{ requireJsApp }}');
{% endif %}
app.finishLoading();
});
});The following quote is taken from an excerpt of the Typescript docs:
When importing a module using
export =, TypeScript-specificimport module = require("module")must be used to import the module. I can’t have adefault exportnext to aexport =!
I still don’t understand:
- Why
require.jsdecided to have this weird syntax?! - How
require.jsautomatically does some kind of async stuff? as mentioned in comments of the question here? - Why do I have to use
export =in order to use the aboverequire(['app/foo'], ...)syntax?
Comparison to flow
Comparison with Facebook Flow Type System
Mimic flow types - Allow other arbitrary params in object
To mimic the behavior of non-strict flow-type type definitions:
-
Here next to
barany other param is additionally allowed in the interface:interface Foo { bar: string; [key: string]: any; // a string index signature } -
Unions in index signatures
The
inkeyword is used in mapped type definitions as part of the syntax to iterate over all the items in a union of keys:type Foo = 'a' | 'b'; type Bar = { [key in Foo]: any }; // { a: any, b: any }This code will not do what you expect. It will allow anything in the object. It won’t restrict keys
type Foo = 'a' | 'b'; type Bar = { [key: Foo]: any };The following code causes this error
An index signature parameter type cannot be a union type. Consider using a mapped object type instead:type K = 'foo' | 'bar'; interface SomeType { [prop: K]: any; }
Indexer
The first line inside the interface is called the indexer:
interface Foo {
[key: string]: string | number; // indexer
bar: string;
}Tips/Workarounds
Casts with as any
-
Cast to any with
as anyExample
interface Props = { foo: number; bar: string; // no baz defined } workWithProps({ foo: 1, bar: 'hello' baz: 3, } as any)
How to deal with undefined
-
Attention:
age?: number;is not the same asage?: number | undefined;- Read more about it here where Exact Optional Property Types are discussed.
-
Work around
undefinedIn case you have a type of the sort
let foo: string[] | undefined;
or ```ts const bar: { foo?: string[]; }TypeScript will force you to deal with the possibility that
foomight not be defined, i.e. that it isundefined.Here are three ways of how you can deal with them:
const { foo } = bar // foo might be undefined // Alternative 1 - clean modern JS (Optional chaining) const myLength = foo?.length // Alternative 2 - clean way via type guard if (foo != undefined) { const myLength = foo.length; } // Alternative 3 - elaborate way via type cast (more dangerous) const myLength = (foo as string).length; // Alternative 4 - dirty way (via non-null assertion -> can't be null or undefined) const myLength = foo!.length;Discussion of all three alternatives
- Optional chaining
- Type Guard
- Type Cast
- Non-null assertion
Be aware of async wrappers like
setTimeout:They open a new scope.
if (foo != undefined) { const myLenght = foo.length; // works! setTimeout(() => { const myLength = foo.length; // foo is a let, so it could've changed in the meantime! }, 500); } -
Trick to refrain from using type definitions for a module (which e.g. has none)
You get the error that the module has no type definitions.
- Try to run
yarn add --dev @types/module-without-typings. Perhaps types for it already exist. - Declare
anymodule
declare module 'module-without-typings' { var noTypeInfoYet: any; export = noTypeInfoYet; }- Use
requireinstead of CommonJS way.
const ModuleWithoutTypings = require('module-without-typings');instead of
import * as ModuleWithoutTypings from 'module-without-typings'; - Try to run
-
Great for refactoring!
- Make a change to a variable/prop
- Press
F 8in VSCode to jump from TS error to error to fix them
Data Type: Record<K, T>
Definition: Construct an object type with keys of type K and values of type T
Record creates the type of an object where the entries of EntryName are possible keys and the values are of type number | null | undefined
export type EntryName = typeof entryNames[number]
export const entryNames = [
"connections"
"views",
"likes",
"favorites",
] as const
type Values = Record<EntryName, number | null | undefined>More complex example from react-navigation
This is the type definition of ParamListBase:
export declare type ParamListBase = Record<string, object | undefined>;objectcan be typed as{ [key: string]: any }- Hence, the
Recordis an object of objects orundefined, i.e. either
{ [key: string]: { [key: string]: any } }or
{ [key: string]: undefined }See https://stackoverflow.com/questions/51936369/what-is-the-record-type-in-typescript
TypeScript implements Duck Typing
- This blog article talks about TypeScript and Duck Typing.
- Duck Typing: Check whether it is a duck by checking whether it quacks like a duck and walks like a duck. In contrast to checking its DNA.
// types
interface IItem {
id: number;
title: string;
}
interface IProduct {
id: number;
title: string;
author: string;
}
// print function
function print(item: IItem) {
console.log(item.id + ' > ' + item.title);
}
// book
var book: IProduct = {
id: 1,
title: 'C# in Depth',
author: 'Jon Skeet',
};
print(book); // No type error since it'sConstrained generics
Here type P gets constraint to object which signifies a non-primitive type, i.e. not string, number, boolean, bigint, symbol, undefined or null.
<P extends object>Similar but different (see here for more infos):
<P extends {}>Unconstrained generics are given the implicit constraints of unknown, i.e.
<P extends unknown>Read more about it in this nice SO answer.
Interfaces as function types
This blog post covers the topic.
I encountered this type:
export interface CreateStyledComponent<
P extends {},
ComponentProps extends {} = {},
JSXProps extends {} = {},
StyleType extends NativeStyle = NativeStyle
> {
<AdditionalProps extends {} = {}>(
...styles: Interpolation<P & ComponentProps & AdditionalProps & { theme: Theme }, StyleType>[]
): StyledComponent<P & AdditionalProps, ComponentProps, JSXProps>;
<AdditionalProps extends {} = {}>(
template: TemplateStringsArray,
...styles: Interpolation<P & ComponentProps & AdditionalProps & { theme: Theme }, StyleType>[]
): StyledComponent<P & AdditionalProps, ComponentProps, JSXProps>;
}A whole lot is happening here!
- An interface which describes a generic type with 4 input parameters (types) and as return values functions.
- Function overloading
- Many constrained generics with default types
- Many type intersections
Other example from lodash
Its filter function heavily uses function overloading inside an interface which defines a function:
interface LoDashStatic {
/**
* Iterates over elements of collection, returning an array of all elements predicate returns truthy for. The
* predicate is invoked with three arguments: (value, index|key, collection).
*
* @param collection The collection to iterate over.
* @param predicate The function invoked per iteration.
* @return Returns the new filtered array.
*/
filter(collection: string | null | undefined, predicate?: StringIterator<boolean>): string[];
/**
* @see _.filter
*/
filter<T, S extends T>(collection: List<T> | null | undefined, predicate: ListIteratorTypeGuard<T, S>): S[];
/**
* @see _.filter
*/
filter<T>(collection: List<T> | null | undefined, predicate?: ListIterateeCustom<T, boolean>): T[];
/**
* @see _.filter
*/
filter<T extends object, S extends T[keyof T]>(collection: T | null | undefined, predicate: ObjectIteratorTypeGuard<T, S>): S[];
/**
* @see _.filter
*/
filter<T extends object>(collection: T | null | undefined, predicate?: ObjectIterateeCustom<T, boolean>): Array<T[keyof T]>;
}Utility types
- Utility types are built-in conditional types.
Examples from styled-components-react-native:
type AnyIfEmpty<T extends object> = keyof T extends never ? any : T;e.g.
export const ThemeProvider: ThemeProviderComponent<AnyIfEmpty<DefaultTheme>>;Omit
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;Pick
Implementation of Pick:
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};- The second argument of Pick has to be a union because
keyofcreates a union of keys as string literals.
Partial
Implementation of Partial:
type Partial<T> = {
[P in keyof T]?: T[P];
};Discriminated unions
Unions of objects that share a property of string literals
- This answer is worth reading in this regard.
- Also my answer about the fact that today a discriminant is not always necessary.
TODO: Solve with adapted solution from here: https://artsy.github.io/blog/2018/11/21/conditional-types-in-typescript/
isDirty: Union without discriminant
- Here
idjust has different types. No type literal is here which works as discriminant. - This works at least since TS 3.3.3
type LectureT = LectureIdT & {
title: string;
};
/**
* Lecture can only have isDirty field if id is of type number.
*/
type LectureIdT =
| { id: string; isDirty: boolean }
| { id: number; isDirty?: never };
const lectureMockWithNumberId: LectureT = {
id: 1,
title: 'Lecture 1',
// isDirty: false, // Uncomment to see error
};
const lectureMockWithStringId: LectureT = {
id: "1",
title: 'Lecture 2',
isDirty: false,
};Can be played around with in this TS Playground.
Lookup types
keyofto get akey.- And
T[keyof T]to get a value of objectT:
T[keyof T]Examples
Partial type implementation
The Partial utility type is implemented via a mapped type.
type Partial<T> = {
[P in keyof T]?: T[P];
};
// Usage
type PartialPerson = Partial<Person>;filter function:
filter<T extends object, S extends T[keyof T]>(collection: T | null | undefined, predicate: ObjectIteratorTypeGuard<T, S>): S[];Mapped Types
e.g.
type ContactDetails = { [K in "name" | "email"]: string };See for example this article about mapped types for more.
For objects
See Pick or Partial above in the utility type section
For unions
- See the section about mapped types in the old TS docs.
- Very interesting dive into TypeScript to explore the type system.
as in mapped types to redefine object
TODO
Mapped types on tuples and arrays
Mapping over a tuple is mapping over the tuple elements -> K is the numerical index of the tuple element.
type MapToPromise<T> = { [K in keyof T]: Promise<T[K]> };If T is a tuple, {[K in keyof T]: any} is a tuple of the same length as T:
Structural vs. Nominal typing (Typing theory)
- Structural: Has to be the same type - does not have to be the same instance
- Nominal: Has to be the same instance
Interesting TypeScript tricks
-
(string & {})To allow auto-completion of some strings but still allow all string values.
Example:
type Color = 'inherit' | 'primary' | 'secondary' | 'tertiary' | 'black' | 'white' | (string & {}); const color: Color = 'foo';See this TS Playground.
Here the given strings are theme colors and all other provided colors will be used as CSS color -> in a different manner.
Explanation: The
& {}is a technique to lower the inference priority. It means that the string becomes non-inferential.stringis now not used to infer the entire type of theColortype, but it’s part of the type. So we get both! The auto-completion because it’s a union type, but also it will allow all strings.See the lower part of this answer from jcalz for more infos.
-
Hints for the TypeScript compiler, e.g.
| [never]See the tuples file!
Template Literal Types
- This article introduces them nicely.
- TS 4.4 will introduce template literal types as dynamic object keys. See this SO reply for more.
infer
With infer you declare a new variable in the scope of a conditional type.
Why do we need infer?
You sometimes have to declare new variables there if it has no matching counterpart on the left-hand side of the type declaration.
inferis there to say you know you are declaring a new type (in the conditional type’s scope) - much like you have to writevar,letorconstto tell the compiler you know you’re declaring a new variable.
See this great example to explain infer from this answer in SO:
Example to show why we need infer:
type R = { a: number }
type MyType<T> = T extends infer R ? R : never; // infer new variable R from T
type MyType2<T> = T extends R ? R : never; // compare T with above type R
type MyType3<T> = T extends R2 ? R2 : never; // error, R2 undeclared
type T1 = MyType<{b: string}> // T1 is { b: string; }
type T2 = MyType2<{b: string}> // T2 is neverExplanation: type MyType2<T> = ... is a type alias declaration, in which generic type variable/parameter T cannot be resolved yet. Later on we instantiate T with {b: string} by using the type reference MyType2<{b: string}> inside the type alias declaration type T2 = ....
Now, the actual type can be resolved. As {b: string} (instance of T) is not assignable to R - which is {a: number} -, T2 resolves to never.
Example: Implementation of ReturnType utility type
// Own implementation of `ReturnType` (have to choose different name so that name doesn't clash)
type ReturnTypeNew<T> = T extends (...args: any[]) => infer R ? R : any;
type f1 = () => "hello"
type inter = { a: string, b: number } | boolean;
const foo: inter = { a: "one", b: 2 };See this TS Playground.
Type Unions and Intersections
Type Intersections
Implicit type intersections
- When overwriting fields via intersections the types of the fields intersect as well.
- Attention: Given primitive data types this can quickly yield unwanted
nevervalues.
See this TS Playground.
// Implicit type intersections
type A = { a: { c: number }, b: number };
type B = A & { a: { d: string } }; // { a: { c: number, d: string }, b: number }
const example1: B = { a: { c: 2, d: 'd' }, b: 3 };
// Primitive types: 'never' is easily produced via intersections of something impossible.
type C = { a: number, b: string };
type D = C & { a: string };
// Why never?
type CandD = number & string; // type `never` - can't be number and string at the same time.
const example2: D = { a: 'one', b: 'two' }; // Error: Type 'string' is not assignable to type 'never'.
// Solution: use Omit utility type
type E = Omit<C, 'a'> & { a: string };
const example3: E = { a: 'one', b: 'two' }; // Works fine!Discuss on Twitter ● Improve this article: Edit on GitHub