When types are shared between modules and used sparsely, create a src/types.ts
file
as per TypeScript's conventions, where you will pile up all the common types.
Presumably you will also build a declaration at lib/types.d.ts
.
If your repository hosts a library, then it will be useful to have those types easily imported into parent modules.
Consider adding types.d.ts
in the root of your repository with the content export * from './lib/types.d.ts';
,
since this will make parent modules be able to write import {FunkyType} from 'funky-module/types';
.
At times, you may need to clarify to typescript that a variable is certainly of a specific type within a block.
let fn = function(a: boolean | string): boolean {
if (typeof (a as string).match === 'function') {
a = a as string;
return a.match(/^true$/) === null;
}
return a;
};
Similarly, you may use:
- the
in
operator e.g.if ('match' in a)
- the
typeof
guard e.g.if (typeof a === 'string')
- the
instanceof
guard e.g.let a = new String('foo'); if (a instanceof String
if ((a as any).match) { a = a as string; ...
if ((a as any).match) { a = a as unknown as string; ...
(in extreme cases)
Read more about this. Read event more about this.
At times, you may hit issues because of type mismatching.
If you are certain that the implementation is correct (but maybe the typing is only partially correct),
rather than falling back to // @ts-ignore
, use a type assertion to unknown
and then to the intended type.
If you've never heard of the type unknown
, see the section below.
type Person = {
name: string;
gender: string;
};
let fn = function(): Person {
// Partial<Person> is a Person type with all properties optional
let p = {} as Partial<Person>;
p.name = 'Luiza';
p.gender = 'unspecified';
// will complain that Partial<Person> does not match Person
// return p;
// since you are certain that p will actually have all the required properties, you can
return p as unknown as Person;
};
Generics are good for mapping types
e.g. type MaybePromise<T> = T | Promise<T>
allows for let a: MaybePromise<void>
, let b: MaybePromise<number>
, etc.
But they are also good for creating type inferences in function signatures e.g.
let identity = function<T>(a: T): T {
return a;
};
// PS: Naïve example. TypeScript would have perfectly inferred the type, yes.
Use unknown
as much as possible, and not any
.
unknown
is the perfect default type, when you cannot proxy types
Whenever an unknown
variable will be used (e.g. access a property) or passed onto a function with a type constraint,
typescript will force you to cast it, and thus keep your code safely typed. any
on the otherhand would allow anything.
Read more about the unknown type.
Newer versions of TypeScript have opened up the possibility for so-called utility types e.g.
Partial<T>
would be a type for a partial object of type TReturnType<T>
would be the type of the return value of a function of type T- see more types at
- official list but not comprehensive: https://www.typescriptlang.org/docs/handbook/utility-types.html
- https://codewithstyle.info/Comprehensive-list-of-useful-built-in-types-in-TypeScript/
Similarly to the built-in types, more utility-types exist as external packages:
- our own collection: https://github.com/ysoftwareab/lodash-y/blob/master/src/types.ts
- https://github.com/piotrwitek/utility-types
- https://github.com/pirix-gh/ts-toolbelt
- comprehensive but can easily throw TypeScript errors because of high complexity
Read more about utility types here..
Sometimes external modules have missing/incorrect types, and you need to amend their interfaces/classes e.g.
interface ClockInterface {
currentTime: Date;
}
class Clock implements ClockInterface {
currentTime: Date = new Date();
constructor(h: number, m: number) { ... }
}
// We amend prototype just as a way to escape typescript type inference.
// In a real-life scenario, the Clock class might be defined in a JavaScript project
// with manually written (and thus incorrect) declaration files.
Clock.prototype.currentHour = function() {
return this.h;
}
let c = new Clock(10, 10);
// c.currentHour() will NOT be allowed
interface ClockInterface {
currentHour(): number;
}
// c.currentHour() will be allowed
It is worth noting that classes translate to interfaces, therefore this is also perfectly correct as well:
let c = new Clock(10, 10);
// c.currentHour() will NOT be allowed
interface Clock { // amending directly the Clock class (interface)
currentHour(): number;
}
// c.currentHour() will be allowed
Similarly, if the Clock class is in a external module, one can amend the interfaces by wrapping the typing
in a declare module 'package-name/or/path/to/module' { ... }
block e.g.
// ./clock.ts
export class Clock {...}
// ./index.ts
import {Clock} from './clock';
declare module './clock' {
interface Clock {
currentHour(): number;
}
}
let c = new Clock(10, 10);
// c.currentHour() will be allowed
Historically type
was inferior to interface
, but that is not the case today. Differences still exist though.
The simple recommendation is to use interface
, unless you cannot and you need type
.
A clear-cut comparison, from https://pawelgrzybek.com/typescript-interface-vs-type/, is that:
interface
cannot type a primitiveinterface
declarations can be merged (amended), buttype
doesn't allow thattype
declarations can use computed propertiestype
declarations are resolved eagerly
type
is for type aliases,
either trivial ones like type Seconds = number;
which add visual semantics to the type,
or advanced ones to increase readability e.g. in a function signature.
Type aliases will never transcend to the compiler, they act like inline replacements.
So let s: Seconds;
will be treated simply as let s: number;
, and the compiler (and intellisense) will show you s: number
.
Advanced types will be a mix of:
- intersection types e.g.
{a: boolean} & {b: boolean}
means that all the type constraints need to be fulfilled - union types e.g.
{a: boolean} | {b: boolean}
means that only one of the type constraints needs to be fulfilled - conditional types e.g.
T extends U ? X : Y
means that type is X or Y depending on whether T extends U - mapped types (with generics) e.g.
NonNullable<T> = T extends null | undefined ? never : T
means the type is T, but not null or undefined.
Read more about advanced types.
Both types and interfaces can be
for object types .e.g.
interface SomeObject {
someProp: boolean;
}
or tuple types .e.g.
interface SomeTuple {
0: string;
1: boolean;
}
or function types e.g.
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(source, subString) { // types will be inferred
let result = source.search(subString);
return result > -1;
}
or overloaded function types e.g.
interface SearchFunc {
(source: string, subString: string): boolean;
(source: object, subString: string): boolean;
}
or constructor types e.g.
interface SearchConstructor {
new (source: string, subString: string);
}
class Search implements SearchConstructor {
constructor(source: string, subString: string) { ... }
}
or for indexable types (arrays, objects, etc) e.g.
interface StringArray {
[index: number]: string;
}
or for class types e.g.
interface ClockInterface {
currentTime: Date;
}
class Clock implements ClockInterface {
currentTime: Date = new Date();
constructor(h: number, m: number) { ... }
}
or for hybrid types e.g.
interface SearchFunc {
(source: string, subString: string): boolean;
cache: Map;
}
See https://michalzalecki.com/nominal-typing-in-typescript/ .
NOTE Built-in nominal types might come under the unique
keyword.
See microsoft/TypeScript#33038 .
When you need multiple lines to define an intersection or a union type,
consider a leading &
or |
, instead of a trailing one e.g.
// better
type FooBar =
& Foo
& Bar;
// instead of
type FooBar = Foo
& Bar;
// or
type FooBar =
Foo
& Bar;
NOTE Multiline generics should be indented, but an eslint bug prevents this.
// intended
type SomeFunction =
Fn<
Result,
Args
>;
// but due to the bug
type SomeFunction =
Fn<
Result,
Args
>;
- https://mariusschulz.com/blog/series/typescript-evolution
- https://www.typescriptlang.org/docs/handbook/basic-types.html
- https://microsoft.github.io/TypeScript-New-Handbook/outline/
- https://basarat.gitbook.io/typescript/
- https://github.com/David-Else/modern-typescript-with-examples-cheat-sheet
- https://2ality.com/2020/02/types-for-classes-typescript.html