Repository: uber-go/dig
Pull request #293 (2021-09-15 03:05:37)

Add Dig.As ProvideOption
This brings back `Dig.As` from #233 with some changes. Specifically, `Dig.Provide(t1, As(t2))`used to provide type `t2` as well as type `t1` but that is no longer the case.

For example, `c.Provide(newBuffer, dig.As(new(io.Reader), new(io.Writer)))` will provide `io.Reader` and `io.Writer`, but not `buffer`. 

Refs: GO-451

CLAassistant: [![CLA assistant check](https://cla-assistant.io/pull/badge/not_signed)](https://cla-assistant.io/uber-go/dig?pullRequest=293) <br/>Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our [Contributor License Agreement](https://cla-assistant.io/uber-go/dig?pullRequest=293) before we can accept your contribution.<br/>**1** out of **2** committers have signed the CLA.<br/><br/>:white_check_mark: sywhang<br/>:x: alessandrozucca<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla-assistant.io/check/uber-go/dig?pullRequest=293) it.</sub>
codecov[bot]: # [Codecov](https://codecov.io/gh/uber-go/dig/pull/293?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) Report
> Merging [#293](https://codecov.io/gh/uber-go/dig/pull/293?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) (2da6412) into [master](https://codecov.io/gh/uber-go/dig/commit/4350cd34d1e25940d17eda2e514533df503254d1?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) (4350cd3) will **increase** coverage by `0.06%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/uber-go/dig/pull/293/graphs/tree.svg?width=650&height=150&src=pr&token=ARuH7Frwgw&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go)](https://codecov.io/gh/uber-go/dig/pull/293?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go)

```diff
@@            Coverage Diff             @@
##           master     #293      +/-   ##
==========================================
+ Coverage   98.35%   98.42%   +0.06%     
==========================================
  Files          14       14              
  Lines        1097     1144      +47     
==========================================
+ Hits         1079     1126      +47     
  Misses         12       12              
  Partials        6        6              
```


| [Impacted Files](https://codecov.io/gh/uber-go/dig/pull/293?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) | Coverage Δ | |
|---|---|---|
| [dig.go](https://codecov.io/gh/uber-go/dig/pull/293/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-ZGlnLmdv) | `100.00% <100.00%> (ø)` | |
| [result.go](https://codecov.io/gh/uber-go/dig/pull/293/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-cmVzdWx0Lmdv) | `97.15% <100.00%> (+0.44%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/uber-go/dig/pull/293?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/uber-go/dig/pull/293?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go). Last update [4350cd3...2da6412](https://codecov.io/gh/uber-go/dig/pull/293?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go).

sywhang: Hey @alessandrozucca, I know it's been a long time but we're bringing back `Dig.As`. Could you sign the CLA so that we can move forward with this? Thanks!
alessandrozucca: @sywhang all signed 🙌 
sywhang: Awesome! Thank you :) 

Patch:
diff --git a/dig.go b/dig.go
index ae33096..e99cc9e 100644
--- a/dig.go
+++ b/dig.go
@@ -63,14 +63,20 @@ type provideOptions struct {
 	Name     string
 	Group    string
 	Info     *ProvideInfo
+	As       []interface{}
 	Location *digreflect.Func
 }
 
 func (o *provideOptions) Validate() error {
-	if len(o.Group) > 0 && len(o.Name) > 0 {
-		return errf(
-			"cannot use named values with value groups",
-			"name:%q provided with group:%q", o.Name, o.Group)
+	if len(o.Group) > 0 {
+		if len(o.Name) > 0 {
+			return fmt.Errorf(
+				"cannot use named values with value groups: name:%q provided with group:%q", o.Name, o.Group)
+		}
+		if len(o.As) > 0 {
+			return fmt.Errorf(
+				"cannot use dig.As with value groups: dig.As provided with group:%q", o.Group)
+		}
 	}
 
 	// Names must be representable inside a backquoted string. The only
@@ -83,6 +89,23 @@ func (o *provideOptions) Validate() error {
 	if strings.ContainsRune(o.Group, '`') {
 		return errf("invalid dig.Group(%q): group names cannot contain backquotes", o.Group)
 	}
+
+	for _, i := range o.As {
+		t := reflect.TypeOf(i)
+
+		if t == nil {
+			return fmt.Errorf("invalid dig.As(nil): argument must be a pointer to an interface")
+		}
+
+		if t.Kind() != reflect.Ptr {
+			return fmt.Errorf("invalid dig.As(%v): argument must be a pointer to an interface", t)
+		}
+
+		pointingTo := t.Elem()
+		if pointingTo.Kind() != reflect.Interface {
+			return fmt.Errorf("invalid dig.As(*%v): argument must be a pointer to an interface", pointingTo)
+		}
+	}
 	return nil
 }
 
@@ -199,6 +222,54 @@ func FillProvideInfo(info *ProvideInfo) ProvideOption {
 	})
 }
 
+// As is a ProvideOption that specifies that the value produced by the
+// constructor implements one or more other interfaces and is provided
+// to the container as those interfaces.
+//
+// As expects one or more pointers to the implemented interfaces. Values
+// produced by constructors will be then available in the container as
+// implementations of all of those interfaces, but not as the value itself.
+//
+// For example, the following will make io.Reader and io.Writer available
+// in the container, but not buffer.
+//
+//   c.Provide(newBuffer, dig.As(new(io.Reader), new(io.Writer)))
+//
+// That is, the above is equivalent to the following.
+//
+//   c.Provide(func(...) (io.Reader, io.Writer) {
+//     b := newBuffer(...)
+//     return b, b
+//   })
+//
+// If used with dig.Name, the type produced by the constructor and the types
+// specified with dig.As will all use the same name. For example,
+//
+//   c.Provide(newFile, dig.As(new(io.Reader)), dig.Name("temp"))
+//
+// The above is equivalent to the following.
+//
+//   type Result struct {
+//     dig.Out
+//
+//     Reader io.Reader `name:"temp"`
+//   }
+//
+//   c.Provide(func(...) Result {
+//     f := newFile(...)
+//     return Result{
+//       Reader: f,
+//     }
+//   })
+//
+// This option cannot be provided for constructors which produce result
+// objects.
+func As(i ...interface{}) ProvideOption {
+	return provideOptionFunc(func(opts *provideOptions) {
+		opts.As = append(opts.As, i...)
+	})
+}
+
 // LocationForPC is a ProvideOption which specifies an alternate function program
 // counter address to be used for debug information. The package, name, file and
 // line number of this alternate function address will be used in error messages
@@ -551,6 +622,7 @@ func (c *Container) provide(ctor interface{}, opts provideOptions) error {
 		nodeOptions{
 			ResultName:  opts.Name,
 			ResultGroup: opts.Group,
+			ResultAs:    opts.As,
 			Location:    opts.Location,
 		},
 	)
@@ -688,32 +760,22 @@ func (cv connectionVisitor) Visit(res result) resultVisitor {
 	path := strings.Join(cv.currentResultPath, ".")
 
 	switch r := res.(type) {
+
 	case resultSingle:
 		k := key{name: r.Name, t: r.Type}
 
-		if conflict, ok := cv.keyPaths[k]; ok {
-			*cv.err = errf(
-				"cannot provide %v from %v", k, path,
-				"already provided by %v", conflict,
-			)
+		if err := cv.checkKey(k, path); err != nil {
+			*cv.err = err
 			return nil
 		}
-
-		if ps := cv.c.providers[k]; len(ps) > 0 {
-			cons := make([]string, len(ps))
-			for i, p := range ps {
-				cons[i] = fmt.Sprint(p.Location())
+		for _, asType := range r.As {
+			k := key{name: r.Name, t: asType}
+			if err := cv.checkKey(k, path); err != nil {
+				*cv.err = err
+				return nil
 			}
-
-			*cv.err = errf(
-				"cannot provide %v from %v", k, path,
-				"already provided by %v", strings.Join(cons, "; "),
-			)
-			return nil
 		}
 
-		cv.keyPaths[k] = path
-
 	case resultGrouped:
 		// we don't really care about the path for this since conflicts are
 		// okay for group results. We'll track it for the sake of having a
@@ -725,6 +787,28 @@ func (cv connectionVisitor) Visit(res result) resultVisitor {
 	return cv
 }
 
+func (cv connectionVisitor) checkKey(k key, path string) error {
+	defer func() { cv.keyPaths[k] = path }()
+	if conflict, ok := cv.keyPaths[k]; ok {
+		return errf(
+			"cannot provide %v from %v", k, path,
+			"already provided by %v", conflict,
+		)
+	}
+	if ps := cv.c.providers[k]; len(ps) > 0 {
+		cons := make([]string, len(ps))
+		for i, p := range ps {
+			cons[i] = fmt.Sprint(p.Location())
+		}
+
+		return errf(
+			"cannot provide %v from %v", k, path,
+			"already provided by %v", strings.Join(cons, "; "),
+		)
+	}
+	return nil
+}
+
 // node is a node in the dependency graph. Each node maps to a single
 // constructor provided by the user.
 //
@@ -753,9 +837,10 @@ type node struct {
 
 type nodeOptions struct {
 	// If specified, all values produced by this node have the provided name
-	// or belong to the specified value group
+	// belong to the specified value group or implement any of the interfaces.
 	ResultName  string
 	ResultGroup string
+	ResultAs    []interface{}
 	Location    *digreflect.Func
 }
 
@@ -774,6 +859,7 @@ func newNode(ctor interface{}, opts nodeOptions) (*node, error) {
 		resultOptions{
 			Name:  opts.ResultName,
 			Group: opts.ResultGroup,
+			As:    opts.ResultAs,
 		},
 	)
 	if err != nil {
diff --git a/result.go b/result.go
index 6f8bf23..28e9d47 100644
--- a/result.go
+++ b/result.go
@@ -22,6 +22,7 @@ package dig
 
 import (
 	"errors"
+	"fmt"
 	"reflect"
 
 	"go.uber.org/dig/internal/digerror"
@@ -37,6 +38,7 @@ import (
 //                 another result.
 //   resultGrouped A value produced by a constructor that is part of a value
 //                 group.
+
 type result interface {
 	// Extracts the values for this result from the provided value and
 	// stores them into the provided containerWriter.
@@ -61,6 +63,7 @@ type resultOptions struct {
 	// For Result Objects, name:".." tags on fields override this.
 	Name  string
 	Group string
+	As    []interface{}
 }
 
 // newResult builds a result from the given type.
@@ -97,7 +100,7 @@ func newResult(t reflect.Type, opts resultOptions) (result, error) {
 		}
 		return rg, nil
 	default:
-		return resultSingle{Type: t, Name: opts.Name}, nil
+		return newResultSingle(t, opts)
 	}
 }
 
@@ -190,14 +193,15 @@ func (rl resultList) DotResult() []*dot.Result {
 }
 
 func newResultList(ctype reflect.Type, opts resultOptions) (resultList, error) {
+	numOut := ctype.NumOut()
 	rl := resultList{
 		ctype:         ctype,
-		Results:       make([]result, 0, ctype.NumOut()),
-		resultIndexes: make([]int, ctype.NumOut()),
+		Results:       make([]result, 0, numOut),
+		resultIndexes: make([]int, numOut),
 	}
 
 	resultIdx := 0
-	for i := 0; i < ctype.NumOut(); i++ {
+	for i := 0; i < numOut; i++ {
 		t := ctype.Out(i)
 		if isError(t) {
 			rl.resultIndexes[i] = -1
@@ -243,21 +247,69 @@ func (rl resultList) ExtractList(cw containerWriter, values []reflect.Value) err
 type resultSingle struct {
 	Name string
 	Type reflect.Type
+
+	// If specified, this is a list of types which the value will be made
+	// available as, in addition to its own type.
+	As []reflect.Type
+}
+
+func newResultSingle(t reflect.Type, opts resultOptions) (resultSingle, error) {
+	r := resultSingle{
+		Type: t,
+		Name: opts.Name,
+	}
+
+	var asTypes []reflect.Type
+
+	for _, as := range opts.As {
+		ifaceType := reflect.TypeOf(as).Elem()
+		if ifaceType == t {
+			// Special case:
+			//   c.Provide(func() io.Reader, As(new(io.Reader)))
+			// Ignore instead of erroring out.
+			continue
+		}
+		if !t.Implements(ifaceType) {
+			return r, fmt.Errorf("invalid dig.As: %v does not implement %v", t, ifaceType)
+		}
+		asTypes = append(asTypes, ifaceType)
+	}
+
+	if len(asTypes) == 0 {
+		return r, nil
+	}
+
+	return resultSingle{
+		Type: asTypes[0],
+		Name: opts.Name,
+		As:   asTypes[1:],
+	}, nil
 }
 
 func (rs resultSingle) DotResult() []*dot.Result {
-	return []*dot.Result{
-		{
-			Node: &dot.Node{
-				Type: rs.Type,
-				Name: rs.Name,
-			},
+	dotResults := make([]*dot.Result, 0, len(rs.As)+1)
+	dotResults = append(dotResults, &dot.Result{
+		Node: &dot.Node{
+			Type: rs.Type,
+			Name: rs.Name,
 		},
+	})
+
+	for _, asType := range rs.As {
+		dotResults = append(dotResults, &dot.Result{
+			Node: &dot.Node{Type: asType, Name: rs.Name},
+		})
 	}
+
+	return dotResults
 }
 
 func (rs resultSingle) Extract(cw containerWriter, v reflect.Value) {
 	cw.setValue(rs.Name, rs.Type, v)
+
+	for _, asType := range rs.As {
+		cw.setValue(rs.Name, asType, v)
+	}
 }
 
 // resultObject is a dig.Out struct where each field is another result.


Test patch:
diff --git a/dig_test.go b/dig_test.go
index 7c0db2a..a66ae0c 100644
--- a/dig_test.go
+++ b/dig_test.go
@@ -65,12 +65,14 @@ func TestEndToEndSuccess(t *testing.T) {
 
 	t.Run("struct constructor", func(t *testing.T) {
 		c := New()
-		var buf bytes.Buffer
-		buf.WriteString("foo")
-		require.NoError(t, c.Provide(func() bytes.Buffer { return buf }), "provide failed")
+		require.NoError(t, c.Provide(func() bytes.Buffer {
+			var buf bytes.Buffer
+			buf.WriteString("foo")
+			return buf
+		}), "provide failed")
 		require.NoError(t, c.Invoke(func(b bytes.Buffer) {
 			// ensure we're getting back the buffer we put in
-			require.Equal(t, "foo", buf.String(), "invoke got new buffer")
+			require.Equal(t, "foo", b.String(), "invoke got new buffer")
 		}), "invoke failed")
 	})
 
@@ -627,6 +629,70 @@ func TestEndToEndSuccess(t *testing.T) {
 		}), "both objects should be successfully resolved on Invoke")
 	})
 
+	t.Run("struct constructor with as interface option", func(t *testing.T) {
+		c := New()
+
+		err := c.Provide(
+			func() *bytes.Buffer {
+				return bytes.NewBufferString("foo")
+			},
+			As(new(fmt.Stringer), new(io.Reader)),
+		)
+
+		require.NoError(t, err, "provide failed")
+
+		require.NoError(t, c.Invoke(
+			func(s fmt.Stringer, r io.Reader) {
+				require.Equal(t, "foo", s.String(), "invoke got new buffer")
+				got, err := ioutil.ReadAll(r)
+				assert.NoError(t, err, "failed to read from reader")
+				require.Equal(t, "foo", string(got), "invoke got new buffer")
+			},
+		), "invoke failed")
+
+		require.Error(t, c.Invoke(func(*bytes.Buffer) {
+			t.Fatalf("must not be called")
+		}), "must not have a *bytes.Buffer in the container")
+
+	})
+
+	t.Run("As with Name", func(t *testing.T) {
+		c := New()
+
+		require.NoError(t, c.Provide(
+			func() *bytes.Buffer {
+				return bytes.NewBufferString("foo")
+			},
+			As(new(io.Reader)),
+			Name("buff"),
+		), "failed to provide")
+
+		type in struct {
+			In
+
+			Buffer *bytes.Buffer `name:"buff"`
+			Reader io.Reader     `name:"buff"`
+		}
+
+		require.Error(t, c.Invoke(func(got in) {
+			t.Fatal("should not be called")
+		}))
+	})
+
+	t.Run("As same interface", func(t *testing.T) {
+		c := New()
+		require.NoError(t, c.Provide(func() io.Reader {
+			panic("this function should not be called")
+		}, As(new(io.Reader))), "failed to provide")
+	})
+
+	t.Run("As different interface", func(t *testing.T) {
+		c := New()
+		require.NoError(t, c.Provide(func() io.ReadCloser {
+			panic("this function should not be called")
+		}, As(new(io.Reader), new(io.Closer))), "failed to provide")
+	})
+
 	t.Run("invoke on a type that depends on named parameters", func(t *testing.T) {
 		c := New()
 		type A struct{ idx int }
@@ -1537,16 +1603,123 @@ func TestProvideInvalidGroup(t *testing.T) {
 	assert.Contains(t, err.Error(), `cannot parse group "foo,bar": invalid option "bar"`)
 }
 
-func TestProvideGroupAndName(t *testing.T) {
+func TestProvideInvalidAs(t *testing.T) {
+	ptrToStruct := &struct {
+		name string
+	}{
+		name: "example",
+	}
+	type out struct {
+		Out
+
+		name string
+	}
+	var nilInterface io.Reader
+
+	tests := []struct {
+		name        string
+		param       interface{}
+		expectedErr string
+	}{
+		{
+			name:        "as param is not an type interface",
+			param:       123,
+			expectedErr: "invalid dig.As(int): argument must be a pointer to an interface",
+		},
+		{
+			name:        "as param is a pointer to struct",
+			param:       ptrToStruct,
+			expectedErr: "invalid dig.As(*struct { name string }): argument must be a pointer to an interface",
+		},
+		{
+			name:        "as param is a nil interface",
+			param:       nilInterface,
+			expectedErr: "invalid dig.As(nil): argument must be a pointer to an interface",
+		},
+		{
+			name:        "as param is a nil",
+			param:       nil,
+			expectedErr: "invalid dig.As(nil): argument must be a pointer to an interface",
+		},
+		{
+			name:        "as param is a func",
+			param:       func() {},
+			expectedErr: "invalid dig.As(func()): argument must be a pointer to an interface",
+		},
+		{
+			name:        "as param is a func returning dig.out",
+			param:       func() *out { return &out{name: "example"} },
+			expectedErr: "invalid dig.As(func() *dig.out): argument must be a pointer to an interface",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			c := New()
+			err := c.Provide(
+				func() *bytes.Buffer {
+					var buf bytes.Buffer
+					return &buf
+				},
+				As(tt.param),
+			)
+
+			require.Error(t, err, "provide must fail")
+			assert.Contains(t, err.Error(), tt.expectedErr)
+		})
+	}
+}
+
+func TestAsExpectingOriginalType(t *testing.T) {
 	t.Parallel()
 
-	c := New()
-	err := c.Provide(func() io.Reader {
-		panic("this function must not be called")
-	}, Group("foo"), Name("bar"))
-	require.Error(t, err)
-	assert.Contains(t, err.Error(), "cannot use named values with value groups: "+
-		"name:\"bar\" provided with group:\"foo\"")
+	t.Run("fail on expecting original type", func(t *testing.T) {
+		c := New()
+
+		require.NoError(t, c.Provide(
+			func() *bytes.Buffer {
+				return bytes.NewBufferString("foo")
+			},
+			As(new(io.Reader)),
+			Name("buff"),
+		), "failed to provide")
+
+		type in struct {
+			In
+
+			Buffer *bytes.Buffer `name:"buff"`
+			Reader io.Reader     `name:"buff"`
+		}
+
+		require.Error(t, c.Invoke(func(got in) {
+			t.Fatal("*bytes.Buffer with name buff shouldn't be provided")
+		}))
+	})
+}
+
+func TestProvideIncompatibleOptions(t *testing.T) {
+	t.Parallel()
+
+	t.Run("group and name", func(t *testing.T) {
+		c := New()
+		err := c.Provide(func() io.Reader {
+			panic("this function must not be called")
+		}, Group("foo"), Name("bar"))
+		require.Error(t, err)
+		assert.Contains(t, err.Error(), "cannot use named values with value groups: "+
+			`name:"bar" provided with group:"foo"`)
+	})
+
+	t.Run("group and As", func(t *testing.T) {
+		c := New()
+		err := c.Provide(func() *bytes.Buffer {
+			panic("this function must not be called")
+		}, Group("foo"), As(new(io.Reader)))
+		require.Error(t, err)
+		assert.Contains(t, err.Error(), "cannot use dig.As with value groups: "+
+			`dig.As provided with group:"foo"`)
+	})
 }
 
 type testStruct struct{}
@@ -1985,6 +2158,33 @@ func testProvideFailures(t *testing.T, dryRun bool) {
 		)
 	})
 
+	t.Run("out returning multiple instances of the same type and As option", func(t *testing.T) {
+		c := New()
+		type A struct{ idx int }
+		type ret struct {
+			Out
+
+			A1 A // same type A provided three times
+			A2 A
+			A3 A
+		}
+
+		err := c.Provide(func() ret {
+			return ret{
+				A1: A{idx: 1},
+				A2: A{idx: 2},
+				A3: A{idx: 3},
+			}
+		}, As(new(interface{})))
+		require.Error(t, err, "provide must return error")
+		assertErrorMatches(t, err,
+			`cannot provide function "go.uber.org/dig".testProvideFailures\S+`,
+			`dig_test.go:\d+`, // file:line
+			`cannot provide interface {} from \[0\].A2:`,
+			`already provided by \[0\].A1`,
+		)
+	})
+
 	t.Run("provide multiple instances with the same name", func(t *testing.T) {
 		c := New(DryRun(dryRun))
 		type A struct{}
@@ -2069,6 +2269,46 @@ func testProvideFailures(t *testing.T, dryRun bool) {
 			`dig.out embeds \*dig.Out`,
 		)
 	})
+
+	t.Run("provide the same implemented interface", func(t *testing.T) {
+		c := New()
+		err := c.Provide(
+			func() *bytes.Buffer {
+				var buf bytes.Buffer
+				return &buf
+			},
+			As(new(io.Reader)),
+			As(new(io.Reader)),
+		)
+
+		require.Error(t, err, "provide must fail")
+		assert.Contains(t, err.Error(), "cannot provide io.Reader")
+		assert.Contains(t, err.Error(), "already provided")
+	})
+
+	t.Run("provide the same implementation with as interface", func(t *testing.T) {
+		c := New()
+		err := c.Provide(
+			func() *bytes.Buffer {
+				var buf bytes.Buffer
+				return &buf
+			},
+			As(new(io.Reader)),
+		)
+		require.NoError(t, err, "provide must not fail here")
+
+		err = c.Provide(
+			func() *bytes.Buffer {
+				var buf bytes.Buffer
+				return &buf
+			},
+			As(new(io.Reader)),
+		)
+
+		require.Error(t, err, "provide must fail")
+		assert.Contains(t, err.Error(), "cannot provide io.Reader")
+		assert.Contains(t, err.Error(), "already provided")
+	})
 }
 
 func TestInvokeFailures(t *testing.T) {
diff --git a/graph_test.go b/graph_test.go
index 1c1d4c1..99f994f 100644
--- a/graph_test.go
+++ b/graph_test.go
@@ -21,9 +21,11 @@
 package dig
 
 import (
+	"bytes"
 	"fmt"
 	"io"
 	"reflect"
+	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -59,11 +61,14 @@ func TestDotGraph(t *testing.T) {
 	type t2 struct{}
 	type t3 struct{}
 	type t4 struct{}
+	type t5 strings.Reader
 
 	type1 := reflect.TypeOf(t1{})
 	type2 := reflect.TypeOf(t2{})
 	type3 := reflect.TypeOf(t3{})
 	type4 := reflect.TypeOf(t4{})
+	type5 := reflect.TypeOf(t5{})
+	type6 := reflect.Indirect(reflect.ValueOf(new(io.Reader))).Type()
 
 	p1 := tparam(type1, "", "", false)
 	p2 := tparam(type2, "", "", false)
@@ -74,6 +79,8 @@ func TestDotGraph(t *testing.T) {
 	r2 := tresult(type2, "", "", 0)
 	r3 := tresult(type3, "", "", 0)
 	r4 := tresult(type4, "", "", 0)
+	r5 := tresult(type5, "", "", 0)
+	r6 := tresult(type6, "", "", 0)
 
 	t.Parallel()
 
@@ -92,6 +99,21 @@ func TestDotGraph(t *testing.T) {
 		assertCtorsEqual(t, expected, dg.Ctors)
 	})
 
+	t.Run("create graph with one constructor and as interface option", func(t *testing.T) {
+		expected := []*dot.Ctor{
+			{
+				Params:  []*dot.Param{p1},
+				Results: []*dot.Result{r5, r6},
+			},
+		}
+
+		c := New()
+		c.Provide(func(A t1) t5 { return t5{} }, As(new(io.Reader)))
+
+		dg := c.createGraph()
+		assertCtorsEqual(t, expected, dg.Ctors)
+	})
+
 	t.Run("create graph with multple constructors", func(t *testing.T) {
 		expected := []*dot.Ctor{
 			{
@@ -418,6 +440,19 @@ func TestVisualize(t *testing.T) {
 		VerifyVisualization(t, "named", c)
 	})
 
+	t.Run("dig.As two types", func(t *testing.T) {
+		c := New()
+
+		require.NoError(t, c.Provide(
+			func() *bytes.Buffer {
+				panic("this function should not be called")
+			},
+			As(new(io.Reader), new(io.Writer)),
+		))
+
+		VerifyVisualization(t, "dig_as_two", c)
+	})
+
 	t.Run("optional params", func(t *testing.T) {
 		c := New()
 
diff --git a/testdata/dig_as_two.dot b/testdata/dig_as_two.dot
new file mode 100644
index 0000000..1a07398
--- /dev/null
+++ b/testdata/dig_as_two.dot
@@ -0,0 +1,16 @@
+digraph {
+	rankdir=RL;
+	graph [compound=true];
+	
+		subgraph cluster_0 {
+			label = "go.uber.org/dig";
+			constructor_0 [shape=plaintext label="TestVisualize.func4.1"];
+			
+			"io.Reader" [label=<io.Reader>];
+			"io.Writer" [label=<io.Writer>];
+			
+		}
+		
+		
+	
+}
\ No newline at end of file
diff --git a/testdata/error.dot b/testdata/error.dot
index 8accfc6..c3904f9 100644
--- a/testdata/error.dot
+++ b/testdata/error.dot
@@ -11,7 +11,7 @@ digraph {
 	
 		subgraph cluster_0 {
 			label = "go.uber.org/dig";
-			constructor_0 [shape=plaintext label="TestVisualize.func6.1"];
+			constructor_0 [shape=plaintext label="TestVisualize.func7.1"];
 			color=orange;
 			"dig.t3[name=n3]" [label=<dig.t3<BR /><FONT POINT-SIZE="10">Name: n3</FONT>>];
 			"dig.t2[group=g2]0" [label=<dig.t2<BR /><FONT POINT-SIZE="10">Group: g2</FONT>>];
@@ -23,7 +23,7 @@ digraph {
 		
 		subgraph cluster_1 {
 			label = "go.uber.org/dig";
-			constructor_1 [shape=plaintext label="TestVisualize.func6.2"];
+			constructor_1 [shape=plaintext label="TestVisualize.func7.2"];
 			color=orange;
 			"dig.t4" [label=<dig.t4>];
 			
@@ -36,7 +36,7 @@ digraph {
 		
 		subgraph cluster_2 {
 			label = "go.uber.org/dig";
-			constructor_2 [shape=plaintext label="TestVisualize.func6.4"];
+			constructor_2 [shape=plaintext label="TestVisualize.func7.4"];
 			color=red;
 			"dig.t1[group=g1]0" [label=<dig.t1<BR /><FONT POINT-SIZE="10">Group: g1</FONT>>];
 			"dig.t2[group=g2]2" [label=<dig.t2<BR /><FONT POINT-SIZE="10">Group: g2</FONT>>];
diff --git a/testdata/grouped.dot b/testdata/grouped.dot
index 7dc4fb1..16fe7cf 100644
--- a/testdata/grouped.dot
+++ b/testdata/grouped.dot
@@ -8,7 +8,7 @@ digraph {
 	
 		subgraph cluster_0 {
 			label = "go.uber.org/dig";
-			constructor_0 [shape=plaintext label="TestVisualize.func5.1"];
+			constructor_0 [shape=plaintext label="TestVisualize.func6.1"];
 			
 			"dig.t3[group=foo]0" [label=<dig.t3<BR /><FONT POINT-SIZE="10">Group: foo</FONT>>];
 			
@@ -17,7 +17,7 @@ digraph {
 		
 		subgraph cluster_1 {
 			label = "go.uber.org/dig";
-			constructor_1 [shape=plaintext label="TestVisualize.func5.2"];
+			constructor_1 [shape=plaintext label="TestVisualize.func6.2"];
 			
 			"dig.t3[group=foo]1" [label=<dig.t3<BR /><FONT POINT-SIZE="10">Group: foo</FONT>>];
 			
@@ -26,7 +26,7 @@ digraph {
 		
 		subgraph cluster_2 {
 			label = "go.uber.org/dig";
-			constructor_2 [shape=plaintext label="TestVisualize.func5.3"];
+			constructor_2 [shape=plaintext label="TestVisualize.func6.3"];
 			
 			"dig.t2" [label=<dig.t2>];
 			
diff --git a/testdata/missing.dot b/testdata/missing.dot
index 4541df3..93097bc 100644
--- a/testdata/missing.dot
+++ b/testdata/missing.dot
@@ -4,7 +4,7 @@ digraph {
 	
 		subgraph cluster_0 {
 			label = "go.uber.org/dig";
-			constructor_0 [shape=plaintext label="TestVisualize.func7.1"];
+			constructor_0 [shape=plaintext label="TestVisualize.func8.1"];
 			color=orange;
 			"dig.t4" [label=<dig.t4>];
 			
diff --git a/testdata/optional.dot b/testdata/optional.dot
index f96eb30..949e96e 100644
--- a/testdata/optional.dot
+++ b/testdata/optional.dot
@@ -4,7 +4,7 @@ digraph {
 	
 		subgraph cluster_0 {
 			label = "go.uber.org/dig";
-			constructor_0 [shape=plaintext label="TestVisualize.func4.1"];
+			constructor_0 [shape=plaintext label="TestVisualize.func5.1"];
 			
 			"dig.t1" [label=<dig.t1>];
 			
@@ -13,7 +13,7 @@ digraph {
 		
 		subgraph cluster_1 {
 			label = "go.uber.org/dig";
-			constructor_1 [shape=plaintext label="TestVisualize.func4.2"];
+			constructor_1 [shape=plaintext label="TestVisualize.func5.2"];
 			
 			"dig.t2" [label=<dig.t2>];
 			
diff --git a/testdata/prune_constructor_result.dot b/testdata/prune_constructor_result.dot
index 0be24f9..1c31bd6 100644
--- a/testdata/prune_constructor_result.dot
+++ b/testdata/prune_constructor_result.dot
@@ -7,7 +7,7 @@ digraph {
 	
 		subgraph cluster_0 {
 			label = "go.uber.org/dig";
-			constructor_0 [shape=plaintext label="TestVisualize.func6.6.1.2"];
+			constructor_0 [shape=plaintext label="TestVisualize.func7.6.1.2"];
 			color=orange;
 			"dig.t4" [label=<dig.t4>];
 			
@@ -18,7 +18,7 @@ digraph {
 		
 		subgraph cluster_1 {
 			label = "go.uber.org/dig";
-			constructor_1 [shape=plaintext label="TestVisualize.func6.6.1.3"];
+			constructor_1 [shape=plaintext label="TestVisualize.func7.6.1.3"];
 			color=red;
 			"dig.t2[group=g2]1" [label=<dig.t2<BR /><FONT POINT-SIZE="10">Group: g2</FONT>>];
 			
diff --git a/testdata/prune_non_root_nodes.dot b/testdata/prune_non_root_nodes.dot
index 20dbcca..7c66293 100644
--- a/testdata/prune_non_root_nodes.dot
+++ b/testdata/prune_non_root_nodes.dot
@@ -4,7 +4,7 @@ digraph {
 	
 		subgraph cluster_0 {
 			label = "go.uber.org/dig";
-			constructor_0 [shape=plaintext label="TestVisualize.func6.6.2.2"];
+			constructor_0 [shape=plaintext label="TestVisualize.func7.6.2.2"];
 			color=red;
 			"dig.t4" [label=<dig.t4>];