2

What i want

I have a type with known values

type Possibilities = "a" | "b" | "c"

And want to be able to create object that can only have keys that belongs to that type, but not necessarily all of then.

And also prevent each prop's type from outputing as string | undefined img

As it is meant to be used on constants, that have fixed values. And TS can already infer proper typing: img

So, it should accept:

const mine = {
    a: "this",
    b: "and",
    c: "that"
}
const yours = {
    b: "it"
}

But should not:

// not known key ( 'x' )
const theirs = {
    a: "these",
    x: "those"
}
// preferably - empty object
const noOnes = {}

I've tryed:

  • If i do one of bellow

    const mine: Record<Possibilities, string>;
    const mine: {[key in Possibilities]: string};
    

    it says the obj must have all the keys. Wrong.

  • If i do one of

    const mine: Partial<Record<Possibilities, string>>;
    const mine: {[key in Possibilities]?: string};
    

    it allow me to set only some, however, types gets out as string | undefined. Even if "casting" object as const.

  • Pick<> or Omit<>, for every single "instance" created, makes the desired result, but it's a pain in the ass, and kind of repeating myself (need to write "a", "b"... on type and again on value). img Also, at least here, Omit<> is not even auto-completing the possible values. Making it even harder and/or error prone when using most of possibilities, by needing to pick a lot or omit 'blindly'.

I got a feeling that the solution is somewhere close by using a generic that may extend the type... But couldn't figure it out.

--- EDIT ---

@tokland comment took me to an "almost there" solution

type AtLeastOne<T, U = {[K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U]

type Possibilities = "a" | "b" | "c"

let mine: AtLeastOne<Record<Possibilities, string>>;

This enforces the object to have a, b or c prop. Forbids it to have unknown props. And correctly outputs prop's type as not "possibly undefined"

img img img

But it breaks apart when multiple allowed props are set

img

  • It's not clear what you mean by "types get out as ... | partial though i've set the value". Maybe this https://devblogs.microsoft.com/typescript/announcing-typescript-4-4-beta/#exact-optional-property-types ? Some code on https://www.typescriptlang.org/play would be helpful. – tokland Jul 01 '22 at 19:19
  • I mean, if i set a `const x: Partial<...> = {a="abc"}` the type of x.a is `string | undefined` (in VS Code at least) because it's been set to be optional. That's not what i want. I want it o "inter" type just as if not type was set. But limit with props are allowed on atribution – Jonatas Amaral Jul 01 '22 at 21:06
  • Does this answer your question? [typescript interface require one of two properties to exist](https://stackoverflow.com/questions/40510611/typescript-interface-require-one-of-two-properties-to-exist) – David P. Caldwell Jul 01 '22 at 23:08
  • I flagged this as a duplicate. I'll link the answer. It's not pretty. https://stackoverflow.com/questions/40510611/typescript-interface-require-one-of-two-properties-to-exist – David P. Caldwell Jul 01 '22 at 23:09
  • 1
    Also related: https://stackoverflow.com/a/48244432/188031 – tokland Jul 02 '22 at 10:00
  • @DavidP.Caldwell, it is not related at all – Jonatas Amaral Jul 13 '22 at 20:23
  • @tokland, nice. That typing trickery ALMOST thie the thing. `const mine: AtLeastOne>` does output the type of prop (say mine.c) as `string` only. But it breaks if more than one props is set, and all become `string | undefined` – Jonatas Amaral Jul 13 '22 at 20:28

3 Answers3

2

I think this is what you are trying to accomplish:

// Here you can include all the keys you want
enum Possibilities {
    a = "a",
    b = "b",
    c = "c",
}

type OtherType = {
    [key in Possibilities]?: {}; // here the sign ? allows you to set the keys as optional
}
// valid
let A: OtherType = {
    a: {}
}
// valid
let C: OtherType = {
    a: {},
    b: {},
    c: {},
}

// also valid
let B: OtherType = {}

you can check this for more explanation about the topic

  • Hey, as stated in the "I've tryed" part, `Partial<...>` (or `{[...]?:...}`) outputs for each props' type as `... | undefined` https://imgur.com/ui13Xvq I want it's type to be "as required". Just the variable not needing to set ALL the keys – Jonatas Amaral Jul 13 '22 at 20:08
2

With TypeScript 4.9 and above you can achieve this with the satisfies operator.

This allows you to ensure a value satisfies a type, without applying that type to it.

const mine = {
  a: 'this',
  b: 'that',
} satisfies Partial<Record<Possibilities, string>>;

If you use this with as const, you can even get the exact string values as their type, rather than just string, and make all the keys readonly.

E.g. in the below example the type of mine.a is 'this', and it cannot be modified.

const mine = {
  a: 'this',
  b: 'that',
} as const satisfies Partial<Record<Possibilities, string>>;
JakeSidSmith
  • 819
  • 6
  • 12
0

You should be able to use Conditional Types for this:

type Possibilities = "a" | "b" | "c"

type OnlyPossibilities<P extends string | number | symbol, T> = T extends {
  [key in P]?: any
} ? T : never

type Foo = OnlyPossibilities<Possibilities, {
  a: number
}>

type Bar = OnlyPossibilities<Possibilities, {
  d: number
}>

(Link to the TS Playground)

Here I am declaring a helper type which accepts both the possibilities, P, and the type you want to check, T. Foo will correctly be of type:

type Foo = { a: number }

...but Bar will be:

type Bar = never

thus "not allowing any values".

You can also get rid of P, I just used it to make the helper type customizable:

type Possibilities = "a" | "b" | "c"

type OnlyPossibilities<T> = T extends {
  [key in Possibilities]?: any
} ? T : never

If you have any banned, you should be able to use unknown instead.

Lonami
  • 5,945
  • 2
  • 20
  • 38