Roaders
Roaders

Reputation: 4545

How to define a type that can only use property names from a string union

I have an Enum:

enum SomeEnumIds {
  nameOne = 0,
  nameTwo = 1,
  nameThree = 2,
  nameFour = 3,
}

and a derived Type:

export type EnumNames = keyof typeof SomeEnumIds;

and I want to define a type something like this:

export type EnumValues = {
    nameOne: string;
    nameTwo: boolean;
    nameThree: Date
}

but I want to ensure that all the property names are of type EnumNames.

Does anyone have any suggestions?

I tried this:

export interface EnumValues extends Record<EnumNames, any> {
    nameOne: string;
    nameTwo: boolean;
    nameThree: Date
}

but while this results in a compile error:

export interface EnumValues extends Record<string, Date> {
    nameOne: string;
}

this does not:

export interface EnumValuesThree extends Record<EnumNames, any> {
    notValid: string;
}

The ultimate goal is to have a class like this:

class SomeClass<TypeLookup extends Partial<Record<EnumNames, any>>>{

  getValue<T extends EnumNames>(name: T): TypeLookup[T]{
    return {} as any;
  }
}

where a type is passed that defined the return types of the getValue function. It should only be possible to define known property values on that type but it must be possible to pass in a partial type.

Upvotes: 1

Views: 38

Answers (1)

Alex Wayne
Alex Wayne

Reputation: 187144

Preventing extra properties can be tricky, and Typescript doesn't like to do it.

I think this may be your best bet:

class SomeClass<TypeLookup extends Partial<Record<EnumNames, unknown>>>{
  getValue<T extends keyof TypeLookup & EnumNames>(name: T): TypeLookup[T]{
    return {} as any;
  }
}

Now this works as expected:

new SomeClass<{ nameOne: string }>().getValue('nameOne') // fine
new SomeClass<{ nameOne: string }>().getValue('nameThree') // error
new SomeClass<{ notValid: string }>() // error

All I changed with the constraint of T in getValue. Now it must be a key of the TypeLookup generic parameter, intersected with the keys of the original enum. This means T will always be a subset of SomeEnumIds keys and the keys of type passed in.


One caveat though is that the extra keys are only reject if passed in directly. But because getValue is constrained to the keys of the original enum, you won't be able to access those properties with the getValue method.

type SomeType = { nameOne: string, notValid: string }
new SomeClass<SomeType>().getValue('nameOne') // fine
new SomeClass<SomeType>().getValue('notValid') // error

This is probably the best your going to get since I think at best you could make new SomeClass<SomeType> resolve to never with some clever exact types but that would only throw type errors downstream from that.

Playground

Upvotes: 1

Related Questions