Mapped types don't seem to work with generics

时间:2019-04-08 13:34:22

标签: typescript

export type KeysOfType<Obj, KeyT> = {
    [K in keyof Obj]-?: Obj[K] extends KeyT ? K : never;
}[keyof Obj];

type Row<T> = T & { id: number };

export enum ColumnType {
    Node,
}

interface BaseColumn<T, ValidPropValue> {
    accessor: KeysOfType<T, ValidPropValue | undefined>;
    type: ColumnType;
}

export interface NodeColumn<T> extends BaseColumn<T, string> {
    type: ColumnType.Node;
}

interface Car {
    name: string;
    weight: number;
}

type CarRow = Row<Car>;
type CarNodeCol = NodeColumn<Car>;

const car: Car = { name: '', weight: 0 };
const carRow: CarRow = { name: '', weight: 0, id: 1 };

const carNameCol: CarNodeCol = {
    type: ColumnType.Node,
    accessor: 'name'
};

// works
const nameAccessor: 'name' = carNameCol.accessor;
const name: string | undefined = carRow[nameAccessor];

// doesn't work. should do exactly the same
const renderNode = <T>(row: Row<T>, column: NodeColumn<T>): string => {
    const accessor: KeysOfType<T, string | undefined> = column.accessor;
    const value: string | undefined = row[accessor]; // <--
    return value || '';
};

console.log(renderNode(carRow, carNameCol));

In function the value definition is marked by compiler as incorrect:

Type 'Row<T>[{ [K in keyof T]-?: T[K] extends string ? K : never; }[keyof T]]' is not assignable to type 'string'.
  Type '{ id: number; }[{ [K in keyof T]-?: T[K] extends string ? K : never; }[keyof T]]' is not assignable to type 'string'.

It looks to me like Row<T> is wrongly substituted with { id: number; } missing the & T part.

Is it a problem with my types, or is this a bug/limitation of the TypeScript compiler?

1 个答案:

答案 0 :(得分:1)

As the comments say, the compiler just isn't clever enough to figure this out. It's more of a limitation than a bug. The easy way to deal with this is to embrace your mental superiority and use a type assertion to calm the compiler down.

const renderNode = <T>(row: Row<T>, column: NodeColumn<T>): string => {
    const accessor: KeysOfType<T, string | undefined> = column.accessor;
    const value = row[accessor] as unknown as string | undefined; //  me smarter than compiler
    return value || '';
};

The other way to go is to lead the compiler through the steps of being sure about your types. For example, you can make the annotated type of row explicitly require that it is not just Row<T> but also Record<KeysOfType<T, string | undefined>, string | undefined> (meaning something whose values at those keys are indeed string | undefined). That's redundant in your mind, but it helps the compiler understand. And the call should still work.

const renderNode = <T>(
  row: Row<T> & Record<KeysOfType<T, string | undefined>, string | undefined>,
  column: NodeColumn<T>
): string => {
    const accessor: KeysOfType<T, string | undefined> = column.accessor;
    const value: string | undefined = row[accessor]; // no error now
    return value || '';
};

Personally I'd use the assertion because the extra type baggage you have to carry around to appease the compiler wouldn't be worth it to me.

Hope that helps; good luck!