34

I just want a type like this one:

type ObjectWithAnyKey = { [key: string]: string };

with all keys allowed except a foo key.

How to do that?

B. Branchard
  • 786
  • 1
  • 10
  • 21
  • Have you tried `type OmitFoo = Omit;` – ocheriaf Oct 28 '19 at 16:04
  • 2
    @ocheriaf `Omit` is the same as `ObjectWithAnyKey`. (There are no true [subtraction types](https://github.com/microsoft/TypeScript/issues/4183) in TypeScript, so `Exclude` is just `string`. You can remove elements from explicit unions, but not remove elements from things like `string`.) – jcalz Oct 28 '19 at 16:18

4 Answers4

38

I think in this case you can use the following definition:

type ObjectWithAnyKeyExceptFoo = {
  [key: string]: string
} & { foo?: never };

The type {foo?: never} has an optional property named foo, whose type is never (which actually is the same as undefined for optional properties). So if you have a property named foo, it can't have a defined value. And actually since undefined & string is never, you can't have a foo property at all. Let's make sure it works:

function acceptAnyKeyAcceptFoo(obj: ObjectWithAnyKeyExceptFoo) { }

acceptAnyKeyAcceptFoo({}); // okay
acceptAnyKeyAcceptFoo({ a: "123" }); // okay
acceptAnyKeyAcceptFoo({ a: "123", foo: "oops" }); // error!
//  ----------------------------> ~~~
// Type 'string' is not assignable to type 'undefined'.
acceptAnyKeyAcceptFoo({ a: "123", foo: undefined }); // error!
//  ----------------> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Type 'undefined' is not assignable to type 'string'.

Looks good.


More technical stuff below:

Note that using the intersection (&) here is a little strange, and maybe even cheating a little bit. With an index signature, manually specified properties like foo are required to have values that are assignable to their index signature property type also. The following doesn't work because it violates that:

type BadDefn = {
  [key: string]: string;
  foo?: never; // error!
//~~~ <-- undefined is not assignable to string
}

Since foo may be undefined, but string is not, the compiler is unhappy. Never mind that most values from the index signature actually will be undefined and not string (e.g., const obj: ObjectWithAnyKeyExceptFoo = {a: "123"} has an a property of type string, but what if you access its b property? You will get undefined at runtime, but the compiler says string). But that's the way index signatures are, I guess.

The intersection definition is essentially the same thing as the disallowed type above, except that it sidesteps the compiler error. There are often issues with doing this intentionally to violate the index signature (see this question), but we don't actually want to use that violating property. We don't want foo properties at all, and since the compiler sees the foo property as string & undefined, which is never, it's actually better for this use case.

Non-violating single types like these can be made:

type OkayDefnButPossiblyUndefined = {
  [key: string]: string | undefined;
  foo?: never;
}

This one is actually reasonable if you want to represent that obj.b is undefined and not string, but might not be the best fit if you like the current index signature behavior. There's also this one:

type AlsoOkayButRequiresFoo = {
  [key: string]: string;
  foo: never;
}

which is worse, since foo becomes a required property and it's hard to actually initialize something of this type.

End technical stuff


Okay, hope that helps. Good luck!

Link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • What an answer! Thanks! For some reason: ` type acceptAnyKeyExceptFoo = { [key: string]: any, foo?: never } ` works for me. – B. Branchard Oct 28 '19 at 16:27
  • 1
    Yes, `any` turns off type checking. I assumed you wanted the values to be strings. – jcalz Oct 28 '19 at 16:35
  • This works from this point of view, but it doesn't throw an error if you try to *access* the value, e.g. `const x: ObjectWithAnyKeyExceptFoo = {} as ObjectWithAnyKeyExceptFoo; console.log(x.foo);` --- should throw an error when trying to access x.foo, but it does not. – brandonscript Aug 29 '23 at 21:36
11

Use the Omit keyword in TypeScript (3.5+)

type ObjectWithAnyKey = {
  [key: string]: string
}

type ObjectWithAnyKeyButFoo = Omit<ObjectWithAnyKey, 'foo'> 

For excluding multiple properties:

type ObjectWithAnyKeyButFooOrBar = Omit<ObjectWithAnyKey, 'foo' | 'bar'>

For redefining specific properties:

type ObjectWithAnyKeyAndUniqueFooBar = Omit<ObjectWithAnyKey, 'foo' | 'bar'> & {
  bar: number
  foo: Record<string, number>
}
Gibolt
  • 42,564
  • 15
  • 187
  • 127
1

The answer by jcalz is fantastic, definitely worth a detailed read. In my case I want to do this pattern in many places and I enjoy advanced types, hence I wrote this advanced type that implements jcalz's answer via a type that you can export and use freely wherever.

type keyOmit<T, U extends keyof any> = T & { [P in U]?: never }

As with above the same behaviour is observed:

type ObjectWithAnyKeyExceptFoo = keyOmit<{ [key: string]: string }, "foo">

function acceptAnyKeyAcceptFoo(obj: ObjectWithAnyKeyExceptFoo) {}

acceptAnyKeyAcceptFoo({}) // okay
acceptAnyKeyAcceptFoo({ a: "123" }) // okay
acceptAnyKeyAcceptFoo({ a: "123", foo: "oops" }) // error!
//  ----------------------------> ~~~
// Type 'string' is not assignable to type 'undefined'.
acceptAnyKeyAcceptFoo({ a: "123", foo: undefined }) // error!
//  ----------------> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Type 'undefined' is not assignable to type 'string'

Also note that you can use this specific type the same as the built in 'Omit' type so you can omit many keys with this (my particular use case)

type ObjectWithAnyKeyExceptFooOrBar = keyOmit<{ [key: string]: string }, "foo" | "bar">

function acceptAnyKeyAcceptFooOrBar(obj: ObjectWithAnyKeyExceptFooOrBar) {}

acceptAnyKeyAcceptFooOrBar({}) // okay
acceptAnyKeyAcceptFooOrBar({ a: "123" }) // okay
acceptAnyKeyAcceptFooOrBar({ a: "123", foo: "oops" }) // error!
//  ---------------------------------> ~~~
// Type 'string' is not assignable to type 'undefined'.
acceptAnyKeyAcceptFooOrBar({ a: "123", bar: "oops" }) // error!
//  ---------------------------------> ~~~
// Type 'string' is not assignable to type 'undefined'.
acceptAnyKeyAcceptFooOrBar({ a: "123", foo: "oops", bar: "2nd oops" }) // error!
//  ---------------------------------> ~~~          ~~~
Pier-Luc Gendreau
  • 13,553
  • 4
  • 58
  • 69
Jamie Shepherd
  • 1,039
  • 9
  • 15
-1

https://learn.microsoft.com/en-us/javascript/api/@azure/keyvault-certificates/requireatleastone?view=azure-node-latest

  • [K in keyof T]-? this property (K) is valid only if it has the same name as any property of T.
  • Required<Pick<T, K>> makes a new type from T with just the current property in the iteration, and marks it as required
  • Partial<Pick<T, Exclude<keyof T, K>>> makes a new type with all the properties of T, except from the property K.
  • & is what unites the type with only one required property from Required<...> with all the optional properties from Partial<...>.
  • [keyof T] ensures that only properties of T are allowed.

Code:

type RequireAtLeastOne<T> = {
  [K in keyof T]-?: Required<Pick<T, K>> &
    Partial<Pick<T, Exclude<keyof T, K>>>;
}[keyof T];
  • Please do not just drop off code that you've found in some random URL. Explain what you're doing and provide the specific examples to solve the problem in context. – R. Karlus Aug 23 '23 at 20:23