Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

collections: most functions that work on Record do not work with interface #1126

Closed
Tracked by #4647
zhangyuannie opened this issue Aug 12, 2021 · 7 comments
Closed
Tracked by #4647
Labels
bug Something isn't working needs triage

Comments

@zhangyuannie
Copy link
Contributor

zhangyuannie commented Aug 12, 2021

Most of collections' functions that work on Record do not work with interface

To Reproduce

import { deepMerge, filterEntries } from "https://deno.land/std/collections/mod.ts";

interface Foo {
  bar: string;
}

const foo1: Foo = { bar: "deno" };
const foo2: Foo = { bar: "deno" };

const a = deepMerge(foo1, foo2);
const b = filterEntries(foo1, ([key]) => key !== "a");

Expected behavior

It compiles.

Current behaviour

It does not compile.

Argument of type 'Foo' is not assignable to parameter of type 'Record<PropertyKey, unknown>'.
      Index signature is missing in type 'Foo'

Desktop (please complete the following information):

deno 1.13.0 (release, x86_64-unknown-linux-gnu)
v8 9.3.345.11
typescript 4.3.5

Additional context
Possible workaround is to use type instead of interface, but that's not ideal:

type Foo = {
  bar: string;
}
@zhangyuannie zhangyuannie added bug Something isn't working needs triage labels Aug 12, 2021
@zhangyuannie zhangyuannie changed the title collections: deepMerge typing is limited collections: most functions that work on Record do not work with interface Aug 12, 2021
@LionC
Copy link
Contributor

LionC commented Aug 15, 2021

I think we could fix this with Partial Records. I will take a look at fixing it.

@LionC
Copy link
Contributor

LionC commented Aug 15, 2021

Having read into this, I realized that have been down that rabbit hole before. I do not think there is an easy fix for this without breaking the type promise of the functions.

The problem is that interfaces can be extended and added onto, which makes the index signature that we rely on to have a proper return type very unsafe.

Given

interface Human { age: number }
interface Developer extends Human { usesVim: boolean }

you would expect that passing a Human as a Record<string, number> should be fine when in fact that Human could be a Developer with non-number values.

This is generally one of the many reasons you should use type aliases over interfaces by default if possible, as they provide stronger guarantees because they can neither be extended nor added onto by arbitrary libraries.

There are hacks to pass this - the least ugly of them being spread ({ ...human }) fooling the compiler into accepting it - but this ultimately just hides the actual problem.

I do not see a fix for this if we want type signatures to stay helpful.

@zhangyuannie
Copy link
Contributor Author

zhangyuannie commented Aug 16, 2021

I agree with most, however 2 small points:

  • Prefer type over interface is highly debatable. [1][2]
  • type can also be extended: type Developer = Human & { usesVim: boolean }

There isn't an elegant solution, but we can try to augment the type of functions that one might use on interfaces to help our users.

@getspooky
Copy link
Contributor

@zhangyuannie Something that can help considerably with this issue is to use the following type:

type Typify<T> = { [ K in keyof T ]: T[ K ] };

Example :

import { deepMerge, filterEntries } from "https://deno.land/std/collections/mod.ts";

interface Foo {
  bar: string;
}

type Typify<T> = { [ K in keyof T ]: T[ K ] };

const foo1: Typify<Foo> = { bar: "deno" };
const foo2: Typify<Foo> = { bar: "deno" };

const a = deepMerge(foo1, foo2);
const b = filterEntries(foo1, ([key]) => key !== "a");

@LionC
Copy link
Contributor

LionC commented Aug 18, 2021

I agree with most, however 2 small points:

  • Prefer type over interface is highly debatable. [1][2]
  • type can also be extended: type Developer = Human & { usesVim: boolean }

There isn't an elegant solution, but we can try to augment the type of functions that one might use on interfaces to help our users.

That is definitely debatable :-)

I do not think there is a reasonable solution on the API side though, so I would vote to close this issue.

@lino-levan
Copy link
Contributor

I think this issue should probably be closed, though the discussion in this issue is very interesting.

@iuioiua
Copy link
Contributor

iuioiua commented May 8, 2024

Interfaces tend to be used for objects with well-defined shapes, while records tend to be used for objects that are more generic, which aligns with the std/collections' design. I'll close this as I think this situation is fine, and there doesn't appear to be an apparent desire to change that. Either way, thank you for the suggestions and input, everyone!

@iuioiua iuioiua closed this as not planned Won't fix, can't repro, duplicate, stale May 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working needs triage
Projects
None yet
Development

No branches or pull requests

5 participants