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

Related Issues:
This might relate
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]
@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"
}
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"
}
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.
Most helpful comment
There's an issue asking for a version of
as Tthat is just "check assignability toTbut 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