undefined
undefined

Reputation: 1128

Typescript: Type with exactly one property of another type

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

Answers (8)

Carlos
Carlos

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];

Usage:

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

S. Young
S. Young

Reputation: 11

Generic solution

This works for me in TypeScript 5.0:

type OneProp<T> = {
    [K in keyof T]: { [_ in K]: T[K] }
}[keyof T]

Explanation

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 }

Usage

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

Limitations

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

Romain LAURENT
Romain LAURENT

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

Viesturs Knopkens
Viesturs Knopkens

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

Soe Moe
Soe Moe

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

Matt
Matt

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

mperktold
mperktold

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

Lim Jing Rong
Lim Jing Rong

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

Related Questions