Skip to content

keyof typeof Partial Record #35981

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
AmirTugi opened this issue Jan 3, 2020 · 9 comments
Closed

keyof typeof Partial Record #35981

AmirTugi opened this issue Jan 3, 2020 · 9 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@AmirTugi
Copy link

AmirTugi commented Jan 3, 2020

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

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Jan 3, 2020

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"

@jcalz
Copy link
Contributor

jcalz commented Jan 3, 2020

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

@AmirTugi
Copy link
Author

AmirTugi commented Jan 3, 2020

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

@jcalz
Copy link
Contributor

jcalz commented Jan 3, 2020

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"
}

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Jan 3, 2020

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]

#31062

#33262

@AmirTugi
Copy link
Author

AmirTugi commented Jan 3, 2020

@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.

@AnyhowStep
Copy link
Contributor

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

@AmirTugi
Copy link
Author

AmirTugi commented Jan 4, 2020

@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.

@RyanCavanaugh RyanCavanaugh added the Question An issue which isn't directly actionable in code label Jan 8, 2020
@typescript-bot
Copy link
Collaborator

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

5 participants