Allow optional inputs to be Input<T | undefined>

The Node.js runtime accepts `Output<undefined>` values as inputs to
optional parameters, but the TypeScript typing currently does not
allow these. This extends the TypeScript typings to allow
`Output<undefined>` values as inputs to optional input properties.

I cannot think of any way in which this is breaking or would regress
any aspect of the TypeScript experience, other than making the `.d.ts`
files a little "noisier".

Fixes #6175.
This commit is contained in:
Luke Hoban 2021-02-12 13:28:43 +11:00 committed by Pat Gavlin
parent ed769377dc
commit ae70447a18
4 changed files with 62 additions and 26 deletions

View file

@ -58,7 +58,7 @@
"plain": "map[string]string"
},
"nodejs": {
"input": "pulumi.Input<{[key: string]: pulumi.Input<string>}> | undefined",
"input": "pulumi.Input<{[key: string]: pulumi.Input<string>} | undefined> | undefined",
"plain": "{[key: string]: string} | undefined"
},
"python": {
@ -114,7 +114,7 @@
"plain": "pulumi.Archive"
},
"nodejs": {
"input": "pulumi.Input<pulumi.asset.Archive> | undefined",
"input": "pulumi.Input<pulumi.asset.Archive | undefined> | undefined",
"plain": "pulumi.asset.Archive | undefined"
},
"python": {
@ -139,7 +139,7 @@
"plain": "pulumi.AssetOrArchive"
},
"nodejs": {
"input": "pulumi.Input<pulumi.asset.Asset | pulumi.asset.Archive> | undefined",
"input": "pulumi.Input<pulumi.asset.Asset | pulumi.asset.Archive | undefined> | undefined",
"plain": "pulumi.asset.Asset | pulumi.asset.Archive | undefined"
},
"python": {
@ -164,7 +164,7 @@
"plain": "*bool"
},
"nodejs": {
"input": "pulumi.Input<boolean> | undefined",
"input": "pulumi.Input<boolean | undefined> | undefined",
"plain": "boolean | undefined"
},
"python": {
@ -189,7 +189,7 @@
"plain": "*int"
},
"nodejs": {
"input": "pulumi.Input<number> | undefined",
"input": "pulumi.Input<number | undefined> | undefined",
"plain": "number | undefined"
},
"python": {
@ -239,7 +239,7 @@
"plain": "*float64"
},
"nodejs": {
"input": "pulumi.Input<number> | undefined",
"input": "pulumi.Input<number | undefined> | undefined",
"plain": "number | undefined"
},
"python": {
@ -464,7 +464,7 @@
"plain": "*string"
},
"nodejs": {
"input": "pulumi.Input<string> | undefined",
"input": "pulumi.Input<string | undefined> | undefined",
"plain": "string | undefined"
},
"python": {
@ -508,7 +508,7 @@
"plain": "map[string]string"
},
"nodejs": {
"input": "pulumi.Input<{[key: string]: pulumi.Input<string>}> | undefined",
"input": "pulumi.Input<{[key: string]: pulumi.Input<string>} | undefined> | undefined",
"plain": "{[key: string]: string} | undefined"
},
"python": {
@ -549,7 +549,7 @@
"plain": "interface{}"
},
"nodejs": {
"input": "pulumi.Input<outputs.ObjectArgs | any[]> | undefined",
"input": "pulumi.Input<outputs.ObjectArgs | any[] | undefined> | undefined",
"plain": "outputs.Object | any[] | undefined"
},
"python": {

View file

@ -39,6 +39,14 @@ func Identifier(id string) TypeAst {
return &idType{id}
}
// IsIdentifier returns true if the AST node is an identifier.
func IsIdentifier(t TypeAst) (string, bool) {
if i, ok := t.(*idType); ok {
return i.id, true
}
return "", false
}
// Builds a `T[]` type from a `T` type.
func Array(t TypeAst) TypeAst {
return &arrayType{t}

View file

@ -277,16 +277,38 @@ func tokenToFunctionName(tok string) string {
func (mod *modContext) typeAst(t schema.Type, input bool, constValue interface{}) tstypes.TypeAst {
switch t := t.(type) {
case *schema.OptionalType:
// Treat optional(input(T)) as optional(input(optional(T))).
elementType := t.ElementType
if input, isInput := elementType.(*schema.InputType); isInput {
elementType = &schema.InputType{
ElementType: &schema.OptionalType{
ElementType: input.ElementType,
},
}
}
return tstypes.Union(
mod.typeAst(t.ElementType, input, constValue),
mod.typeAst(elementType, input, constValue),
tstypes.Identifier("undefined"),
)
case *schema.InputType:
typ := mod.typeString(codegen.SimplifyInputUnion(t.ElementType), input, constValue)
if typ == "any" {
return tstypes.Identifier("any")
elementType := t.ElementType
optional, isOptional := elementType.(*schema.OptionalType)
if isOptional {
elementType = optional.ElementType
}
return tstypes.Identifier(fmt.Sprintf("pulumi.Input<%s>", typ))
typ := mod.typeAst(codegen.SimplifyInputUnion(elementType), input, constValue)
if id, ok := tstypes.IsIdentifier(typ); ok && id == "any" {
return typ
}
if isOptional {
typ = tstypes.Union(typ, tstypes.Identifier("undefined"))
}
typeArgument := tstypes.TypeLiteral(tstypes.Normalize(typ))
return tstypes.Identifier(fmt.Sprintf("pulumi.Input<%s>", typeArgument))
case *schema.EnumType:
return tstypes.Identifier(mod.objectType(nil, nil, t.Token, input, false, true))
case *schema.ArrayType:

View file

@ -13,8 +13,8 @@
// limitations under the License.
import * as assert from "assert";
import { ComponentResource, CustomResource, DependencyResource, Inputs, Output, Resource, ResourceOptions, runtime,
secret } from "../../index";
import { ComponentResource, CustomResource, DependencyResource, Input, Inputs, Output, Resource, ResourceOptions,
output, runtime, secret } from "../../index";
import { asyncTest } from "../util";
const gstruct = require("google-protobuf/google/protobuf/struct_pb.js");
@ -114,7 +114,9 @@ type TestBoolEnum = (typeof TestBoolEnum)[keyof typeof TestBoolEnum];
interface TestInputs {
aNum: number;
bStr: string;
bStrInput: Input<string>;
cUnd: undefined;
cUndInput: Input<undefined>;
dArr: Promise<Array<any>>;
id: string;
urn: string;
@ -207,16 +209,18 @@ describe("runtime", () => {
it("marshals basic properties correctly", asyncTest(async () => {
const inputs: TestInputs = {
"aNum": 42,
"bStr": "a string",
"cUnd": undefined,
"dArr": Promise.resolve([ "x", 42, Promise.resolve(true), Promise.resolve(undefined) ]),
"id": "foo",
"urn": "bar",
"strEnum": TestStrEnum.Foo,
"intEnum": TestIntEnum.One,
"numEnum": TestNumEnum.One,
"boolEnum": TestBoolEnum.One,
aNum: 42,
bStr: "a string",
bStrInput: output("a string"),
cUnd: undefined,
cUndInput: output(undefined),
dArr: Promise.resolve([ "x", 42, Promise.resolve(true), Promise.resolve(undefined) ]),
id: "foo",
urn: "bar",
strEnum: TestStrEnum.Foo,
intEnum: TestIntEnum.One,
numEnum: TestNumEnum.One,
boolEnum: TestBoolEnum.One,
};
// Serialize and then deserialize all the properties, checking that they round-trip as expected.
const transfer = gstruct.Struct.fromJavaScript(
@ -224,7 +228,9 @@ describe("runtime", () => {
const result = runtime.deserializeProperties(transfer);
assert.strictEqual(result.aNum, 42);
assert.strictEqual(result.bStr, "a string");
assert.strictEqual(result.bStrInput, "a string");
assert.strictEqual(result.cUnd, undefined);
assert.strictEqual(result.cUndInput, undefined);
assert.deepStrictEqual(result.dArr, [ "x", 42, true, null ]);
assert.strictEqual(result.id, "foo");
assert.strictEqual(result.urn, "bar");