Mapped types
A mapped type builds an object type by looping over a set of keys. One key per turn, one property written per turn, and an expression that decides what the property's type is. Nearly every utility type you already use is one of these: Record, Partial, Required, Readonly, Pick, Omit. This entry writes each of them out, because the shortest way to trust a utility type is to see it in four characters of syntax.
Create programs/mapped-types.test.ts for this reference and keep it open, starting with the imports:
import { assertEquals, assertStrictEquals } from "@std/assert";
import { assertType, type IsExact } from "@std/testing/types";
Below the imports, add the types, the class, and the function the whole page shares; each earns its explanation in its step, and prefix earns a refusal first.
type Article = {
title: string;
tags: Array<string>;
score: number;
};
type Field<Value> = { value: Value; dirty: boolean };
type Fields<T> = { [Key in keyof T]: Field<T[Key]> };
class Car {
readonly year = 2020;
owner = "nobody";
get maker(): string {
return "Saab";
}
}
type Person = { name: undefined | string; age?: number };
type Prefixed<T> = {
[Key in keyof T & string as `$${Key}`]: T[Key];
};
function prefix<T extends object>(source: T): Prefixed<T> {
return Object.fromEntries(
Object.entries(source).map(([key, value]) => [`$${key}`, value]),
// deno-lint-ignore no-explicit-any
) as any;
}
Follow the page as you add and revise the runnable examples below it.
one property written per turn
Deno.test("one property written per turn", () => {
const draft: Fields<Article> = {
title: { value: "Mapped types", dirty: false },
tags: { value: ["typescript"], dirty: true },
score: { value: 3, dirty: false },
};
assertType<
IsExact<Fields<Article>["score"], { value: number; dirty: boolean }>
>(true);
assertStrictEquals(draft.title.value, "Mapped types");
assertEquals(Object.keys(draft), ["title", "tags", "score"]);
});
Check programs/mapped-types.test.ts
running 1 test from ./programs/mapped-types.test.ts
one property written per turn ... ok (315µs)
ok | 1 passed | 0 failed (1ms)
Key in keyof T is the loop. Field<T[Key]> is the body, and it can use Key, which is what makes this more than a rename: every property of Article comes out wrapped, with its own type intact, which the IsExact on score pins for one of them.
any union of keys, and Record written out
Deno.test("any union of keys, and Record written out", () => {
type Switches = { [Key in "debug" | "verbose"]: boolean };
type Counts = { [Key in string]: number };
type MyRecord<K extends PropertyKey, T> = { [P in K]: T };
assertType<IsExact<Switches, { debug: boolean; verbose: boolean }>>(true);
assertType<IsExact<Counts, { [key: string]: number }>>(true);
assertType<IsExact<Switches, MyRecord<"debug" | "verbose", boolean>>>(true);
assertType<IsExact<Switches, Record<"debug" | "verbose", boolean>>>(true);
assertType<IsExact<Counts, Record<string, number>>>(true);
const switches: Switches = { debug: true, verbose: false };
assertEquals(Object.keys(switches), ["debug", "verbose"]);
});
any union of keys, and Record written out ... ok (32µs)
The set of keys does not have to come from keyof; it can be any union of keys, and whether it is finite decides what you get back. A finite union gives you ordinary properties. An infinite one, such as string, gives you an index signature, because there is no other way to write down a type with unboundedly many properties. MyRecord is Record as the standard library defines it, one mapped type over the key you pass, and the assertions say the reproduction is the same type rather than a similar one. This is the paragraph the object types page points at when it says a Record and an index signature have different keyofs, and now the reason is visible: they are different only when the key set is finite, the fact the keyof and indexed access page measured from the other side.
an index signature is not a mapped type
The two look alike and they are not interchangeable. Write the finite key where the infinite one belongs:
Deno.test("an index signature is not a mapped type", () => {
type Loose = { [key: string]: number };
type Finite = { [key: "debug"]: boolean };
const counts: Loose = { debug: 1 };
assertStrictEquals(counts.debug, 1);
});
Check programs/mapped-types.test.ts
TS1337 [ERROR]: An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.
type Finite = { [key: "debug"]: boolean };
~~~
at file:///programs/mapped-types.test.ts:69:20
error: Type checking failed.
The key in an index signature is a label with no meaning: you cannot use it on the right of the colon, and the type after it must be one of the infinite key types. The Key in a mapped type is a real type variable that the body can use. That is the difference, and the compiler names the replacement for you when you reach for the wrong one. Shield the refusal:
Deno.test("an index signature is not a mapped type", () => {
type Loose = { [key: string]: number };
// @ts-expect-error: An index signature parameter type cannot be a literal type or generic type.
type Finite = { [key: "debug"]: boolean };
const counts: Loose = { debug: 1 };
assertStrictEquals(counts.debug, 1);
});
an index signature is not a mapped type ... ok (13µs)
mapping preserves the kind of type it was given
Deno.test("mapping preserves the kind of type it was given", async () => {
type Wrap<T> = { [Key in keyof T]: Promise<T[Key]> };
assertType<IsExact<Wrap<{ a: 1 }>, { a: Promise<1> }>>(true);
assertType<
IsExact<Wrap<{ readonly a: 1 }>, { readonly a: Promise<1> }>
>(true);
assertType<IsExact<Wrap<Array<string>>, Array<Promise<string>>>>(true);
assertType<
IsExact<Wrap<ReadonlyArray<string>>, ReadonlyArray<Promise<string>>>
>(true);
assertType<
IsExact<Wrap<readonly ["a", "b"]>, readonly [Promise<"a">, Promise<"b">]>
>(true);
assertType<
IsExact<Wrap<[x: 1, y: 2]>, [x: Promise<1>, y: Promise<2>]>
>(true);
const wrapped: Wrap<{ a: 1 }> = { a: Promise.resolve(1) };
assertStrictEquals(await wrapped.a, 1);
});
mapping preserves the kind of type it was given ... ok (18µs)
Object in, object out. Array in, array out. Tuple in, tuple out, with the same length and the same element labels, and readonly survives in all three shapes. The array and tuple cases are the surprising ones, because keyof of a tuple includes "length", "push", and every other method, as the keyof and indexed access page shows. A plain mapped type quietly skips all of that and visits only the indices. That is not a rule you could have guessed, and it is why Promise.all can be typed at all; the tuple types page takes that apart.
as renames the key it is about to write
Deno.test("as renames the key it is about to write", () => {
type Getters<T> = {
[Key in keyof T & string as `get${Capitalize<Key>}`]: () => T[Key];
};
const readers: Getters<Article> = {
getTitle: () => "Mapped types",
getTags: () => ["typescript"],
getScore: () => 3,
};
assertStrictEquals(readers.getTitle(), "Mapped types");
assertEquals(readers.getTags(), ["typescript"]);
assertStrictEquals(readers.getScore(), 3);
});
as renames the key it is about to write ... ok (41µs)
as takes the key the loop is on and computes the key to write instead. The computation here is a template literal type, which is the usual partner for this and gets its own entry on the template literal types page. The & string is not decoration: a key may be a symbol, and a symbol cannot go inside a template, so it restricts the loop to the string keys.
as costs you tuple-ness
Renaming has one consequence worth knowing before you meet it by accident: with as in the mapped type, the result is always an object type, so the preservation from the previous step stops. The type below was meant to be a filter over a tuple. Predict that the run-time filter it imitates behaves the same way, leaving "a" at index 1:
Deno.test("as costs you tuple-ness", () => {
type Trimmed<T extends Array<string>> = {
[Key in keyof T as T[Key] extends "" ? never : Key]: T[Key];
};
type Gapped = Trimmed<["", "a", "", "b"]>;
assertType<IsExact<Gapped[1], "a">>(true);
assertType<IsExact<Gapped[3], "b">>(true);
assertType<IsExact<Gapped["length"], 4>>(true);
assertType<
IsExact<Pick<Gapped, 1 | 3 | "length">, { 1: "a"; 3: "b"; length: 4 }>
>(true);
// The run-time operation it was imitating, which does close the gaps.
const filtered = ["", "a", "", "b"].filter((item) => item !== "");
assertStrictEquals(filtered[1], "a");
});
Check programs/mapped-types.test.ts
running 6 tests from ./programs/mapped-types.test.ts
...
as costs you tuple-ness ... FAILED (8ms)
ERRORS
as costs you tuple-ness => ./programs/mapped-types.test.ts:114:6
error: AssertionError: Values are not strictly equal.
[Diff] Actual / Expected
- b
+ a
FAILURES
as costs you tuple-ness => ./programs/mapped-types.test.ts:114:6
FAILED | 5 passed | 1 failed (9ms)
error: Test failed
filter closes the gaps, so "b" is at index 1, and the mapped type is the one that does not: the two empty strings are gone, the properties 0 and 2 are gone with them, and the remaining properties kept the indices they had, 1 and 3, with length still 4. A mapped type visits keys and has no notion of shifting anything along. Correct the prediction:
Deno.test("as costs you tuple-ness", () => {
type Trimmed<T extends Array<string>> = {
[Key in keyof T as T[Key] extends "" ? never : Key]: T[Key];
};
type Gapped = Trimmed<["", "a", "", "b"]>;
assertType<IsExact<Gapped[1], "a">>(true);
assertType<IsExact<Gapped[3], "b">>(true);
assertType<IsExact<Gapped["length"], 4>>(true);
assertType<
IsExact<Pick<Gapped, 1 | 3 | "length">, { 1: "a"; 3: "b"; length: 4 }>
>(true);
// The run-time operation it was imitating, which does close the gaps.
const filtered = ["", "a", "", "b"].filter((item) => item !== "");
assertEquals(filtered, ["a", "b"]);
assertStrictEquals(filtered[1], "b");
});
as costs you tuple-ness ... ok (60µs)
Filtering a tuple needs recursion, and that is the tuple types page again.
two ways to drop a property
Deno.test("two ways to drop a property", () => {
type StringValued<T> = {
[Key in keyof T as T[Key] extends string ? Key : never]: T[Key];
};
type StringKeys<T> = {
[Key in keyof T]: T[Key] extends string ? Key : never;
}[keyof T];
type AlsoStringValued<T> = { [Key in StringKeys<T>]: T[Key] };
assertType<IsExact<StringValued<Article>, { title: string }>>(true);
assertType<IsExact<StringKeys<Article>, "title">>(true);
assertType<IsExact<AlsoStringValued<Article>, { title: string }>>(true);
const stringValued: StringValued<Article> = { title: "Mapped types" };
assertEquals(Object.keys(stringValued), ["title"]);
});
two ways to drop a property ... ok (34µs)
Filtering is a side effect of renaming: rename a key to never and the property is not written. That is the first form, and it is the one to reach for. The second form is what everybody wrote before as existed, and it is worth being able to read: map every key to either itself or never, then index the result with keyof T to collect the values, which drops the nevers because they contribute nothing to a union, the same fact the conditional types page builds its filters on. Two steps and a much less obvious one, but it has an advantage: StringKeys<T> is a union you can name and reuse, while the as version computes the same thing invisibly.
the four modifiers, and the four utility types they are
Deno.test("the four modifiers, and the four utility types they are", () => {
type Optional<T> = { [Key in keyof T]+?: T[Key] };
type Demanded<T> = { [Key in keyof T]-?: T[Key] };
type Frozen<T> = { +readonly [Key in keyof T]: T[Key] };
type Thawed<T> = { -readonly [Key in keyof T]: T[Key] };
assertType<IsExact<Optional<Article>, Partial<Article>>>(true);
assertType<IsExact<Demanded<Partial<Article>>, Required<Partial<Article>>>>(
true,
);
assertType<IsExact<Frozen<Article>, Readonly<Article>>>(true);
assertType<IsExact<Thawed<Readonly<Article>>, Article>>(true);
// Making a property optional also adds `undefined` to its type.
assertType<
IsExact<Optional<{ score: number }>, { score?: number | undefined }>
>(true);
const sparse: Optional<Article> = { title: "x", score: undefined };
assertEquals(Object.keys(sparse), ["title", "score"]);
});
the four modifiers, and the four utility types they are ... ok (21µs)
A mapped type can add or remove the two property modifiers: +? makes every property optional and -? makes it required, +readonly and -readonly do the same for read-only. The + is optional and worth writing, because ? on its own looks like part of the property and +? looks like an instruction. Three of the four are built in as Partial<T>, Required<T>, and Readonly<T>. The fourth is not: there is no built-in that removes readonly, so Thawed above is the whole implementation and you will end up writing it, and the read-only page is the entry for what readonly does and does not protect. The last assertion is the detail that catches people: Partial<T> does not only mark properties optional, it widens each type with undefined, so the sparse value with an explicit score: undefined is accepted. Whether those two should be the same thing is what exactOptionalPropertyTypes is about, which the object types page covers, and Deno leaves the flag off, so they are the same thing here.
Pick and Omit are a mapped type and a filter
Deno.test("Pick and Omit are a mapped type and a filter", () => {
type MyPick<T, K extends keyof T> = { [P in K]: T[P] };
type MyOmit<T, K extends PropertyKey> = Pick<T, Exclude<keyof T, K>>;
assertType<
IsExact<
MyPick<Article, "title" | "score">,
Pick<Article, "title" | "score">
>
>(true);
assertType<IsExact<MyOmit<Article, "tags">, Omit<Article, "tags">>>(true);
// @ts-expect-error: Type '"tittle"' does not satisfy the constraint 'keyof Article'.
type Refused = MyPick<Article, "tittle">;
assertType<IsExact<Omit<Article, "tittle">, Article>>(true);
type LoosePick<T, K extends PropertyKey> = { [P in K & keyof T]: T[P] };
assertType<
IsExact<LoosePick<Article, "title" | "tittle">, { title: string }>
>(true);
const picked: MyPick<Article, "title"> = { title: "Mapped types" };
assertEquals(Object.keys(picked), ["title"]);
});
Pick and Omit are a mapped type and a filter ... ok (21µs)
Pick is a mapped type over a subset of the keys, which is why the loop is P in K rather than P in keyof T, and Omit is Pick with the keys filtered first, the filter being the Exclude the conditional types page derives. Their constraints differ, and the asymmetry is deliberate. Pick insists that K extends keyof T, so the shielded typo is TS2344 at the point of use; Omit takes any PropertyKey, so omitting a key that was never there is silently fine, which the IsExact right below the shield proves by showing Omit<Article, "tittle"> is just Article. That is the right call for Omit, which is usually removing something from a type it does not fully control, and it does mean a typo in an Omit does nothing at all rather than complaining. If you want the loose behaviour from Pick, the mapped type is one intersection away: LoosePick accepts any K and quietly keeps the keys that exist, which matters most when the key type is computed from a template rather than written out.
readonly is invisible to assignability
Deno.test("readonly is invisible to assignability", () => {
type MutuallyAssignable<A, B> = [A] extends [B] ? [B] extends [A] ? true
: false
: false;
assertType<
IsExact<MutuallyAssignable<{ readonly a: 1 }, { a: 1 }>, true>
>(true);
const mutable: { a: 1 } = { a: 1 };
const frozen: { readonly a: 1 } = mutable;
const thawedAgain: { a: 1 } = frozen;
type IsReadonly<T, K extends keyof T> = IsExact<
Pick<T, K>,
Readonly<Pick<T, K>>
>;
assertType<IsExact<IsReadonly<Car, "year">, true>>(true);
assertType<IsExact<IsReadonly<Car, "maker">, true>>(true);
assertType<IsExact<IsReadonly<Car, "owner">, false>>(true);
assertStrictEquals(thawedAgain, mutable);
assertStrictEquals(new Car().maker, "Saab");
});
readonly is invisible to assignability ... ok (24µs)
The three assignments in the middle all compile, in both directions, which is the fact the assertion above them states: readonly does not affect assignability, so you can launder a read-only type back into a mutable one by assigning it to a variable, with no cast and no warning. That makes the modifier undetectable by any extends test, so a type that asks "is this property read-only" has to compare two types for identity rather than compatibility: pick one property out, make a readonly copy of it, and ask whether the copy is the same type, because if making it read-only changed nothing, it already was. The identity check itself is the one thing here a conditional type cannot do, and the testing types page is where the trick behind IsExact gets written out. Car has three properties and one of them is a getter, which comes back true: a getter with no setter is read-only whether or not you wrote the word, worth knowing if you are generating one of these from a class.
optional is detectable, and easier
An object with nothing in it is assignable to a type whose every property is optional, and to nothing else, so Pick the property out and ask whether the empty object fits. Spell the empty object {} and the linter objects:
Deno.test("optional is detectable, and easier", () => {
type IsOptional<T, K extends keyof T> = {} extends Pick<T, K> ? true
: false;
assertType<IsExact<IsOptional<Person, "name">, false>>(true);
assertType<IsExact<IsOptional<Person, "age">, true>>(true);
assertType<IsExact<Pick<Person, "name">, { name: undefined | string }>>(
true,
);
assertType<IsExact<Pick<Person, "age">, { age?: number | undefined }>>(true);
// deno-lint-ignore ban-types
type AnythingButNullish = number extends {} ? true : false;
type NotReally = number extends Record<PropertyKey, never> ? true : false;
assertType<IsExact<AnythingButNullish, true>>(true);
assertType<IsExact<NotReally, false>>(true);
const person: Person = { name: undefined };
assertEquals(Object.keys(person), ["name"]);
});
error[ban-types]: `{}` doesn't mean an empty object, but means any types other than `null` and `undefined`
--> /programs/mapped-types.test.ts:227:43
|
227 | type IsOptional<T, K extends keyof T> = {} extends Pick<T, K> ? true
| ^^
= hint: If you want a type that means "empty object", use `Record<PropertyKey, never>` instead
docs: https://docs.deno.com/lint/rules/ban-types
Found 1 problem
Checked 1 file
The rule is right for this position, and the hint is exactly the replacement the final version uses, but the swap deserves one sentence of care, which the two extra assertions pin: on the left of extends the two behave identically, and on the right they do not. number extends {} is true, because {} means "anything except null and undefined", while number extends Record<PropertyKey, never> is false. Take the hint, and keep the one visible ignore where {} is the thing being measured:
Deno.test("optional is detectable, and easier", () => {
type IsOptional<T, K extends keyof T> = Record<PropertyKey, never> extends
Pick<T, K> ? true : false;
assertType<IsExact<IsOptional<Person, "name">, false>>(true);
assertType<IsExact<IsOptional<Person, "age">, true>>(true);
assertType<IsExact<Pick<Person, "name">, { name: undefined | string }>>(
true,
);
assertType<IsExact<Pick<Person, "age">, { age?: number | undefined }>>(true);
// deno-lint-ignore ban-types
type AnythingButNullish = number extends {} ? true : false;
type NotReally = number extends Record<PropertyKey, never> ? true : false;
assertType<IsExact<AnythingButNullish, true>>(true);
assertType<IsExact<NotReally, false>>(true);
const person: Person = { name: undefined };
assertEquals(Object.keys(person), ["name"]);
});
optional is detectable, and easier ... ok (39µs)
Person is built to separate the two things people confuse: name: undefined | string must be provided and may be undefined, while age? may be left out. The first two assertions say the check tells them apart, and the two Picks show why: only the optional one has a ? in its picked type.
a computed return type will not accept what you computed
Write the honest body for the module-scope prefix and the checker refuses it:
function prefix<T extends object>(source: T): Prefixed<T> {
return Object.fromEntries(
Object.entries(source).map(([key, value]) => [`$${key}`, value]),
);
}
Check programs/mapped-types.test.ts
TS2322 [ERROR]: Type '{ [k: string]: any; }' is not assignable to type 'Prefixed<T>'.
return Object.fromEntries(
~~~~~~
at file:///programs/mapped-types.test.ts:29:3
error: Type checking failed.
The type works, and the body does not type-check, because Object.fromEntries returns a dictionary type and Prefixed<T> is undecided while T is a type parameter. That is the same deferral the conditional types page covers, and the same ways out: as unknown as Prefixed<T>, or the as any the module-scope version uses with a visible lint ignore attached:
function prefix<T extends object>(source: T): Prefixed<T> {
return Object.fromEntries(
Object.entries(source).map(([key, value]) => [`$${key}`, value]),
// deno-lint-ignore no-explicit-any
) as any;
}
Deno.test("a computed return type will not accept what you computed", () => {
const prefixed = prefix({ score: 3, title: "Mapped types" });
assertType<
IsExact<typeof prefixed, { $score: number; $title: string }>
>(true);
assertEquals(prefixed, { $score: 3, $title: "Mapped types" });
});
a computed return type will not accept what you computed ... ok (53µs)
So a mapped return type is a place where the checker stops helping, and the compensation is entirely at the call site: prefix returns exactly {$score: number; $title: string} for that argument, verified where it is used. Two habits make the trade worth it. Write the return type explicitly, as above, rather than letting it be inferred from the cast, because an inferred return type here would be any, silently. And know that Deno has an opinion about this for published code. Put the same function, minus its return annotation, in a scratch package: a directory anywhere with a deno.json naming a name and exports, and the function in its mod.ts:
{
"name": "@scratch/prefix",
"version": "0.1.0",
"exports": "./mod.ts"
}
type Prefixed<T> = {
[Key in keyof T & string as `$${Key}`]: T[Key];
};
export function prefix<T extends object>(source: T) {
return Object.fromEntries(
Object.entries(source).map(([key, value]) => [`$${key}`, value]),
// deno-lint-ignore no-explicit-any
) as any;
}
error[no-slow-types]: missing explicit return type in the public API
--> /prefix-package/mod.ts:5:17
|
5 | export function prefix<T extends object>(source: T) {
| ^^^^^^ this function is missing an explicit return type
|
= hint: add an explicit return type to the function
info: all functions in the public API must have an explicit return type
docs: https://jsr.io/go/slow-type-missing-explicit-return-type
Found 1 problem
Checked 1 file
That is no-slow-types, the rule deno lint --rules-tags=jsr turns on and JSR applies to packages. It only fires for a package with a name and exports in its deno.json, so it is a publishing concern rather than an everyday one, but it is the same advice arriving with a tool attached.
The whole entry
Run the whole reference suite:
Check programs/any-unknown-never.test.ts
Check programs/arrays.test.ts
Check programs/assignment.test.ts
Check programs/async-functions.test.ts
Check programs/async-iteration.test.ts
Check programs/branching.test.ts
Check programs/branding.test.ts
Check programs/buffers-and-views.test.ts
Check programs/classes-as-values.test.ts
Check programs/classes.test.ts
Check programs/closures.test.ts
Check programs/conditional-types.test.ts
Check programs/conversion-and-coercion.test.ts
Check programs/dates-and-times.test.ts
Check programs/designing-error-types.test.ts
Check programs/destructuring.test.ts
Check programs/enum-patterns.test.ts
Check programs/enums.test.ts
Check programs/equality.test.ts
Check programs/errors-and-exceptions.test.ts
Check programs/function-types.test.ts
Check programs/functions.test.ts
Check programs/generators.test.ts
Check programs/interfaces-and-type-aliases.test.ts
Check programs/iterables-and-iterators.test.ts
Check programs/iterator-helpers.test.ts
Check programs/json.test.ts
Check programs/keyof-and-indexed-access.test.ts
Check programs/loops.test.ts
Check programs/mapped-types.test.ts
Check programs/maps.test.ts
Check programs/matching-and-replacing.test.ts
Check programs/module-specifiers.test.ts
Check programs/modules.test.ts
Check programs/mutating-arrays.test.ts
Check programs/nothing-twice.test.ts
Check programs/numbers.test.ts
Check programs/object-types.test.ts
Check programs/objects-as-dictionaries.test.ts
Check programs/objects.test.ts
Check programs/ordering-and-sorting.test.ts
Check programs/overloading.test.ts
Check programs/parameters-and-arguments.test.ts
Check programs/private-class-members.test.ts
Check programs/promise-combinators.test.ts
Check programs/promises.test.ts
Check programs/prototypes-and-inheritance.test.ts
Check programs/read-only.test.ts
Check programs/regular-expressions.test.ts
Check programs/scope-and-declarations.test.ts
Check programs/sentinels.test.ts
Check programs/sets.test.ts
Check programs/strings.test.ts
Check programs/subclassing.test.ts
Check programs/symbols.test.ts
Check programs/tagged-templates.test.ts
Check programs/testing-types.test.ts
Check programs/text-and-characters.test.ts
Check programs/the-event-loop.test.ts
Check programs/the-value-of-this.test.ts
Check programs/transforming-arrays.test.ts
Check programs/truthiness.test.ts
Check programs/typed-arrays.test.ts
Check programs/typing-arrays.test.ts
Check programs/typing-classes.test.ts
Check programs/unicode-in-patterns.test.ts
Check programs/unions-and-narrowing.test.ts
Check programs/values-and-references.test.ts
Check programs/weak-collections.test.ts
Check programs/what-a-type-is.test.ts
running 10 tests from ./programs/any-unknown-never.test.ts
...
running 13 tests from ./programs/arrays.test.ts
...
running 9 tests from ./programs/assignment.test.ts
...
running 10 tests from ./programs/async-functions.test.ts
...
running 11 tests from ./programs/async-iteration.test.ts
...
running 10 tests from ./programs/branching.test.ts
...
running 9 tests from ./programs/branding.test.ts
...
running 12 tests from ./programs/buffers-and-views.test.ts
...
running 10 tests from ./programs/classes-as-values.test.ts
...
running 11 tests from ./programs/classes.test.ts
...
running 6 tests from ./programs/closures.test.ts
...
running 14 tests from ./programs/conditional-types.test.ts
...
running 11 tests from ./programs/conversion-and-coercion.test.ts
...
running 13 tests from ./programs/dates-and-times.test.ts
...
running 10 tests from ./programs/designing-error-types.test.ts
...
running 14 tests from ./programs/destructuring.test.ts
...
running 12 tests from ./programs/enum-patterns.test.ts
...
running 7 tests from ./programs/enums.test.ts
...
running 11 tests from ./programs/equality.test.ts
...
running 10 tests from ./programs/errors-and-exceptions.test.ts
...
running 12 tests from ./programs/function-types.test.ts
...
running 11 tests from ./programs/functions.test.ts
...
running 12 tests from ./programs/generators.test.ts
...
running 9 tests from ./programs/interfaces-and-type-aliases.test.ts
...
running 14 tests from ./programs/iterables-and-iterators.test.ts
...
running 12 tests from ./programs/iterator-helpers.test.ts
...
running 11 tests from ./programs/json.test.ts
...
running 14 tests from ./programs/keyof-and-indexed-access.test.ts
...
running 14 tests from ./programs/loops.test.ts
...
running 12 tests from ./programs/mapped-types.test.ts
one property written per turn ... ok (277µs)
any union of keys, and Record written out ... ok (32µs)
an index signature is not a mapped type ... ok (11µs)
mapping preserves the kind of type it was given ... ok (16µs)
as renames the key it is about to write ... ok (35µs)
as costs you tuple-ness ... ok (30µs)
two ways to drop a property ... ok (22µs)
the four modifiers, and the four utility types they are ... ok (24µs)
Pick and Omit are a mapped type and a filter ... ok (15µs)
readonly is invisible to assignability ... ok (20µs)
optional is detectable, and easier ... ok (19µs)
a computed return type will not accept what you computed ... ok (48µs)
running 15 tests from ./programs/maps.test.ts
...
running 15 tests from ./programs/matching-and-replacing.test.ts
...
running 6 tests from ./programs/module-specifiers.test.ts
...
running 12 tests from ./programs/modules.test.ts
...
running 10 tests from ./programs/mutating-arrays.test.ts
...
running 11 tests from ./programs/nothing-twice.test.ts
...
running 15 tests from ./programs/numbers.test.ts
...
running 15 tests from ./programs/object-types.test.ts
...
running 14 tests from ./programs/objects-as-dictionaries.test.ts
...
running 13 tests from ./programs/objects.test.ts
...
running 12 tests from ./programs/ordering-and-sorting.test.ts
...
running 8 tests from ./programs/overloading.test.ts
...
running 11 tests from ./programs/parameters-and-arguments.test.ts
...
running 11 tests from ./programs/private-class-members.test.ts
...
running 11 tests from ./programs/promise-combinators.test.ts
...
running 11 tests from ./programs/promises.test.ts
...
running 12 tests from ./programs/prototypes-and-inheritance.test.ts
...
running 12 tests from ./programs/read-only.test.ts
...
running 13 tests from ./programs/regular-expressions.test.ts
...
running 9 tests from ./programs/scope-and-declarations.test.ts
...
running 8 tests from ./programs/sentinels.test.ts
...
running 13 tests from ./programs/sets.test.ts
...
running 10 tests from ./programs/strings.test.ts
...
running 11 tests from ./programs/subclassing.test.ts
...
running 10 tests from ./programs/symbols.test.ts
...
running 8 tests from ./programs/tagged-templates.test.ts
...
running 14 tests from ./programs/testing-types.test.ts
...
running 10 tests from ./programs/text-and-characters.test.ts
...
running 9 tests from ./programs/the-event-loop.test.ts
...
running 10 tests from ./programs/the-value-of-this.test.ts
...
running 13 tests from ./programs/transforming-arrays.test.ts
...
running 9 tests from ./programs/truthiness.test.ts
...
running 14 tests from ./programs/typed-arrays.test.ts
...
running 10 tests from ./programs/typing-arrays.test.ts
...
running 12 tests from ./programs/typing-classes.test.ts
...
running 11 tests from ./programs/unicode-in-patterns.test.ts
...
running 13 tests from ./programs/unions-and-narrowing.test.ts
...
running 13 tests from ./programs/values-and-references.test.ts
...
running 9 tests from ./programs/weak-collections.test.ts
...
running 7 tests from ./programs/what-a-type-is.test.ts
...
ok | 784 passed | 0 failed (915ms)
Twelve tests, and the practice is short. Use the built-in before you write your own: Record, Partial, Required, Readonly, Pick, Omit, each now written out above, and knowing what each one is happens to be different from needing to write them. Reach for as to filter, not just to rename, because renaming a key to never drops the property and reads better than mapping to never and indexing the result; keep the key-union form for when you want the union of keys as its own named type. Remember that as costs you tuple-ness, so if the input might be a tuple or an array and you want it to stay one, do not rename. Write +? and +readonly with their plus signs, and remember the removal forms exist, -readonly in particular, since there is no utility type for it. Keep a mapped type to one line of body, because errors from a mapped type are reported against the whole thing, so a long body means a long message about a type you cannot see; if the property expression needs a conditional inside a conditional, name the inner type. And give a function with a mapped return type an explicit return annotation, expecting to cast inside it: that cast is the only unchecked line, so make it the last thing in the function and keep the function small enough that a reader can confirm it by eye.