This library is intended to serve as a cookbook to all manner of type based operations you would need in typescript.
This library contains some type definitions as well as some helper functions. The type definitions have no runtime performance since they are compiled out. The helper function may incur an infinitesimal amount of runtime performance but will also be compiled out in any production build.
Table of Contents
- How do I...
- Make all properties on an interface optional
- Make all properties on an interface required
- Make all properties on an interface nullable
- Make all properties on an interface readonly
- Create a tuple from a known set of elements
- Create a new type with some keys of another type
- Remove properties from an interface
- Get the Array type, Promise type, Observable type, ...etc from something
- Make a readonly object mutable
- Make some readonly keys on an object be mutable
- Create a sub interface explicitly using only some keys of the interface:
- Create a sub interface infering which keys of the interface to use:
- Create a deep readonly object
- Remove some types from an interface
- Create a new interface using some types of another interface
- Require one and only one property of an object exist
- Advanced Mapped Types Crash Course
Use the Partial
type.
interface Person {
name: string;
age: number;
}
type PersonWithAllPropertiesOptional = Partial<Person>;
const person1: PersonWithAllPropertiesOptional = {}; // OK
// OK
const person2: PersonWithAllPropertiesOptional = {
name: 'Me',
};
// OK
const person3: PersonWithAllPropertiesOptional = {
name: 'Me',
age: 123,
};
const person4: PersonWithAllPropertiesOptional = {
name: 'Me',
age: 123,
// Error: Object literal may only specify known properties, and 'foo' does not exist in type 'Partial<Person>'.
foo: 'bar',
};
Use the Required
type.
interface Person {
name: string;
age?: number;
}
const person: Person = { name: 'Alex' };
// Error: Property 'age' is optional in type 'Person' but required in type 'Required<Person>'.
const requiredPerson: Required<Person> = person;
Use the Nullable
type.
type Nullable<T> = { [P in keyof T]: T[P] | null };
interface Person {
name: string;
age: number;
}
// OK
const person: Nullable<Person> = {
name: null,
age: null,
};
// OK
person.name = 'Adam';
Use the Readonly
type.
interface Person {
name: string;
age?: number;
}
const person: Readonly<Person> = { name: 'Alex' };
// Error: Cannot assign to 'name' because it is a constant or a read-only property.
person.name = 'Bob';
Use the tuple
function of toopl
.
import { tuple } from 'toopl';
// Type: [number, string, boolean]
const myTuple = tuple(1, '2', true);
Use the Pick
type.
interface Person {
name: string;
age: number;
id: number;
}
const personForTest: Pick<Person, 'name'|'age'> = {
name: 'Charlie',
age: 123,
};
Use the OmitStrict
type from type-zoo
.
import { OmitStrict } from 'type-zoo';
interface Person {
name: string;
age: number;
id: number;
}
const person: OmitStrict<Person, 'age'|'id'> = { name: 'Danny' };
Note: Omit
from type-zoo
would also work but wouldn't check if the property actually exists on the interface.
Use the infer
keyword.
type ArrayUnpacker<T> = T extends Array<infer U> ? U : never;
const stringArray = ['this', 'is', 'cool'];
// Type: string
let unpackedStringArray: ArrayUnpacker<typeof stringArray>;
type PromiseUnpacker<T> = T extends Promise<infer U> ? U : never;
const stringPromise = Promise.resolve('test');
// Type: string
let unpackedStringPromise: PromiseUnpacker<typeof stringPromise>;
class Box<T> {
constructor(private readonly value: T) {}
}
type BoxUnpacker<T> = T extends Box<infer U> ? U : never;
const myBox = new Box('a string box!');
// Type: string
let myUnpackedBox: BoxUnpacker<typeof myBox>;
Use the Mutable
type from [ts-cookbook
].
import { Mutable } from 'ts-cookbook';
interface ImmutablePerson {
readonly name: string;
readonly age: number;
}
const immutablePerson: ImmutablePerson = {
name: 'Danny',
age: 50,
};
// Error: Cannot assign to 'age' because it is a read-only property.
immutablePerson.age = 51;
const person: Mutable<ImmutablePerson> = {
name: 'Eric',
age: 34,
};
// OK
person.age = 35;
Use the MutableKeys
type from ts-cookbook
.
import { MutableKeys } from 'ts-cookbook';
interface ImmutablePerson {
readonly name: string;
readonly age: number;
readonly isPremium: boolean;
}
const immutablePerson: ImmutablePerson = {
name: 'Danny',
age: 50,
isPremium: false,
};
// Error: Cannot assign to 'age' because it is a read-only property.
immutablePerson.age = 51;
const person: MutableKeys<ImmutablePerson, 'age'|'isPremium'> = {
name: 'Eric',
age: 34,
isPremium: false,
};
// OK
person.age = 35;
person.isPremium = true;
// Error: Cannot assign to 'name' because it is a read-only property.
immutablePerson.name = 'Erik';
Use the Use the Pick
type.
interface Person {
name: string;
age: number;
id: string;
}
type PersonWithNameAndAge = Pick<Person, 'name'|'age'>;
const person: PersonWithNameAndAge = { name: 'Greg', age: 23 };
Use the inferPick
function of ts-cookbook
.
import { inferPick } from 'ts-cookbook';
interface Person {
name: string;
age: number;
id: string;
}
const person = inferPick<Person>()({ name: 'Greg', age: 23 });
Use the Readonly
type or readonly
function from ts-cookbook
. Note: for shallow objects you can use the built in Typescript Readonly
type. If you want to ensure that it will work if the object is a Map
, Set
, or Array
, use the ShallowReadonly
type (or shallowReadonly
function) from ts-cookbook
.
import { Readonly, readonly, shallowReadonly } from 'ts-cookbook';
const array = readonly([1, 2, 3]);
// Error: Property 'push' does not exist on type 'ReadonlyArray<number>'.
array.push(4);
class Person {
constructor(public name: string, public age: number) {}
}
// `person` is Readonly<Person>
const person = readonly(new Person('Harry', 42));
// Error: Cannot assign to 'name' because it is a read-only property
person.name = 'Harr';
const person2: Readonly<Person> = new Person('Kevin', 43);
// Error: Cannot assign to 'name' because it is a read-only property
person.name += '!';
// `map` is a ReadonlyMap<string, string>
const map = readonly(new Map([['foo', 'bar']]));
// Error: Property 'set' does not exist on type 'ReadonlyMap<string, string>'.
map.set('baz', 'bork');
// `myObj` is Readonly<{cool: string}>
const myObj = readonly({ cool: 'thing' });
// Note: `readonly` creates a deep readonly object, as opposed to the native
// Typescript `Readonly` type which only creates a shallow readonly
// object. You can still get the inferred readonly behavior in a shallow
// fashion by using the `shallowReadonly` function from `ts-cookbook`.
const myObj2 = readonly({
o: {
prop: 1,
},
map: new Map([['foo', 'bar']]),
a: [1, 2, 3],
});
// Error: Cannot assign to 'prop' because it is a read-only property.
myObj2.o.prop = 2;
// Error: Property 'set' does not exist on type 'DeepReadonlyMap<string, string>'.
myObj2.map.set('boo', 'zaf');
// Error: Property 'push' does not exist on type 'DeepReadonlyArray<number>'.
myObj2.a.push(4);
Use the RemoveType
function from ts-cookbook
.
import { RemoveType } from 'ts-cookbook';
interface Person {
name: string;
age: number;
isSaved: boolean;
save: () => void;
}
const personForTest: RemoveType<Person, boolean|Function> = {
name: 'Joe',
age: 44,
};
Use the KeepType
function from ts-cookbook
.
import { KeepType } from 'ts-cookbook';
interface Person {
name: string;
age: number;
isSaved: boolean;
save: () => void;
}
const personForTest: KeepType<Person, string|number> = {
name: 'Joe',
age: 44,
};
Use the OneOf
function from ts-cookbook
.
import { OneOf } from 'ts-cookbook';
interface UnsavedRecord {
name: string;
age: number;
}
type DbRecord = UnsavedRecord &
OneOf<{
draftId: string;
dbId: string;
}>;
const record: DbRecord = {} as any;
if (record.dbId) {
record.draftId; // draftId is typed as `draftId?: undefined`.
}
The mapped syntax type is somewhat cryptic, here's the general idea of how it breaks down.
First we have a simple key type:
// This is a simple object type
type MyObject = { ['myCoolKey']: string };
let obj: MyObject = { myCoolKey: 'test' };
obj.myCoolKey; // OK
Typescript allows use of a union type (eg 'foo'|'bar'
) with the in
keyword:
type MyObject = { [K in ('foo'|'bar')]: string };
let obj: MyObject = { foo: 'foo', bar: 'BAR' };
obj.foo; // OK
obj.bar; // OK
Another way of getting a union type is by using the keyof
keyword:
interface Point { x: number; y: number; }
type PointKeys = keyof Point; // same as `type PointKeys = 'x'|'y'`
Using that knowledge, we can create a mapped type as follows:
interface Person {
name: string;
age: number;
}
// Create a readonly person by adding the readonly modifier to the key.
type ReadonlyPerson = { readonly [K in keyof Person]: Person[K] };
The type doesn't need to be tied to an existing type (like Person
in the example above) but can be generic as well:
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
See the official Typescript handbook for more details.