0

How would I define an interface for such objects:

let bob = {
  Bob: {
    age: 9,
    gender: 'M'
  }
};
let alice = {
  Alice: {
    age: 12,
    gender: 'F'
  }
};

I could use solution from this thread but it is too permissive, I want to restrict that use case to a unique (or fixed number of) key(s).

BONUS if you can figure out how to express:

  • how many keys are allowed in the top level object
  • specify a different interface per key

Something like this pseudo code:

interface funnyEdge {
    <string>from: fromEdge,
    <string>to: toEdge
}
let edge: funnyEdge = {foo: {...}, bar: {...}}
3
  • I think what you are trying to do is very similar to the dictionary as you mentioned, and controlling number of keys is a runtime concept which an static type system cannot enforce. Commented Aug 16, 2017 at 17:07
  • interface foo { bar: baz } does enforce a specific number of keys no? Commented Aug 16, 2017 at 17:33
  • 1
    It's less that it enforces a specific number of keys and more that it enforces only having a field named bar of type baz. You can't say "this object only has one field of type baz, but I don't know what the name of that key is right now ". Commented Aug 16, 2017 at 17:49

4 Answers 4

3

The type system in TypeScript doesn't allow you to specify a type with a specified number of unspecified keys. In fact, since it lacks exact types, TypeScript doesn't really allow you to specify a type with a specified number of specified keys, either. It only does excess property checking on object literals. As a type, something like:

type Person = {
  age: number,
  gender: 'M' | 'F' | 'O'
}

does not mean a Person can't have other keys, as in:

const pirate = {
  age: 35,
  gender: 'M' as 'M',
  legs: 1,
  parrot: true
};
const piratesArePeopleToo: Person = pirate; // no problem

So the most you could hope for would be that TypeScript complains if you pass in an object literal with too many keys, as in:

const otherPirate: Person = {
  age: 40,
  gender: 'F',
  eyes: 1,
  parrot: false
}; // error

And no, TypeScript doesn't let you express that constraint, as far as I know.


The best I could think of would be a workaround: create a function which takes a single key and a Person, and returns an object of the type you want:

type NamedPerson<K extends string> = Record<K, Person>;
function makeNamedPerson<K extends string>(key: K, person: Person): NamedPerson<K> {
  let namedPerson = {} as NamedPerson<K>;
  namedPerson[key]=person;
  return namedPerson;
}
const namedPerson = makeNamedPerson('chris', { age: 10, gender: 'O' });
console.log(namedPerson.chris.age);

If you organize your code so the only reasonable way to get an object of type NamedPerson<K> is to call the function makeNamedPerson() (e.g., export only the function and make the type it produces a class with a private member), then you know that each one will have a single key. This isn't quite as good for you as being enforced by the type system, but at least you make it difficult for users of your library to circumvent the constraint.

Hope that helps; good luck!

Sign up to request clarification or add additional context in comments.

Comments

1

Like this?

interface Person {
  age: number;
  gender: "M" | "F";
}

interface Bob extends Person { 
  //...
}

interface Alice extends Person { 
  //...
}

interface Members {
  Bob?: Bob;
  Alice?: Alice;
}

let bob: Members = {
  Bob: {
    age: 9,
    gender: 'M'
  }
};
let alice: Members = {
  Alice: {
    age: 12,
    gender: 'F'
  }
};

alert(alice.Alice.age) // ok
alice.Bob = bob.Bob // ok
alert(alice.Eve.age) // error

Try it in the Playground

Comments

1

How about this?

type Person<TName extends string> = {
  [key in TName]: {
    age: number,
    gender: 'M'|'F'
  }
};

var alice: Person<"Alice">;
alice.Alice.age = 42;

alice.AnythingElse // error;

And multiple keys:

var johnAndMary: Person<"John" | "Mary">;
johnAndMary.John.age;
johnAndMary.Mary.gender;
johnAndMary.Don // error

Comments

0

Turns out there now is a way to do this:

type Peeps = 'Bob'|'Alice';
type Def = { ... };
type Person = {[name in Peeps]?: Def }
// or if you're so inclined
type Person = Map<Peeps,Def>

// here with the other example
type EdgeEnd = 'from'|'to';
type FunnyNode = { ... };
type FunnyEdge = {[end in EdgeEnd]: FunnyNode};

2 Comments

How does this answer your original question? If you know at design time the exact keys you will accept, you can use Record<Peeps, Def>. But that's not what your question appears to be asking. As stated, it asked how to use a fixed number of runtime-specified keys, which is still impossible.
I edited the answer to show that it fits the bill. It says in the question unique keys which in my confused mind at the time probably meant pre defined but I agree the question is badly worded.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.