Typescript: keyof typeof Partial Record

Created on 3 Jan 2020  路  9Comments  路  Source: microsoft/TypeScript

TypeScript Version: 3.5.1


Search Terms:
Partial, keyof, typeof, Record, nested object

As a part of a design system I'm making, I want to create a list of sizes that a component may define for itself.
Say I have a list of small, medium, large.
Now I want to define an object mapping one the above sizes to the actual size like

const sizes = ['small', 'medium', 'large'] as const;

const buttonSize = {
  small: '20px',
  large: '40px'
}

I want that object to enforce using keys from the list of sizes, and then to enforce the consumer of the property to pass only defined sizes (that is small and large).

The best I came up with is to say that buttonSize is an object built from some of sizes.
This however could not limit the option for the consumer (see below snippet).
Seems like trying to get the keys of the buttonSize was just delegating you to size.

Either there is an issue here, or I completely misunderstood this usage of the types.

Code

import React, { FunctionComponent } from 'react';

const sizes = ['small', 'medium', 'large'] as const;

const buttonSize: Partial<Record<typeof sizes[number], any>> = {
  small: '20px',
  large: '20px'
} as const;

interface IButtonProps {
  size: keyof typeof buttonSize;
}

const Button: FunctionComponent<IButtonProps> = () => ();

<Button size={...} />;

Expected behavior:
Only small | large should be allowed in size property (not allowing medium)

Actual behavior:
Every one of the sizes is allowed (small | medium | large).
image

Related Issues:
This might relate

Question

Most helpful comment

There's an issue asking for a version of as T that is just "check assignability to T but do not widen". I can't remember the issue title or number, though

[Edit]

https://github.com/microsoft/TypeScript/issues/31062

https://github.com/microsoft/TypeScript/issues/33262

All 9 comments

Why do you think only test2 should be allowed?

typeof b is { test? : any, test2? : any }.

And keyof { test? : any, test2? : any } is "test"|"test2"

Hmm, I was going to say it looks like you expect b to be narrowed upon assignment, which doesn't happen because Partial<...> is not a union type (which would be related to #8513 and specifically this comment). But what you want isn't even narrowing, because you presumably expect the assignment to widen b from { test? : any, test2? : any} to {test2?: any}, which just won't happen. It would possibly make sense to narrow to {test1?: never , test2?: any}, but keyof that type is stil "test1"|"test2".

Or, another way of looking at this: you don't want to annotate b as Partial<...>. Rather you want to ensure it's assignable to Partial<...> while actually keeping its type as what the compiler would infer without the annotation. So why not do that?

const a = ['test', 'test2'] as const;

const b = { // no annotation
  test2: 'ads',
} as const;

interface Props {
  size: keyof typeof b; // "test2"
}

// ensure assignability later, if it matters
const ifItMatters: Partial<Record<typeof a[number], any>> = b; // okay

@AnyhowStep @jcalz
I updated the post to better explain my use-case and reason.

This is all working as intended; it's not a bug in the compiler.

If Bar is not a union type, the assignment const foo: Bar = baz; results in typeof foo being Bar, no matter what baz is. In particular, typeof Foo is not changed to typeof baz.

In your updated example, that means typeof buttonSize is Partial<Record<typeof sizes[number], any>>, and keyof Partial<Record<typeof sizes[number], any>> is just typeof sizes[number]. If you don't like that, don't annotate buttonSize.

I showed one way to avoid that issue before with b; here's another way with buttonSize:

const sizes = ['small', 'medium', 'large'] as const;

// helper function that ensures assignability without type annotation
const asButtonSizeObj = <T extends Partial<Record<typeof sizes[number], any>>>(t: T) => t;

// buttonSize will be of type `{small: string, large: string}` now
const buttonSize = asButtonSizeObj({
  small: '20px',
  large: '20px'
});

interface IButtonProps {
  size: keyof typeof buttonSize; // "small" | "large"
}

There's an issue asking for a version of as T that is just "check assignability to T but do not widen". I can't remember the issue title or number, though

[Edit]

https://github.com/microsoft/TypeScript/issues/31062

https://github.com/microsoft/TypeScript/issues/33262

@jcalz Isn't one of the ideas of Partial is to say "some of the keys are required", and the extend will say "you must use all keys, but you can also add some"?
So I don't really understand why it worked.

The only downside to the asButtonSizeObj() workaround is this,

const sizes = ['small', 'medium', 'large'] as const;

// helper function that ensures assignability without type annotation
const asButtonSizeObj = <T extends Partial<Record<typeof sizes[number], any>>>(t: T) => t;

// buttonSize will be of type `{small: string, large: string}` now
const buttonSize = asButtonSizeObj({
  small: '20px',
  large: '20px',
  extraProp0 : "boo",
});

interface IButtonProps {
  //"extraProp0" probably unintended
  size: keyof typeof buttonSize; // "small" | "large" | "extraProp0"
}

Playground


There's a workaround for that downside,

const sizes = ['small', 'medium', 'large'] as const;

// helper function that ensures assignability without type annotation
const asButtonSizeObj = <T extends Partial<Record<typeof sizes[number], any>>>(t: T) : (
  {
    [k in Extract<keyof T, typeof sizes[number]>] : T[k]
  }
) => t;

// buttonSize will be of type `{small: string, large: string}` now
const buttonSize = asButtonSizeObj({
  small: '20px',
  large: '20px',
  extraProp0 : "boo",
});

interface IButtonProps {
  size: keyof typeof buttonSize; // "small" | "large"
}

Playground

But these workarounds inside workarounds just make me uneasy.

Another workaround is to forbid these extra properties (essentially, add excess prop checks again, which generics remove), but I find that workaround is pretty brittle

@AnyhowStep Do you mind sending me material about how you've done this, and why isn't keyof typeof Partial<Record<...>> enough?

Another workaround is to forbid these extra properties (essentially, add excess prop checks again, which generics remove), but I find that workaround is pretty brittle

Mind showing me how this is done? This is exactly the last piece I'm missing.

This issue has been marked as 'Question' and has seen no recent activity. It has been automatically closed for house-keeping purposes. If you're still waiting on a response, questions are usually better suited to stackoverflow.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

rwyborn picture rwyborn  路  210Comments

xealot picture xealot  路  150Comments

quantuminformation picture quantuminformation  路  273Comments

yortus picture yortus  路  157Comments

RyanCavanaugh picture RyanCavanaugh  路  205Comments