Skip to content

Commit 6e7fe2e

Browse files
authored
Merge pull request #4220 from github/mbg/fix-action-state-type-inference
Fix type inference for `ActionState`
2 parents 71f2680 + e57c0e6 commit 6e7fe2e

5 files changed

Lines changed: 107 additions & 10 deletions

File tree

‎lib/entry-points.js‎

Lines changed: 11 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/action-common.ts‎

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,29 @@ export interface FeatureState {
5656
/** Identifies a type of state an Action may have. */
5757
export type StateFeature = keyof FeatureState;
5858

59+
/**
60+
* The `Env` feature implies the availability of the `ReadOnlyEnv` feature.
61+
*
62+
* If `T` is `Env`, this returns `Env | ReadOnlyEnv`.
63+
* Otherwise, it is the identity and returns T.
64+
*/
65+
type ImpliedFeatures<T extends StateFeature> = T extends "Env"
66+
? "Env" | "ReadOnlyEnv"
67+
: T;
68+
69+
/**
70+
* Given an object type `Obj`, this tries to lookup a corresponding `StateFeature`
71+
* to which the object type belongs in `FeatureState`. Resolves to `never` if there
72+
* is no match.
73+
*/
74+
type FeatureNameFor<Obj extends object> = {
75+
[K in StateFeature]: [Obj] extends [FeatureState[K]]
76+
? [FeatureState[K]] extends [Obj]
77+
? K
78+
: never
79+
: never;
80+
}[StateFeature];
81+
5982
/** Constructs the intersection of all state types identifies by `Fs`. */
6083
export type FieldsOf<Fs extends readonly StateFeature[]> = Fs extends []
6184
? Record<never, never>
@@ -66,8 +89,54 @@ export type FieldsOf<Fs extends readonly StateFeature[]> = Fs extends []
6689
? FeatureState[Head] & FieldsOf<Tail>
6790
: never;
6891

92+
/**
93+
* Symbol used for a field in `ActionState` that carries the type array of state features.
94+
* This is a Symbol so that it doesn't clash with any property names we might want to have.
95+
*/
96+
const stateFeatures = Symbol();
97+
6998
/** Describes the state of an Action that has access to the state corresponding to `Fs`. */
70-
export type ActionState<Fs extends readonly StateFeature[]> = FieldsOf<Fs>;
99+
export type ActionState<Fs extends readonly StateFeature[]> = FieldsOf<Fs> & {
100+
/**
101+
* When given a chance, TypeScript will simplify an `ActionState<Fs>` type as much as possible,
102+
* which results in a concrete object type that doesn't mention `Fs`.
103+
*
104+
* That causes problems for functions which accept `ActionState<Fs>` values, but need to know the
105+
* feature keys `Fs`. This property here explicitly captures `Fs` in the concrete object type
106+
* that results from simplifying `ActionState<Fs>`.
107+
*
108+
* This is a function rather than a field, because we want to be able to provide values of type
109+
* `ActionState<Fs>` to functions expecting `ActionState<As>` where `As` is a subset of `Fs`.
110+
*
111+
* Since function types are contravariant in the types of their parameters, using a function
112+
* type here allows that to happen.
113+
*
114+
* Because the field is optional, we don't have to explicitly provide a value
115+
* for it anywhere while the type is still inferred.
116+
*
117+
* `Fs[number]` returns the union of all features in `Fs`. We wrap it in `ImpliedFeatures`
118+
* so that `Env` is expanded into `Env | ReadOnlyEnv`, allowing functions that expect the
119+
* `ReadOnlyEnv` feature to be provided with an `ActionState` that has the `Env` feature
120+
* without requiring this to be made explicit.
121+
*/
122+
readonly [stateFeatures]?: (ts: ImpliedFeatures<Fs[number]>) => void;
123+
};
124+
125+
/** Extends `state` with an `extra` feature. */
126+
export function extendActionState<
127+
// In first position, so that it can be explicitly provided if `FeatureNameFor`
128+
// should not work on `extra`.
129+
F extends StateFeature,
130+
Fs extends readonly StateFeature[],
131+
E extends FeatureState[F],
132+
>(
133+
state: ActionState<Fs>,
134+
extra: E,
135+
): ActionState<[...Fs, FeatureNameFor<E> & F]> {
136+
return { ...state, ...extra } as unknown as ActionState<
137+
[...Fs, FeatureNameFor<E> & F]
138+
>;
139+
}
71140

72141
/** The type of an Action's main entry point. This is a function that is provided
73142
* with a basic `ActionState` object with features that are always available.

‎src/init-action.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ import * as core from "@actions/core";
55
import * as io from "@actions/io";
66
import * as semver from "semver";
77

8-
import { Action, ActionState, runInActions } from "./action-common";
8+
import {
9+
Action,
10+
ActionState,
11+
extendActionState,
12+
runInActions,
13+
} from "./action-common";
914
import {
1015
FileCmdNotFoundError,
1116
getActionVersion,
@@ -277,7 +282,9 @@ async function run(
277282
}
278283

279284
// Compute the value of the `config-file` input.
280-
const actionStateWithFeatures = { ...actionState, features };
285+
const actionStateWithFeatures = extendActionState(actionState, {
286+
features,
287+
});
281288
configFile = await getConfigFileInput(
282289
actionStateWithFeatures,
283290
repositoryProperties,

‎src/setup-codeql-action.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import * as core from "@actions/core";
22

3-
import { Action, ActionState, runInActions } from "./action-common";
3+
import {
4+
Action,
5+
ActionState,
6+
extendActionState,
7+
runInActions,
8+
} from "./action-common";
49
import {
510
getActionVersion,
611
getOptionalInput,
@@ -131,7 +136,9 @@ async function run(
131136
);
132137
const repositoryProperties = repositoryPropertiesResult.orElse({});
133138

134-
const actionStateWithFeatures = { ...actionState, features };
139+
const actionStateWithFeatures = extendActionState(actionState, {
140+
features,
141+
});
135142

136143
const statusReportBase = await createStatusReportBase(
137144
ActionName.SetupCodeQL,

‎src/start-proxy-action.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import * as path from "path";
33

44
import * as core from "@actions/core";
55

6-
import { Action, ActionState, runInActions } from "./action-common";
6+
import {
7+
Action,
8+
ActionState,
9+
extendActionState,
10+
runInActions,
11+
} from "./action-common";
712
import * as actionsUtil from "./actions-util";
813
import { getGitHubVersion } from "./api-client";
914
import { FeatureEnablement, initFeatures } from "./feature-flags";
@@ -98,7 +103,8 @@ async function run(action: ActionState<["Base", "Logger", "Env", "Actions"]>) {
98103
};
99104

100105
// Start the Proxy
101-
const proxyBin = await getProxyBinaryPath({ ...action, features });
106+
const actionWithFeatures = extendActionState(action, { features });
107+
const proxyBin = await getProxyBinaryPath(actionWithFeatures);
102108
const proxyInfo = await startProxy(
103109
proxyBin,
104110
proxyConfig,

0 commit comments

Comments
 (0)