Skip to content

Too-permissive assignability with complex discriminated unions #34751

Closed
@Harpush

Description

@Harpush

I was trying to add type safety to some model i have and it seems to sometimes work in a weird way.

Code

export enum FilterType {
  String = 'string',
  Number = 'number',
  Boolean = 'boolean'
}

export interface FilterTypeToLiteralTypeMap {
  [FilterType.String]: string;
  [FilterType.Number]: number;
  [FilterType.Boolean]: boolean;
}

export type AllowedFilterTypeLiteralTypes = FilterTypeToLiteralTypeMap[FilterType];

export type Values<T> = T[keyof T];

export type FilterTypeFromLiteralType<
  T extends AllowedFilterTypeLiteralTypes
> = Exclude<
  Values<
    {
      [key in FilterType]: FilterTypeToLiteralTypeMap[key] extends T
        ? key
        : never;
    }
  >,
  never
>;

export interface Filterable<T extends AllowedFilterTypeLiteralTypes> {
  isFilterable: true;
  filterType: FilterTypeFromLiteralType<T>;
}

export interface NotFilterable {
  isFilterable: false;
}

export type FilterableTrait<T extends AllowedFilterTypeLiteralTypes> =
  | Filterable<T>
  | NotFilterable;

What i did here is created an enum and mapped each entry to a literal type. That way when using the FilterableTrait interface it can be either Filterable or NotFilterable and if filterable the FilterType is decided based on the given type.

Some usages that work

// Errors correctly
export const a: FilterableTrait<string> = {
  isFilterable: true,
  filterType: FilterType.Number
};

// Works correctly
export const b: FilterableTrait<string> = {
  isFilterable: true,
  filterType: FilterType.String
};

export const c: FilterableTrait<string> = {
  isFilterable: false,
  filterType: FilterType.String // Errors correctly
};

// Errors correctly
export const d: FilterableTrait<string> | FilterableTrait<number> = {
  isFilterable: true,
  filterType: FilterType.Boolean
};

// Works correctly
export const e: FilterableTrait<string> | FilterableTrait<number> = {
  isFilterable: true,
  filterType: FilterType.Number
};

The problem starts when i do this

export const f: FilterableTrait<string> | FilterableTrait<number> = {
  isFilterable: false,
  filterType: FilterType.Number // No error? Auto complete doesn't add this as an option but it compiles
};

A few notes here:

  1. If i change Filterable to be filterType: FilterTypeFromLiteralType<string> instead of filterType: FilterTypeFromLiteralType<T> it works... it doesnt work just when i use the generic type.
  2. I know i can use
export const g: FilterableTrait<string | number> = {
  isFilterable: false,
  filterType: FilterType.Number
};

and it will work but my actual use case requires me to use something like

export const h:
  | ({ type: '1' } & FilterableTrait<string>)
  | ({ type: '2' } & FilterableTrait<number>) = {
  type: '2',
  isFilterable: true,
  filterType: FilterType.Number
};

Which means i can't use the above

Expected behavior:
Compilation error

Actual behavior:
Compiles successfully

Playground Link: http://www.typescriptlang.org/play/?ts=3.7-Beta&ssl=31&ssc=3&pln=17&pc=1#

Metadata

Metadata

Assignees

Labels

Design LimitationConstraints of the existing architecture prevent this from being fixed

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions