Reputation: 1128
I have interface and wanna make type using key of this interface.
Example
interface Test {
a: string;
b: string;
c: string;
d: string | undefined;
e: string | undefined;
}
My new type can have one property among above keys. I wrote like below
type MyNewType<T> = {
[key in keyof T]: T[key];
};
I used this type as MyNewType<Test>
, but encounters error.
Type '{ a: string; }' is missing the following properties from type 'MyNewType': b, c, d, e.
How can I use property optionally?
Clarification
I'm using reducer in React and calling dispatch
function which accepts action creater as parameter. Test
type is state type and MyNewType<Test>
is payload type. Action creator accepts payload and payload can become { a: 'a'}
, { b: 'b' }
, or { d: undefined }
. However error says that payload has to contain all the properties.
Upvotes: 3
Views: 7408
Reputation: 68
Expanding on S.Young's answer
You can make it error with more than one property by using never
export type OneOf<T> = {
[K in keyof T]: { [_ in K]: T[K] } & Partial<
Record<Exclude<keyof T, K>, never>
>;
}[keyof T];
type Foo = OneOf<{ a: 1, b: 2, c: 3 }>;
// Valid cases
const test1: Foo = { a: 1 }; // Ok
const test2: Foo = { b: 2 }; // Ok
const test3: Foo = { c: 3 }; // Ok
// Invalid cases
const test4: Foo = { a: 1, b: 2 }; // Error: Type '{ a: 1; b: 2; }' is not assignable to type 'Foo'
const test5: Foo = { b: 2, c: 3 }; // Error: Type '{ b: 2; c: 3; }' is not assignable to type 'Foo'
const test6: Foo = {}; // Error: Type '{}' is not assignable to type 'Foo'
Upvotes: 0
Reputation: 11
This works for me in TypeScript 5.0:
type OneProp<T> = {
[K in keyof T]: { [_ in K]: T[K] }
}[keyof T]
To demonstrate, the following type:
type Foo = OneProp<{ a: 1, b: 2, c: 3}>
would expand to
type Foo = {
a: { a: 1 },
b: { b: 2 },
c: { c: 3 },
}['a' | 'b' | 'c']
and finally,
type Foo = { a: 1 } | { b: 2 } | { c: 3 }
This function shows how testing membership is enough to infer the actual type:
function getPropValue(x: OneProp<{ a: 1, b: 2, c: 3 }>) {
if ('a' in x) {
// x is inferred to be { a: 1 } because it contains 'a'
return x.a
} else if ('b' in x) {
// x is inferred to be { b: 2 } because it contains 'b'
return x.b
} else {
// x is inferred to be { c: 3 } since it contains neither 'a' nor 'b'
x.c
}
}
Technically, this type enforces that at least one of the properties of T
is present--you won't get an error if more than one is present.
type Foo = OneProp<{ a: 1, b: 2, c: 3 }> // Evaluates to { a: 1 } | { b: 2 } | { c: 3 }
const test1: Foo = { a: 1 } // Ok
const test2: Foo = { b: 2, c: 3 } // Ok: This does NOT enforce that only one prop is provided
const test3: Foo = {} // ERROR: Type '{}' is not assignable to type 'Foo'
Upvotes: 1
Reputation: 306
Actualy you can do it genericaly, here is a way :
TLDR : improved with Matt's trick forcing other properties to be optional and never typed https://stackoverflow.com/a/66180957/15130395
type NoneOf<T> = {
[K in keyof T]?: never;
}
type OnePropertyOf<T> = {
[K in keyof T]: Omit<NoneOf<T>, K> & { [P in K]: T[P] };
}[keyof T];
Details :
Your interface
interface Test {
a: string;
b: string;
c: string;
d: string | undefined;
e: string | undefined;
}
With this generic type :
type OnePropertyOfIntermediate<T> = {
[K in keyof T]: { [P in K]: T[P] };
};
you obtain :
{
a: { a: string; };
b: { b: string; };
c: { c: string; };
d: { d: string | undefined; };
e: { e: string | undefined; };
}
There, you just need to extract values of this object type :
type OnePropertyOf<T> = OnePropertyOfIntermediate<T>[keyof T]
so you get :
{ a: string; } |
{ b: string; } |
{ c: string; } |
{ d: string | undefined; } |
{ e: string | undefined; };
Upvotes: 0
Reputation: 604
The author of this question got it 99% right by himself. There's only one small ?
missing :)
It can be done by marking the key optional:
type MyNewType<T> = {
[key in keyof T]?: T[key];
};
Upvotes: 1
Reputation: 3438
I had similar issue and I solved using Partial
as below:
Example:
interface Test {
a: string;
b: string;
c: string;
d: string | undefined;
e: string | undefined;
}
// Please note that I added Partial
type MyNewType<T> = Partial<
{
[key in keyof T]: T[key];
}
>;
Upvotes: 0
Reputation: 3890
I had a a similar problem, and came up with the following design.
Define an interface, or type, which contains every property you want to support, but make them all optional and type never. You may use any of these properties, but you can't assign anything to them.
interface BaseType {
a?: never;
b?: never;
c?: never;
d?: never;
e?: never;
}
Create a type for each property by omitting the base never property and replacing it with the type you want, but now it's no longer optional.
type AType = Omit<BaseType, "a"> & { "a": string };
type BType = Omit<BaseType, "b"> & { "b": string };
type CType = Omit<BaseType, "c"> & { "c": string };
type DType = Omit<BaseType, "d"> & { "d": string | undefined };
type EType = Omit<BaseType, "e"> & { "e": string | undefined };
Combine all these single-property types to create your MyNewType
. Any instance of this type must be one of the individual types above.
type MyNewType = AType | BType | CType | DType | EType;
The following lines either compile or trigger TypeScript errors:
let testA : MyNewType = { "a": "test" } // A is present, and is a string
let testB : MyNewType = { "b": 1 } // B is present, but cannot be a number
let testD : MyNewType = { "d": undefined } // D is present, and may be undefined
let testMultiple : MyNewType = { "a": "Some String", "b": "Another String" } // Cannot include multiple properties
I've also set this up in Playground, so you can see the TypeScript errors live.
Upvotes: 0
Reputation: 500
If you don't need a generic solution, you can use the following:
MyNewType = { a: string }
| { b: string }
| { c: string }
| { d: string | undefined }
| { d: string | undefined };
I cannot think of any way of doing this for a generic type T
.
But you can use another approach: Instead of just selecting a single key value pair, you could use separate properties for key and value, e.g.:
{
key: "a",
value: "a"
}
This can be done in a generic way:
type MyNewType<T> = {
key: keyof T;
value: T[keyof T];
}
However, this is not very precise, as you can combine any key with any value.
Another approach would be:
type MyNewType<T, K extends keyof T> = {
key: K;
value: T[K];
}
Wich works as expected but introduces a new type parameter to represent the key type. For example, you could use it as:
function dispatch<K extends keyof Test>(testPayload: MyNewType<Test, K>) {
// do something with payload
}
dispatch({ key: "a", value: "a" }); // OK
dispatch({ key: "a", value: undefined }); // Error, because Test.a cannot be undefined
dispatch({ key: "d", value: undefined }); // OK
dispatch({ key: "x", value: undefined }); // Error, because Test does not have property "x"
Edit
I also tried the following to create a type exactly as requested:
type MyNewType<T, K extends keyof T> = {
[key: K]: T[K];
}
But TypeScript gives the following error on key
:
An index signature parameter type must be either 'string' or 'number'.
Upvotes: 4
Reputation: 426
Add a ?
behind the keys that are optional
Example, if you want to make a & b optional
interface Test {
a?: string;
b?: string;
c: string;
d: string | undefined;
e: string | undefined;
}
Reference: https://www.typescriptlang.org/docs/handbook/interfaces.html#optional-properties
Upvotes: 1