Skip to content

Commit

Permalink
Strengthen types
Browse files Browse the repository at this point in the history
  • Loading branch information
pokey committed Oct 20, 2023
1 parent 43fbdd7 commit 5aeaf3f
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 40 deletions.
36 changes: 14 additions & 22 deletions packages/cursorless-engine/src/DefaultSpokenFormMap.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { mapValues } from "lodash";
import {
PartialSpokenFormTypes,
SpokenFormMap,
SpokenFormMapEntry,
SpokenFormMapKeyTypes,
SpokenFormMappingType,
mapSpokenForms,
} from "./SpokenFormMap";

type DefaultSpokenFormMapDefinition = {
Expand Down Expand Up @@ -194,35 +195,26 @@ export interface DefaultSpokenFormMapEntry {
isPrivate: boolean;
}

export type DefaultSpokenFormMap = {
readonly [K in keyof SpokenFormMapKeyTypes]: K extends PartialSpokenFormTypes
? Readonly<
Partial<Record<SpokenFormMapKeyTypes[K], DefaultSpokenFormMapEntry>>
>
: Record<SpokenFormMapKeyTypes[K], DefaultSpokenFormMapEntry>;
};
export type DefaultSpokenFormMap =
SpokenFormMappingType<DefaultSpokenFormMapEntry>;

/**
* This map contains information about the default spoken forms for all our
* speakable entities, including scope types, paired delimiters, etc. Note that
* this map can't be used as a spoken form map. If you want something that can
* be used as a spoken form map, see {@link defaultSpokenFormMap}.
*/
export const defaultSpokenFormInfo = mapValues(
export const defaultSpokenFormInfo: DefaultSpokenFormMap = mapSpokenForms(
defaultSpokenFormMapCore,
(entry) =>
mapValues(entry, (subEntry) =>
typeof subEntry === "string"
? {
defaultSpokenForms: [subEntry],
isDisabledByDefault: false,
isPrivate: false,
}
: subEntry,
),
// FIXME: Don't cast here; need to make our own mapValues with stronger typing
// using tricks from our object.d.ts
) as DefaultSpokenFormMap;
(subEntry) =>
typeof subEntry === "string"
? {
defaultSpokenForms: [subEntry],
isDisabledByDefault: false,
isPrivate: false,
}
: subEntry,
);

/**
* A spoken form map constructed from the default spoken forms. It is designed to
Expand Down
48 changes: 43 additions & 5 deletions packages/cursorless-engine/src/SpokenFormMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,17 @@ export interface SpokenFormMapEntry {
isPrivate: boolean;
}

/**
* A type that contains all the keys of {@link SpokenFormMapKeyTypes}, each of
* whose values are a map from the allowed identifiers for that key to a particular
* value type {@link T}.
*/
export type SpokenFormMappingType<T> = {
readonly [K in SpokenFormType]: K extends PartialSpokenFormTypes
? Readonly<Partial<Record<SpokenFormMapKeyTypes[K], T>>>
: Readonly<Record<SpokenFormMapKeyTypes[K], T>>;
};

/**
* A spoken form map contains information about the spoken forms for all our
* speakable entities, including scope types, paired delimiters, etc. It can
Expand All @@ -108,8 +119,35 @@ export interface SpokenFormMapEntry {
* Each key of this map is a type of spoken form, eg `simpleScopeTypeType`, and
* the value is a map of identifiers to {@link SpokenFormMapEntry}s.
*/
export type SpokenFormMap = {
readonly [K in keyof SpokenFormMapKeyTypes]: K extends PartialSpokenFormTypes
? Readonly<Partial<Record<SpokenFormMapKeyTypes[K], SpokenFormMapEntry>>>
: Readonly<Record<SpokenFormMapKeyTypes[K], SpokenFormMapEntry>>;
};
export type SpokenFormMap = SpokenFormMappingType<SpokenFormMapEntry>;

/**
* Converts a spoken form map to a spoken form component map for use in spoken
* form generation.
* @param spokenFormMap The spoken form map to convert to a spoken form
* component map
* @returns A spoken form component map that can be used to generate spoken
* forms
*/
export function mapSpokenForms<I, O>(
input: SpokenFormMappingType<I>,
mapper: <T extends SpokenFormType>(
input: I,
spokenFormType: T,
id: SpokenFormMapKeyTypes[T],
) => O,
): SpokenFormMappingType<O> {
return Object.fromEntries(
Object.entries(input).map(([spokenFormType, map]) => [
spokenFormType,
Object.fromEntries(
Object.entries(map).map(([id, inputValue]) => [
id,
mapper(inputValue!, spokenFormType as SpokenFormType, id),
]),
),
]),
// FIXME: Don't cast here; need to make our own mapValues with stronger typing
// using tricks from our object.d.ts
) as SpokenFormMappingType<O>;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { SpokenFormMapEntry, SpokenFormType } from "../SpokenFormMap";
import {
SpokenFormMapEntry,
SpokenFormMapKeyTypes,
SpokenFormType,
} from "../SpokenFormMap";

/**
* A component of a spoken form used internally during spoken form generation.
Expand All @@ -17,14 +21,18 @@ export type SpokenFormComponent =
| string
| SpokenFormComponent[];

export interface CustomizableSpokenFormComponentForType<T extends SpokenFormType> {
type: "customizable";
spokenForms: SpokenFormMapEntry;
spokenFormType: T;
id: SpokenFormMapKeyTypes[T];
}

/**
* A customizable spoken form component. This is a spoken form component that
* can be customized by the user. It is used internally during spoken form
* generation.
*/
export interface CustomizableSpokenFormComponent {
type: "customizable";
spokenForms: SpokenFormMapEntry;
spokenFormType: SpokenFormType;
id: string;
}
export type CustomizableSpokenFormComponent = {
[K in SpokenFormType]: CustomizableSpokenFormComponentForType<K>;
}[SpokenFormType];
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { CompositeKeyMap } from "@cursorless/common";
import { SpeakableSurroundingPairName } from "../../SpokenFormMap";
import { SpokenFormComponentMap } from "../getSpokenFormComponentMap";
import { CustomizableSpokenFormComponent } from "../SpokenFormComponent";
import {
CustomizableSpokenFormComponentForType,
} from "../SpokenFormComponent";

const surroundingPairsDelimiters: Record<
SpeakableSurroundingPairName,
Expand Down Expand Up @@ -50,7 +52,7 @@ export function surroundingPairDelimitersToSpokenForm(
spokenFormMap: SpokenFormComponentMap,
left: string,
right: string,
): CustomizableSpokenFormComponent {
): CustomizableSpokenFormComponentForType<"pairedDelimiter"> {
const pairName = surroundingPairDelimiterToName.get([left, right]);
if (pairName == null) {
throw Error(`Unknown surrounding pair delimiters '${left} ${right}'`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import {
PartialSpokenFormTypes,
SpokenFormMap,
SpokenFormMapKeyTypes,
SpokenFormType,
} from "../SpokenFormMap";
import { CustomizableSpokenFormComponent } from "./SpokenFormComponent";
import { CustomizableSpokenFormComponentForType } from "./SpokenFormComponent";

/**
* A spoken form component map is a map of spoken form types to a map of IDs to
Expand All @@ -13,9 +14,21 @@ import { CustomizableSpokenFormComponent } from "./SpokenFormComponent";
* generation.
*/
export type SpokenFormComponentMap = {
readonly [K in keyof SpokenFormMapKeyTypes]: K extends PartialSpokenFormTypes
? Partial<Record<SpokenFormMapKeyTypes[K], CustomizableSpokenFormComponent>>
: Record<SpokenFormMapKeyTypes[K], CustomizableSpokenFormComponent>;
readonly [K in SpokenFormType]: K extends PartialSpokenFormTypes
? Readonly<
Partial<
Record<
SpokenFormMapKeyTypes[K],
CustomizableSpokenFormComponentForType<K>
>
>
>
: Readonly<
Record<
SpokenFormMapKeyTypes[K],
CustomizableSpokenFormComponentForType<K>
>
>;
};

/**
Expand Down

0 comments on commit 5aeaf3f

Please sign in to comment.