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

Records with branded keys (poor man's nominal typing) do not verify the keys and fail when used with noImplicitAny #15746

Closed
nomcopter opened this issue May 10, 2017 · 3 comments
Labels
Duplicate An existing issue was already created

Comments

@nomcopter
Copy link

TypeScript Version: 2.3

Code

Note: Be sure to enable noImplicitAny
Typescript Playground

type NominalString = string & { __nomimalStringBrand: any } // Brand for nominal typing

const nominal = 'nominal' as NominalString;

let nominalRecord: Record<NominalString, string> = {};

nominalRecord = {
    normal: 'normal' // Should be an error
};

nominalRecord = {
    [nominal]: 'nominal'
};

nominalRecord['normal']; // Should be an error (even without noImplicitAny)

nominalRecord[nominal]; // Should work (even with noImplicitAny)

Expected behavior:
Lines commented as should be an error, should be. Lines commented as should work, should not be an error.

Actual behavior:
Compile time errors do not match with expectations.

Related Issues: #202

@mhegazy
Copy link
Contributor

mhegazy commented May 16, 2017

The mapped type Record<K, T> creates a type with keys in K with type T. if K is a set of literal types then each entry translates to a property name. there is a special case for the string type to add a n index signature on the resulting type to T.

The behavior in the sample above is not correct, since the compiler does not detect that it is the string type because of the intersection, and then does nothing. So it should behave like Record<string, string> i.e. { [x:stirng]: T }.

The current behavior or Record takes the known literal types in K, which in this case is the empty set and creates a type for you, and the resulting type is {}. That is why nominalRecord = { normal: 'normal' } works, cause { normal: 'normal' } is assignable to {}; as a matter of fact, any thing is assignable to {}, so you could also have written nominalRecord = 1 or nominalRecord = "my other string"; and you would not have gotten errors. see more about why this happens in the FAQs.

I do not think this is what you were looking for anyways, you were looking for restricting the mapping on an object to a subtype of string (tagged string), that is not a literal type. this has been discussed before in #1778
and #2491 (disregard the enum part of the request).

We have discussed allowing indexing with some subtype of number or string; but we could not come up with a scheme that make sense; given that:

  1. all types in TS are open, i.e. you can add more properties, e.g.:
var y = {a: 1, b:2};
var x: {a: number} = y; // OK, since y has an "a" that is a number

and 2. that all JS objects are maps by definition and you can always index into an object using any good old string.

@mhegazy mhegazy added the Duplicate An existing issue was already created label May 16, 2017
@mhegazy
Copy link
Contributor

mhegazy commented Jun 2, 2017

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

@mhegazy mhegazy closed this as completed Jun 2, 2017
@AlexGalays
Copy link

Can we reopen this?

I'm facing a similar issue with TS 2.6.2 or 2.8.0-dev:

type UserId = string & { __isUserId: true }
type User = { id: UserId, name: string }
type Users = Record<UserId, User>

var id = 0
function nextId() {
  return '' + id++ as UserId 
}

const users = {} as Users
const joe = { id: nextId(), name: 'Joe' }

// Error: Users have no index signature
users[joe.id] = joe

I think this is quite a common use case nowadays as using strings everywhere lacks semantic/meaning/type safety.

@microsoft microsoft locked and limited conversation to collaborators Jul 3, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

3 participants