简体   繁体   中英

TypeScript: generically infer union type member based on a string literal property

TypeScript (v3.2.2) allows me to define a union of interfaces, each with a unique string literal property which can be used as a type guard, eg

type Device = Laptop | Desktop | Phone;

interface Laptop {
  type: 'Laptop';
  countDriveBays: number;
  hasTouchScreen: boolean;
}

interface Desktop {
  type: 'Desktop';
  countDriveBays: number;
}

interface Phone {
  type: 'Phone';
  hasTouchScreen: boolean;
}

function printInfo(device: Device) {
  if (device.type === 'Laptop') {
    // device: Laptop
    console.log(
      `A laptop with ${device.countDriveBays} drive bays and ${
        device.hasTouchScreen ? 'a' : 'no'
      } touchscreen.`,
    );
  } else if (device.type === 'Desktop') {
    // device: Desktop
    console.log(`A desktop with ${device.countDriveBays} drive bays.`);
  } else {
    // device: Phone
    console.log(`A phone with ${device.hasTouchScreen ? 'a' : 'no'} touchscreen.`);
  }
}

I want to write a function isDeviceType in a generic way:

const isDeviceType = <T extends Device['type']>(type: T) => {
  return (device: Device): device is DeviceOf<T> => device.type === type;
}

// e.g.
const isPhone = isDeviceType('Phone');
isPhone({ type: 'Phone', hasTouchScreen: true }); // true

However, the way I have defined the DeviceOf type is pretty verbose since it lists every single type within the union:

type DeviceOf<Type extends Device['type']> =
  Type extends Laptop['type'] ? Laptop :
  Type extends Desktop['type'] ? Desktop :
  Type extends Phone['type'] ? Phone :
  never;

Is there a more concise way to define DeviceOf ? I have tried these:

type DeviceOf<Type extends Device['type']> =
  (infer D)['type'] extends Type ? D : never;

// TS2536: Type '"type"' cannot be used to index type 'D'.
// TS1338: 'infer' declarations are only permitted in the 'extends' clause of a conditional type.
// TS6133: 'D' is declared but its value is never read.
type DeviceOf<Type extends Device['type']> =
  (infer D) extends Device
    ? D['type'] extends Type
    ? D
    : never
    : never;

// TS1338: 'infer' declarations are only permitted in the 'extends' clause of a conditional type.
// TS6133: 'D' is declared but its value is never read.
// TS2304: Cannot find name 'D'.

My impression is that error TS1338 is the limiting factor, and so it's impossible to define DeviceOf in a generic way in the current version of TypeScript.

Got it. You have to apply "if" twice, once for create infer type and second to check if infer type extends device. Only in branch D extends Device you will be able u use D['type']

type DeviceOf<Type extends Device['type']> =
  Device extends (infer D) ?
  D extends Device ?
  D['type'] extends Type ? D : never : never : never;

type Result = DeviceOf<'Laptop'>;

Playground

Found an alternative way, using just conditional types without the infer keyword:

type FindByType<Union, Type> = Union extends { type: Type } ? Union : never;
type DeviceOf<Type extends Device['type']> = FindByType<Device, Type>;

type Result = DeviceOf<'Laptop'>;

Based on Ryan Cavanaugh's comment here: https://github.com/Microsoft/TypeScript/issues/17915#issuecomment-413347828

Typescript 2.8 supports the Extract<Type, Union> utility, which makes this even simpler to implement.

type Result = Extract<Device, { type: 'Laptop' }>;

From the Typescript documentation here :

Constructs a type by extracting from Type all union members that are assignable to Union .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM