Typescript: Предложение: Тип Тип недвижимости

Созданный на 28 нояб. 2014  ·  76Комментарии  ·  Источник: microsoft/TypeScript

Мотивации

Многие библиотеки / фреймворки / шаблоны JavaScript включают вычисления на основе имени свойства объекта. Например, модель Backbone , функциональное преобразование pluck , ImmutableJS основаны на таком механизме.

//backbone
var Contact = Backbone.Model.extend({})
var contact = new Contact();
contact.get('name');
contact.set('age', 21);

// ImmutableJS
var map = Immutable.Map({ name: 'François', age: 20 });
map = map.set('age', 21);
map.get('age'); // 21

//pluck
var arr = [{ name: 'François' }, { name: 'Fabien' }];
_.pluck(arr, 'name') // ['François', 'Fabien'];

В этих примерах мы можем легко понять связь между api и ограничением базового типа.
В случае базовой модели это просто своего рода _proxy_ для объекта типа:

interface Contact {
  name: string;
  age: number;
}

В случае pluck это преобразование

T[] => U[]

где U - тип свойства T prop .

Однако у нас нет способа выразить такое отношение в TypeScript, и мы получаем динамический тип.

Предложенное решение

Предлагаемое решение - ввести новый синтаксис для типа T[prop] где prop - аргумент функции, использующей такой тип как возвращаемое значение или параметр типа.
С помощью этого синтаксиса нового типа мы могли бы написать следующее определение:

declare module Backbone {

  class Model<T> {
    get(prop: string): T[prop];
    set(prop: string, value: T[prop]): void;
  }
}

declare module ImmutableJS {
  class Map<T> {
    get(prop: string): T[prop];
    set(prop: string, value: T[prop]): Map<T>;
  }
}

declare function pluck<T>(arr: T[], prop: string): Array<T[prop]>  // or T[prop][] 

Таким образом, когда мы используем нашу модель Backbone, TypeScript может правильно проверять тип при вызове get и set .

interface Contact {
  name: string;
  age: number;
}
var contact: Backbone.Model<Contact>;

var age = contact.get('age');
contact.set('name', 3) /// error

Константа prop

Ограничение

Очевидно, что константа должна иметь тип, который может использоваться как тип индекса ( string , number , Symbol ).

Случай индексируемого

Давайте посмотрим на наше определение Map :

declare module ImmutableJS {
  class Map<T> {
    get(prop: string): T[string];
    set(prop: string, value: T[string]): Map<T>;
  }
}

Если T индексируется, наша карта наследует это поведение:

var map = new ImmutableJS.Map<{ [index: string]: number}>;

Теперь get имеет для типа get(prop: string): number .

Допрос

В некоторых случаях мне трудно думать о _ правильном_ поведении, давайте начнем снова с нашего определения Map .
Если бы вместо передачи { [index: string]: number } качестве параметра типа мы дали бы
{ [index: number]: number } должен ли компилятор выдать ошибку?

если мы используем pluck с динамическим выражением для prop вместо константы:

var contactArray: Contact[] = []
function pluckContactArray(prop: string) {
  return _.pluck(myArray, prop);
}

или с константой, которая не является свойством типа, переданного в качестве параметра.
если вызов pluck вызовет ошибку, поскольку компилятор не может вывести тип T[prop] , shoud T[prop] будет разрешен в {} или any , если да, должен ли компилятор с --noImplicitAny выдать ошибку?

Fixed Suggestion help wanted

Самый полезный комментарий

@weswigham @mhegazy , и я недавно это обсуждал; мы сообщим вам о любых разработках, с которыми мы столкнемся, и имейте в виду, что это всего лишь прототип идеи.

Текущие идеи:

  • Оператор keysof Foo для получения объединения имен свойств из Foo виде строковых литералов.
  • Тип Foo[K] который указывает, что для некоторого типа K который является типами строковых литералов или объединением типов строковых литералов.

Из этих базовых блоков, если вам нужно вывести строковый литерал как соответствующий тип, вы можете написать

function foo<T, K extends keysof T>(obj: T, key: K): T[K] {
    // ...
}

Здесь K будет подтипом keysof T что означает, что это тип строкового литерала или объединение типов строковых литералов. Все, что вы передаете для параметра key должно быть контекстно типизировано этим литералом / объединением литералов и выведено как одноточечный строковый литерал.

Например

interface HelloWorld { hello: any; world: any; }

function foo<K extends keysof HelloWorld>(key: K): K {
    return key;
}

// 'x' has type '"hello"'
let x = foo("hello");

Самая большая проблема заключается в том, что keysof часто нужно "отложить" свою операцию. Он слишком нетерпелив в том, как он оценивает тип, что является проблемой для параметров типа, как в первом опубликованном мною примере (то есть случай, который мы действительно хотим решить, на самом деле является сложной частью: smile :).

Надеюсь, что это даст вам все обновления.

Все 76 Комментарий

Возможный дубликат №394

См. Также https://github.com/Microsoft/TypeScript/issues/1003#issuecomment -61171048

@NoelAbrahams Я действительно не думаю, что это дубликат # 394, напротив, обе функции довольно дополняют друг друга, например:

 class Model<T> {
    get(prop: memberof T): T[prop];
    set(prop:  memberof T, value: T[prop]): void;
  }

Было бы идеально

@fdecampredon

contact.set(Math.random() >= 0.5 ? 'age' : 'name', 13)

Что делать в этом случае?

Это более или менее тот же случай, что и в последнем абзаце моего номера. Как я уже сказал, у нас есть несколько вариантов выбора: мы можем сообщить об ошибке или вывести any для T[prop] , я думаю, что второе решение более логично.

Отличное предложение. Согласитесь, была бы полезная функция.

@fdecampredon , я считаю, что это дубликат. См. Комментарий Дэна и соответствующий ответ, который содержит предложение для membertypeof .

ИМО, все это - много нового синтаксиса для довольно узкого варианта использования.

@NoelAbrahams, это не то же самое.

  • memberof T возвращает тип, экземпляр которого может быть только строкой с допустимым именем свойства T instance.
  • T[prop] возвращает тип свойства T названного строкой, представленной аргументом / переменной prop .

Есть дополнение к memberof что тип параметра prop должен быть memberof T .

На самом деле, мне бы хотелось иметь более богатую систему вывода типов на основе метаданных типов. Но такой оператор - хорошее начало, как и memberof .

Это интересно и желательно. TypeScript пока не очень хорошо работает со строковыми фреймворками, и это, очевидно, очень поможет.

TypeScript плохо справляется со строковыми фреймворками

Правда. По-прежнему не меняет того факта, что это повторяющееся предложение.

еще, и это, очевидно, очень поможет [для ввода фреймворков с большим количеством строк]

Не уверен в этом. Кажется довольно разрозненным и в некоторой степени специфичным для описанного выше шаблона прокси-объекта. Я бы предпочел более целостный подход к проблеме волшебной струны, как в # 1003.

1003 предлагает any в качестве возвращаемого типа получателя. Это предложение добавляет сверху, что путем добавления способа поиска типа значения также - сочетание приведет к примерно такому:

declare module ImmutableJS {
  class Map<T> {
    get(prop: memberof T): T[prop];
    set(prop: memberof T, value: T[prop]): Map<T>;
  }
}

@spion , вы имели в виду # 394? Если вы прочитаете дальше, вы увидите следующее:

Я подумал о возвращаемом типе, но оставил его в стороне, чтобы не сделать общее предложение слишком сложным.

Это была моя первоначальная мысль, но есть проблемы. Что делать, если существует несколько аргументов типа memberof T , на какой из них ссылается membertypeof T ?

get(property: memberof T): membertypeof T;
set(property: memberof T, value: membertypeof T);

Это решает проблему «какой аргумент я имею в виду», но имя membertypeof кажется неправильным и не является поклонником оператора, нацеленного на имя свойства.

get(property: memberof T): membertypeof property;
set(property: memberof T, value: membertypeof property);

Думаю, так работает лучше.

get(property: memberof T is A): A;
set(property: memberof T is A, value: A)

К сожалению, не уверен, что у меня есть отличное решение, хотя я считаю, что последнее предложение имеет приличный потенциал.

Хорошо, @NoelAbrahams, в № 394 был комментарий, который пытался описать более или менее то же самое, что и этот.
Теперь я думаю, что T[prop] , возможно, немного более элегантно, чем различные предложения этого комментария, и что предложение в этом выпуске, возможно, идет немного дальше в размышлении.
По этой причине я не думаю, что его нужно закрывать как дубликат.
Но думаю, я предвзят, так как это я написал номер;).

@fdecampredon , чем больше, тем веселее: smiley:

@NoelAbrahams ой, я пропустил эту часть. Конечно, они в значительной степени эквивалентны (этот, похоже, не вводит еще один общий параметр, который может быть или не быть проблемой)

Взглянув на Flow, я думаю, что он был бы более элегантным с немного более сильной системой типов и специальными типами, чем сужением специального типа.

Например, с get(prop: string): Contact[prop] мы имеем в виду серию возможных перегрузок:

interface Map {
  get(prop : string) : Contact[prop];
}

// is morally equivalent to 

interface Map {
  get(prop : "name") : string;
  get(prop : "age") : number;
}

Предполагая существование оператора типа & (типы пересечений), это тип, эквивалентный

interface Map {
   get : (prop : "name") => string & (prop : "age") => number;
}

Теперь, когда мы перевели наш неуниверсальный случай в выражение только типов без специальной обработки (без [prop] ), мы можем решить вопрос о параметрах.

Идея состоит в том, чтобы в некоторой степени сгенерировать этот тип из параметра типа. Мы могли бы определить некоторые специальные фиктивные общие типы $MapProperties , $Name и $Value чтобы выразить наш сгенерированный тип в терминах только выражения типа (без специального синтаксиса), но при этом намекая на средство проверки типов. что что-то должно быть сделано.

class Map<T> {
   get : $MapProperties<T, (prop : $Name) => $Value>
   set : $MapProperties<T, (prop : $Name, val : $Value) => void>
}

Это может показаться сложным и близким к шаблону или типам, зависящим от бедняков, но этого нельзя избежать, когда кто-то хочет, чтобы типы зависели от значений.

Еще одна область, где это было бы полезно, - перебор свойств типизированного объекта:

interface Env {
 // pretend this is an actually interesting type
};

var actions = {
  action1: function (env: Env, x: number) : void {},
  action2: function (env: Env, y: string) : void {}
};

// actions has type { action1: (Env, number) => void; action2: (Env, string) => void; }
var env : Env = {};
var boundActions = {};
for (var action in actions) {
  boundActions[action] = actions[action].bind(null, env);
}

// boundActions should have type { action1: (number) => void; action2: (string) => void; }

Эти типы должны быть, по крайней мере, теоретически возможными для вывода (информации о типах достаточно, чтобы вывести результат цикла for ), но это также, вероятно, довольно натянуто.

Обратите внимание, что следующая версия реакции значительно выиграет от этого подхода, см. Https://github.com/facebook/react/issues/3398

Как и https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -64944856, когда строка предоставляется выражением, отличным от строкового литерала, эта функция быстро выходит из строя из-за проблемы с остановкой. Тем не менее, может ли базовая версия все еще быть реализована с использованием изучения проблемы символов ES6 (# 2012)?

Утверждено.

Мы захотим попробовать это в экспериментальной ветке, чтобы почувствовать синтаксис.

Просто интересно, какая версия предложения будет реализована? Т.е. будет ли T[prop] оцениваться на сайте вызова и с готовностью заменяться конкретным типом, или он станет новой формой переменной типа?

Я думаю, нам следует полагаться на более общий и менее подробный синтаксис, определенный в # 3779.

interface Map<T> {
  get<A>(prop: $Member<T,A>): A;
  set<A>(prop: $Member<T,A>, value: A): Map<T>;
}

Или нельзя вывести тип A?

Просто хочу сказать, что я сделал небольшой инструмент для генерации кода, чтобы упростить интеграцию TS с ImmutableJS, пока мы ждем нормального решения: https://www.npmjs.com/package/tsimmutable. Это довольно просто, но я думаю, что он подойдет для большинства случаев использования. Может кому поможет.

Также хочу отметить, что решение с типом члена может не работать с ImmutableJS:

interface Profile {
  firstName: string 
}

interface User {
  profile: Profile  
}

let a: Map<User> = fromJS(/* ... */);
a.get('profile') // Type will be Profile, but the real type is Map<Profile>!

@ s-panferov Что-то вроде этого могло сработать:

interface ImmutableMap<T> {
    get<A extends boolean | number | string>(key : string) : A;
    get<A extends {}>(key : string) : ImmutableMap<A>;
    get<E, A extends Array<any>>(key : string) : ImmutableList<E>;
}

interface Profile {

}

interface User {
    name : string;
    profile : Profile;
}

var map : ImmutableMap<User>;

var name = map.get<string>('name'); // string
var profile = map.get<Profile>('profile'); // ImmutableMap<Profile>

Это не исключает узлы DOM или объекты Date, но нельзя вообще разрешать их использование в неизменяемых структурах. https://github.com/facebook/immutable-js/wiki/Converting-from-JS-objects

Постепенно работаем с оптимальным решением

Я думаю, что было бы полезно сначала начать с лучшего из доступных на данный момент обходных путей, а затем постепенно работать над ним.

Допустим, нам нужна функция, которая возвращает значение свойства для любого содержащего не примитивного объекта :

function getProperty<T extends object>(container: T; propertyName: string) {
    return container[propertyName];
}

Теперь мы хотим, чтобы возвращаемое значение имело тип целевого свойства, поэтому для его типа можно добавить еще один параметр универсального типа:

function getProperty<T extends object, P>(container: T; propertyName: string) {
    return <P> container[propertyName];
}

Таким образом, вариант использования класса будет выглядеть так:

class C {
    member: number;
    static member: string;
}

let instance = new C();
let result = getProperty<C, typeof instance.member>(instance, "member");

И result правильно получит тип number .

Однако похоже, что в вызове есть повторяющаяся ссылка на «член»: одна находится в параметре типа, а другая - это строка _literal_, которая всегда будет получать строковое представление имени целевого свойства. Идея этого предложения состоит в том, что эти два параметра могут быть объединены в один параметр типа, который будет получать только строковое представление.

Таким образом, здесь необходимо отметить, что строка также будет действовать как _генерический параметр_ и должна быть передана как литерал, чтобы это работало (в других случаях можно молча игнорировать его значение, если не доступна какая-либо форма отражения во время выполнения). Итак, чтобы прояснить семантику, должен быть какой-то способ обозначить связь между универсальным параметром и строкой:

function getProperty<T extends object, PName: string = propertyName>(container: T; propertyName: string) {
    return <T[PName]> container[propertyName];
}

И теперь звонки будут выглядеть так:

let instance = new C();
let result = getProperty<C>(instance, "member");

Что внутренне решило бы:

let result = getProperty<C, "member">(instance, "member");

Однако, поскольку C содержит как экземпляр, так и статическое свойство с именем member , универсальное выражение более высокого типа T[PName] неоднозначно. Поскольку цель здесь в основном состоит в том, чтобы применить его к свойствам экземпляров, решение, подобное предложенному оператору typeon может использоваться внутри, что также может улучшить семантику, поскольку T[PName] интерпретируется некоторыми как ссылка на _значение_, не обязательно на тип:

function getProperty<T extends object, PName: string = propertyName>(container: T; propertyName: string) {
    return <typeon T[PName]> container[propertyName];
}

(Это также будет работать, если T является совместимым типом интерфейса, поскольку typeon поддерживает интерфейсы)

Чтобы получить статическое свойство, функция будет вызываться с самим конструктором, а тип конструктора должен быть передан как typeof C :

let result = getProperty<typeof C>(C, "member"); // Note it is called with the constructor object

(Внутренне typeon (typeof C) преобразуется в typeof C так что это сработает)
result теперь правильно получит тип string .

Для поддержки дополнительных типов идентификаторов свойств это можно переписать как:

type PropertyIdentifier = string|number|Symbol;

function getProperty<T extends object, PName: PropertyIdentifier = propertyName>(container: T; propertyName: PropertyIdentifier) {
    return <typeon T[PName]> container[propertyName];
}

Итак, есть несколько различных функций, которые необходимы для поддержки этого:

  • Разрешить передачу буквальных значений в качестве общих параметров.
  • Разрешить этим литералам получать значение аргументов функции.
  • Разрешить форму дженериков более высокого типа.
  • Разрешить ссылку на тип свойства с помощью литерального универсального параметра для _generic_ типа с помощью универсальных шаблонов более высокого типа.

Переосмысление проблемы

Теперь вернемся к исходному обходному пути:

function getProperty<T extends object, P>(container: T; propertyName: string) {
    return <P> container[propertyName];
}

У этого обходного пути есть одно преимущество: его можно легко расширить для поддержки более сложных имен путей, ссылаясь не только на прямые свойства, но и на свойства вложенных объектов:

function getPath<T extends object, P>(container: T; path: string) {
    ... more complex code here ...

    return <P> resultValue;
}

и сейчас:

class C {
    a: {
        b: {
            c: string[];
        }
    }
}
let instance = new C();
let result = getPath<C, typeof instance.a.b.c>(instance, "a.b.c");

result правильно получит тип string[] .

Вопрос в том, должны ли поддерживаться и вложенные пути? Будет ли эта функция настолько полезной, если при вводе не будет доступно автозаполнение?

Это говорит о том, что возможно другое решение, которое будет работать в другом направлении. Вернемся к исходному обходному пути:

function getProperty<T extends object, P>(container: T; propertyName: string) {
    return <P> container[propertyName];
}

Глядя на это с другой стороны, можно взять саму ссылку на тип и преобразовать ее в строку:

function getProperty<T extends object, P>(container: T; propertyName: string = @typeReferencePathOf(P)) {
    return <P> container[propertyName];
}

@typeReferencePathOf по своей концепции аналогичен nameOf но применяется к общим параметрам. Он берет полученную ссылку на тип typeof instance.member (или, альтернативно, typeon C.member ), извлекает путь к свойству member и преобразует его в буквальную строку "member" при компиляции. время. Менее специализированный дополнительный @typeReferenceOf разрешится в полную строку "typeof instance.member" .

А сейчас:

getProperty<C, typeof instance.subObject>(instance);

Решил бы:

getProperty<C, typeof instance.subObject>(instance, "subObject");

И аналогичная реализация для getPath :

getPath<C, typeof instance.a.b.c>(instance);

Решил бы:

getPath<C, typeof instance.a.b.c>(instance, "a.b.c");

Поскольку @typeReferencePathOf(P) устанавливается только как значение по умолчанию. При необходимости аргумент можно указать вручную:

getPath<C, SomeTypeWhichIsNotAPath>(instance, "member.someSubMember.AnotherSubmember.data");

Если параметр второго типа не будет предоставлен, @typeReferencePathOf() может разрешиться в undefined или, альтернативно, в пустую строку "" . Если тип был предоставлен, но не имел внутреннего пути, он преобразуется в "" .

Преимущества такого подхода:

  • Не требует дженериков высшего типа.
  • Не требует разрешения литералов в качестве общих параметров.
  • Никаким образом не требует расширения дженериков.
  • Разрешает автоматическое завершение при вводе выражения typeof и использует существующий механизм проверки типа для его проверки вместо необходимости преобразовывать его из строкового представления во время компиляции.
  • Также может использоваться в общих классах.
  • Не требует typeon (но может поддерживать).
  • Может применяться в других сценариях.

+1

Ницца! Я уже начал писать большое предложение по решению той же проблемы, но нашел это обсуждение, поэтому давайте обсудим здесь.

Вот выдержка из моего не отправленного вопроса:

Вопрос : Как следующую функцию следует объявить в .d.ts, чтобы ее можно было использовать в контекстах, в которых она должна использоваться?

function mapValues(obj, fn) {
      return Object.keys(obj)
          .map(key => ({key, value: fn(obj[key], key)}))
          .reduce((res, {key, value}) => (res[key] = value, res), {})
}

Эта функция (с небольшими вариациями) можно найти в почти каждом родовом «Utils» библиотеки.

Есть два различных варианта использования mapValues в дикой природе:

а. _Object-as-a-_ _Dictionary _, где имена свойств являются динамическими, значения имеют один и тот же тип, а функция в общем случае является мономорфной (с учетом этого типа)

var obj = {'a': 1, 'b': 2, 'c': 3};
var res = mapValues(x, val => val * 5); // {a: 5, b: 10, c: 15}
console.log(res['a']) // 5

б. _Object-as-a-_ _Record _, где каждое свойство имеет свой собственный тип, а функция параметрически полиморфна (по реализации)

var obj = {a: 123, b: "Hello", c: true};
var res = mapValues(p, val => [val]); // {a: [123], b: ["Hello"], c: [true]}
console.log(res.a[0].toFixed(2)) // "123.00"

Наилучшим доступным инструментарием для mapValues с текущим TypeScript является:

declare function mapValues<T1, T2>(
    obj: {[key: string]: T1},
    fn: (arg: T1, key: string) => T2
): {[key: string]: T2};

Это не трудно понять , что эта подпись идеально соответствует к _Object-а-а-_ _Dictionary _ вариант использования, в то время производит несколько удивительно 1 результат вывода типа , когда применяется в том случае , когда _Object-а-а-_ _Record _ было намерением.

Тип {p1: T1, p2: T2, ...pn: Tn} будет приведен к {[key: string]: T1 | T2 | ... | Tn } объединяющему все типы и эффективно отбрасывающему всю информацию о структуре записи:

  • правильные и ожидаемые 2 будут хорошими console.log(res.a[0].toFixed(2)) будут отклонены компилятором;
  • у принятого console.log((<number>res['a'][0]).toFixed(2)) есть два неконтролируемых и подверженных ошибкам места: приведение типа и произвольное имя свойства.

1, 2 - для программистов, переходящих с ES на TS


Возможные шаги для решения этой проблемы (а также решения других)

1. Введите записи с переменным числом аргументов с помощью строковых литералов.

type Numbers<p extends string> =  { ...p: number };
type NumbersOpt<p extends string> = {...p?: number };
type ABC = "a" | "b" | "c";
type abc = Numbers<ABC> // abc =  {a: number, b: number, c: number} 
type abcOpt = NumbersOpt<ABC> // abcOpt =  {a?: number, b?:number, c?: number}

function toFixedAll<p extends string>(obj: {...p: number}, precision):{...p: string}  {
      var result: {...p: string} = {} as any;
      Object.keys(obj).forEach((p:p) => {
           result[p] = obj[p].toFixed(precision);
      });
      return result;
}

var test = toFixedAll({x:5, y:7}, 3); // { x: "5.00", y: "6.00" },  p inferred as "x"|"y"
console.log(test.y.length) // 4   test.y: string
2. Разрешить формальным типам быть подписанными с другими формальными типами, которые ограничены расширением "строки".

пример:


declare function mapValues<p extends string, T1[p], T2[p]>(
     obj:{...p: T1[p]}, fn:(arg: T1[p]) => T2[p]
): {...p: T2[p]};
3. Разрешить деструктуризацию формальных типов

пример:

class C<Array<T>> {
     x: T;
}   

var v1: C<string[]>;   // v1.x: string

со всеми тремя шагами вместе мы могли бы написать

declare module Backbone {

  class Model<{...p: T[p]}> {
    get(prop: p): T[p];
    set(prop: p, value: T[p]): void;
  }
}

declare module ImmutableJS {
  class Map<{...p: T[p]}> {
    get(prop: p): T[p];
    set(prop: p, value: T[p]): this;
  }
}

declare function pluck<p extends string, T[p]>(
    arr: Array<{...p:T[p]}>, prop: p
): Array<T[p]> 

Я знаю, что синтаксис более подробный, чем тот, который предлагает @fdecampredon
но я не могу представить, как выразить тип mapValues или combineReducers с исходным предложением.

function combineReducers(reducers) {
  return (state, action) => mapValues(reducers, (reducer, key) => reducer(state[key], action))
}

с моим предложением его декларация будет выглядеть так:

declare function combineReducers<p extends string, S[p]>(
    reducers: { ...p: (state: S[p], action: Action) => S[p] }
): (state: { ...p: S[p] }, action: Action) => { ...p: S[p] };

Можно ли это выразить в исходном предложении?

@spion , @fdecampredon ?

@Artazor Определенно улучшение. Теперь кажется, что это достаточно хорошо, чтобы выразить мой тестовый пример использования этой функции:

Предполагая, что user.id и user.name - это типы столбцов базы данных, содержащие number и string соответственно, т.е. Column<number> и Column<string>

Напишите тип функции select которая принимает:

select({id: user.id, name: user.name})

и возвращает Query<{id: number; name: string}>

select<T>({...p: Column<T[p]>}):Query<T>

Не знаю, как это будет проверяться и делать выводы. Похоже, может быть проблема, так как типа T не существует. Будет ли возвращаемый тип выражать в деструктурированной форме? т.е.

select<T>({...p: Column<T[p]>}):Query<{...p:T[p]}>

@Artazor забыл спросить, действительно ли необходим параметр p extends string ?

Разве этого не хватит?

select<T[p]>({...p: Column<T[p]>}):Query<{...p:T[p]}>

т.е. для всех переменных типа T [p] в {...p:Column<T[p]>}

edit: еще один вопрос, как это будет проверяться на правильность?

@spion У меня твоя точка зрения - ты меня неправильно понял (но это моя вина), под T[p] я имел в виду совсем другое, и вы увидите, что p extends string здесь очень важен. Позвольте мне подробно объяснить свои мысли.

Рассмотрим четыре варианта использования структур данных: List , Tuple , Dictionary и Record . Мы будем называть их _концептуальными структурами данных_ и описывать их следующими свойствами:

Концептуальные структуры данныхДоступ через
а. должность
(количество)
б. ключ
(строка)
Количество
Предметы
1. переменная
(одинаковая роль для каждого элемента)
Списоктолковый словарь
2. исправлено
(каждый предмет играет свою уникальную роль)
КортежЗапись

В JavaScript эти четыре случая поддерживаются двумя формами хранения: Array - для a1 и a2 , и Object - для b1 и b2 .

_Примечание 1 _. Честно говоря, неверно утверждать, что Tuple поддерживается Array , поскольку единственный "истинный" тип кортежа - это тип объекта arguments который не является Array . Тем не менее, они более или менее взаимозаменяемы, особенно в контексте деструктуризации и Function.prototype.apply открывающего дверь для метапрограммирования. TypeScript также использует массивы для моделирования кортежей.

_Примечание 2 _. Хотя полная концепция словаря с произвольными ключами была представлена ​​десятилетия спустя в форме ES6 Map , первоначальное решение Брендана Эйха объединить Record и ограниченное Dictionary (с только строковые ключи) под тем же капотом Object (где obj.prop эквивалентно obj["prop"] ) стали наиболее спорным элементом всего языка (на мой взгляд).
Это и проклятие, и благословение семантики JavaScript. Это делает размышление тривиальным и побуждает программистов свободно переключаться между уровнями _программирования_ и _метапрограммирования_ практически с нулевыми умственными затратами (даже не замечая!). Я считаю, что это существенная часть успеха JavaScript как языка сценариев.

Теперь настало время для TypeScript предоставить способ выражения типов для этой странной семантики. Когда мы думаем на уровне _программирования_, все в порядке:

Типы на уровне программированияДоступ через
а. должность
(количество)
б. ключ
(строка)
Количество
Предметы
1. переменная
(одинаковая роль для каждого элемента)
Т []{[ключ: строка]: T}
2. исправлено
(каждый предмет играет свою уникальную роль)
[T1, T2, ...]{key1: T1, key2: T2, ...}

Однако, когда мы переключаемся на уровень _мета-программирования_, то вещи, которые были исправлены на уровне _программирования_, внезапно становятся переменными! И здесь мы внезапно распознаем отношения между кортежами и сигнатурами функций и предлагаем такие вещи, как # 5453 (Variadic Kinds), которые на самом деле покрывают только небольшую (но довольно важную) часть потребностей метапрограммирования - конкатенацию сигнатур, давая возможность деструктурировать формальный параметр в типы rest-args:

     function f<T>(a: number, ...args:T) { ... }

    f<[string,boolean]>(1, "A", true);

В случае, если # 6018 также будет реализован, класс Function мог бы выглядеть как

declare class Function<This, TArgs, TRes> {
        This::(...args: TArgs): TRes;
        call(self: This, ...args: TArgs): TRes;
        apply(self: This, args: TArgs): TRes;
        // bind needs also formal pattern matching:
        bind<[...TPartial, ...TCurried] = TArgs>(
           self: This, ...args: TPartial): Function<{}, TCurried, TRes> 
}

Это здорово, но в любом случае неполное.

Например, представьте, что мы хотим присвоить правильный тип следующей функции:

function extractAndWrapAll(...args) {
     return args.map(x => [x]);
}

// wrapAll(1,"A",true) === [[1],["A"],[true]]

С помощью предложенных вариативных видов мы не можем преобразовать типы подписи. Здесь нам нужно что-то посильнее. Фактически, желательно иметь функции времени компиляции, которые могут работать с типами (как в Facebook Flow). Хотя я уверен, что это будет возможно только тогда (и если) система типов TypeScript будет достаточно стабильной, чтобы предоставить ее конечным программистам без значительного риска нарушить пользовательскую среду при следующем небольшом обновлении. Таким образом, нам нужно что-то менее радикальное, чем полная поддержка метапрограммирования, но все же способное справиться с продемонстрированными проблемами.

Чтобы решить эту проблему, я хочу ввести понятие «подпись объекта». Примерно либо

  1. набор имен свойств объекта, или
  2. набор ключей словаря, или
  3. набор числовых индексов массива
  4. набор числовых позиций кортежа.

В идеале было бы неплохо иметь целочисленные литеральные типы, а также строковые литералы для представления сигнатур массивов или кортежей, например

type ZeroToFive = 0 | 1 | 2 | 3 | 4 | 5;
// or
type ZeroToFive = 0 .. 5;   // ZeroToFive extends number (!)

правила для целочисленных литералов совпадают с правилами для строковых литералов;

Затем мы можем ввести синтаксис абстракции подписи, который точно соответствует синтаксису объекта rest / spread:

{...Props: T }

с одним важным исключением: здесь Props - это идентификатор, связанный с универсальным квантификатором:

<Props extends string> {...Props: T }  // every property has type T
<Index extends number> {...Index: T }  // every item has type T  
// the same as T[]

таким образом, он вводится в лексической области видимости типа и может использоваться в любом месте этой области как имя типа. Однако его использование двояко: при использовании вместо имени свойства rest / spread оно представляет абстракцию по сигнатуре объекта, когда он используется как отдельный тип, он представляет подтип строки (или число соответственно).

declare class Object {
      static keys<p extends string, q extends p>(object{...q: {}}): p[];
}

И вот самая сложная часть: _Key Dependent Types_

мы вводим специальную конструкцию типа: T for Prop (я бы хотел использовать этот синтаксис, а не T [Prop], который вас запутал), где Prop - это имя переменной типа, которая содержит абстракцию сигнатуры объекта. Например, <Prop extends string, T for Prop> вводит два формальных типа в лексическую область видимости типа: Prop и T где известно, что для каждого конкретного значения p из Prop будет собственный тип T .

Мы не говорим, что где-то есть объект со свойствами Props и их типами T ! Мы только вводим функциональную зависимость между двумя типами. Тип T соотносится с членами типа Props, и все!

Это дает нам возможность писать такие вещи как

function unwrap<P extends string, T for P>(obj:{...P: Maybe<T>}): Maybe<{...P: T}> {
  ...
}

unwrap({a:{value:1}, b:{value:"A"}, c:{value: true}}) === { a: 1, b: "A", c: true }
// here actual parameters will be inferred as 
unwrap<"a"|"b"|"c", {a: number, b: string, c: boolean}>

однако второй параметр рассматривается не как объект, а как абстрактная карта идентификаторов для типов. В этой перспективе T for P может использоваться для представления абстрактных последовательностей типов для кортежей, когда P является подтипом числа ( @JsonFreeman ?)

Когда T используется где-то внутри {...P: .... T .... } он представляет ровно один конкретный тип этой карты.

Это моя основная идея.
С нетерпением жду вопросов, мыслей, критики -)

Верно, поэтому extends string должен учитывать массивы (и переменные аргументы) в одном случае и строковые константы (как типы) в другом. Сладкий!

Мы не говорим, что где-то есть объект со свойствами Props и их типами T! Мы только вводим функциональную зависимость между двумя типами. Тип T соотносится с членами типа Props, и все!

Я не это имел в виду, я имел в виду больше, поскольку их типы - это T [p], словарь типов, индексированных p. Если это хорошая интуиция, я бы это оставил.

Хотя в целом над синтаксисом может потребоваться немного больше работы, но общая идея выглядит потрясающе.

Можно ли написать unwrap для вариативных аргументов?

edit: nevermind, я только что понял, что ваше предложенное расширение для вариативных видов решает эту проблему.

Всем привет,
Я ломаю голову, как решить одну из своих проблем и нашел это обсуждение.
Проблема в следующем:
У меня есть метод RpcManager.call(command:Command):Promise<T> , и его использование будет таким:

RpcManager.call(new GetBalance(123)).then((result) => {
 // here I want that result would have a type.
});

Решение, я думаю, могло бы быть таким:

interface Command<T> {
    responseType:T;
}

class GetBalance implements Command<number> {
    responseType: number; // somehow this should be avoided. maybe Command should be abstract class.
    constructor(userId:number) {}
}

class RpcManager {
    static call(command:Command):Promise<typeof command.responseType> {
    }
}

or:

class RpcManager {
    static call<T>(command:Command<T>):Promise<T> {
    }
}

Есть мысли по этому поводу?

@ antanas-arvasevicius последний блок кода в этом примере должен делать то, что вы хотите.

похоже, у вас есть больше вопросов о том, как выполнить конкретную задачу; используйте Stack Overflow или сообщите о проблеме, если считаете, что обнаружили ошибку компилятора.

Привет, Райан, спасибо за ответ.
Я пробовал последний блок кода, но он не работает.

Быстрая демонстрация:

interface Command<T> { }
class MyCommand implements Command<{status:string}> { }
class RPC { static call<T>(command:Command<T>):T { return; } }

let response = RPC.call(new MyCommand());
console.log(response.status);

//output: error TS2339: Property 'status' does not exist on type '{}'.
//tested with: Version 1.9.0-dev.20160222

Извините, что не использовал Stack Overflow, но подумал, что это связано с этой проблемой :)
Стоит ли открывать новый выпуск по этой теме?

Неиспользованный параметр универсального типа препятствует работе вывода; в общем, вы не должны _ никогда_ иметь неиспользуемые параметры типа в объявлениях типов, поскольку они не имеют смысла. Если вы потребляете T , все просто работает:

interface Command<T> { foo: T }
class MyCommand implements Command<{status:string}> { foo: { status: string; } }
class RPC { static call<T>(command:Command<T>):T { return; } }

let response = RPC.call(new MyCommand());
console.log(response.status);

Это просто потрясающе! Спасибо!
Я не думал, что параметр типа можно разместить внутри общего типа и
TS его извлечет.
22 февраля 2016 г. в 23:56 "Райан Кавано" [email protected] написал:

Неиспользованный параметр универсального типа препятствует работе вывода; в
в общем, вы не должны _ никогда_ иметь неиспользуемые параметры типа в типе
декларации, поскольку они бессмысленны. Если вы потребляете T, все просто
работает:

команда интерфейса{foo: T} класс MyCommand реализует Command <{status: string}> {foo: {status: string; }} class RPC {статический вызов(команда: Команда): Т {возврат; }}
let response = RPC.call (new MyCommand ());
console.log (response.status);

-
Ответьте на это письмо напрямую или просмотрите его на GitHub
https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -187404245
.

@ antanas-arvasevicius, если вы создаете API в стиле RPC, у меня есть несколько документов, которые могут вам пригодиться: https://github.com/alm-tools/alm/blob/master/docs/contributing/ASYNC.md : rose:

Подходы выше кажутся:

  • довольно сложный;
  • не занимайтесь использованием строк в коде. string не поддерживает функцию «Найти все ссылки» или рефакторинг (например, переименование).
  • некоторые поддерживают только свойства «1-го уровня», а не более сложные выражения (которые поддерживаются некоторыми фреймворками).

Вот еще одна идея, вдохновленная деревьями выражений C #. Это лишь приблизительная идея, ничего до конца продуманного! Синтаксис ужасен. Я просто хочу посмотреть, вдохновляет ли это кого-нибудь.

Предположим, у нас есть особый вид строк для обозначения выражений.
Назовем его type Expr<T, U> = string .
Где T - это начальный тип объекта, а U - тип результата.

Предположим, мы могли бы создать экземпляр Expr<T,U> , используя лямбду, которая принимает один параметр типа T и выполняет доступ к нему члену.
Например: person => person.address.city .
Когда это происходит, вся лямбда компилируется в строку, содержащую любой доступ к параметру, в данном случае: "address.city" .

Вместо этого вы можете использовать простую строку, которая будет отображаться как Expr<any, any> .

Наличие этого специального типа Expr в языке позволяет делать такие вещи:

function pluck<T, U>(array: T[], prop: Expr<T, U>): U[];

let numbers = pluck([{x: 1}, {x: 2}], p => p.x);  // number[]
// compiles to:
// let numbers = pluck([..], "x");

По сути, это ограниченная форма того, для чего выражения используются в C #.
Как вы думаете, можно ли это усовершенствовать и привести к чему-нибудь интересному?

@fdecampredon @RyanCavanaugh

_ ( @ jods4 - извините, я не отвечаю на ваше предложение здесь. Надеюсь, его не «похоронят» в комментариях) _

Я думаю, что название этой функции («Тип свойства») очень сбивает с толку и очень сложно для понимания. Было очень сложно даже понять, что это за концепция, описанная здесь, и что она означает в первую очередь!

Во-первых, не все типы обладают свойствами! undefined и null этого не делают (хотя это только недавние дополнения к системе типов). Такие примитивы, как number , string , boolean редко индексируются по свойству (например, 2 ["prop"]? Хотя это, похоже, работает, но почти всегда ошибка)

Я бы предложил назвать эту проблему Ссылкой на тип свойства интерфейса через значения строкового литерала . Тема здесь не о введении нового «типа», а о очень конкретном способе ссылки на существующий с помощью строковой переменной или параметра функции, значение которого _ должно быть известно во время компиляции_.

Было бы очень полезно, если бы это было описано и проиллюстрировано как можно проще, вне контекста конкретного варианта использования:

interface MyInterface {
  prop1: number;
  prop2: string;
}

let prop1Name = "prop1";
type Prop1Type = MyInterface[prop1Name]; // Prop1Type is now 'number'

let prop2Name = "prop2";
type Prop2Type = MyInterface[prop2Name]; // Prop2Type is now 'string'

let prop3Name = "prop3";
type NonExistingPropType = MyInterface[prop3Name]; // Compilation error: property 'prop3' does not exist on 'MyInterface'.

let randomString = createRandomString();
type NotAvailablePropType = MyInterface[randomString]; // Compilation error: value of 'randomString' is not known at compile time.

_Edit: Похоже, чтобы реализовать это правильно, компилятор должен точно знать, что строка, присвоенная переменной, не изменилась между точкой, в которой она была инициализирована, и точкой, на которую она ссылалась в выражении типа. Я не думаю, что это очень просто? Всегда ли справедливо это предположение о поведении во время выполнения? _

_Edit 2: Возможно, это будет работать только с const при использовании с переменной? _

Я не уверен, что исходное намерение заключалось в том, чтобы разрешить ссылку на свойство только в очень частном случае, когда строковый литерал передается в параметр функции или метода? например

function func(someString: string): MyInterface[someString] {
  ..
}

let x = func("prop"); // x gets the type of MyInterface.prop

Обобщил ли я это за пределами того, что изначально предполагал?

Как это обработать случай, когда аргумент функции не является строковым литералом, т.е. неизвестен во время компиляции? например

let x = func(getRandomString()); // What type if inferred for 'x' here?

Будет ли это ошибка или, возможно, по умолчанию any ?

(PS: если это действительно было намерением, я предлагаю переименовать эту проблему в ссылку на тип свойства интерфейса с помощью строковой литеральной функции или аргументов метода - как бы долго это ни оказалось, это гораздо более точно и пояснительно, чем текущее название.)

Вот простой тестовый пример (достаточно для иллюстрации), который показывает, что эта функция должна включать:

Напишите тип этой функции, которая:

  • принимает объект, содержащий поля обещания, и
  • возвращает тот же объект, но со всеми разрешенными полями, причем каждое поле имеет правильный тип.
function awaitObject(obj) {
  var result = {};
  var wait = Object.keys(obj)
    .map(key => obj[key].then(val => result[key] = val));
  return Promise.all(wait).then(_ => result)
}

При вызове следующего объекта:

var res = awaitObject({a: Promise.resolve(5), b: Promise.resolve("5")})

Результат res должен иметь тип {a: number; b: string}


С помощью этой функции (полностью раскрытой @Artazor) подпись будет

awaitObject<p, T[p]>(obj: {...p: Promise<T[p]>}):Promise<{...p: T[p]}>

изменить: исправлен тип возврата, указанный выше, отсутствовал Promise<...> ^^

@spion

Спасибо за попытку предоставить пример, но нигде здесь я не нашел разумной и сжатой спецификации и набора четких сокращенных примеров, которые _отделены_ от практических приложений и пытаются выделить семантику и различные крайние случаи того, как это будет работать, даже такие простые вещи, как:

function func<T extends object>(name: string): T[name] {
 ...
}
  1. Было приложено очень мало усилий для обеспечения эффективного и пояснительного заголовка (как я уже упоминал, «Тип свойства типа» - сбивающее с толку и не очень понятное имя для этой функции, которое на самом деле не о каком-то конкретном типе, а о ссылке на тип). Лучше всего я предложил заголовок _Ссылка на тип свойства интерфейса с помощью строковой литеральной функции или аргументов метода_ (при условии, что я правильно понял объем этого).
  2. Не было четкого упоминания о том, что произойдет, если name неизвестен во время компиляции, равен нулю или неопределенному, например func<MyType>(getRandomString()) , func<MyType>(undefined) .
  3. Не было четкого упоминания о том, что произойдет, если T будет примитивным типом, таким как number или null .
  4. Не было предоставлено четких подробностей относительно того, применимо ли это только к функциям и можно ли использовать T[name] в теле функции. И в этом случае, что произойдет, если name переназначен в теле функции и, таким образом, его значение может больше не быть известно во время компиляции?
  5. Нет реальной спецификации синтаксиса: например, T["propName"] или T[propName] тоже будут работать сами по себе? без ссылки на параметр или переменную, я имею ввиду - это может быть полезно!
  6. Никакого упоминания или обсуждения, если T[name] можно использовать в других параметрах или даже за пределами функций, например
function func<T extends object>(name: string, val: T[name]) {
 ...
}
type A = { abcd: number };
const name = "abcd";
let x: A[name]; // Type of 'x' resolves to 'number'

_7. Нет реального обсуждения относительно простого обходного пути с использованием дополнительного универсального параметра и typeof (хотя это было упомянуто, но относительно поздно после того, как функция уже была принята):

function get<T, V>(obj: T, propName: string): V {
    return obj[propName];
}

type MyType = { abcd: number };
let x: MyType  = { abcd: 12 };

let result = get<MyType, typeof x.abcd>(x, "abcd"); // Type of 'result' is 'number'

В заключение: настоящего предложения здесь нет, только набор демонстраций вариантов использования. Я удивлен, что это было принято или даже получило положительный интерес со стороны команды TypeScript, потому что иначе я бы не поверил, что это будет соответствовать их стандартам. Прошу прощения, если я могу показаться здесь немного резким (не типичным для меня), но ни одна моя критика не носит личного характера или направлена ​​в адрес какого-либо конкретного человека.

Неужели мы действительно хотим зайти так далеко в плане метапрограммирования?

JS - это динамический язык, код может произвольно манипулировать объектами.
Существуют ограничения на то, что мы можем моделировать в системе типов, и легко придумать функции, которые вы не можете смоделировать в TS.
Вопрос скорее в том, насколько разумно и полезно зайти?

Последний пример ( awaitObject ) от @spion одновременно:

  • _Очень сложно_ с точки зрения языка (здесь я согласен с @malibuzios ).
  • _Очень узкий_ с точки зрения применимости. В этом примере выбираются конкретные ограничения, и он не дает хороших обобщений.

Кстати, @spion, вы Promise .

Мы далеки от исходной проблемы, которая заключалась в вводе API, который принимает строку, представляющую поля, например _.pluck
Эти API-интерфейсы _должны_ поддерживаться, потому что они обычны и не так уж сложны. Для этого нам не нужны метамодели типа { ...p: T[p] } .
В OP есть несколько примеров, еще несколько от Аурелии в моем комментарии в названии проблемы .
Эти варианты использования можно охватить другим подходом, возможную идею см. В моем комментарии выше .

@ jods4 это совсем не узкое. Его можно использовать для ряда вещей, которые в настоящее время невозможно смоделировать в системе типов. Я бы сказал, что это одна из последних крупных недостающих частей в системе типов TS. Выше есть множество реальных примеров от @Artazor https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -177287714, более простых вещей, приведенных выше, конструктора запросов sql "select" и так далее.

Это awaitObject в синей птице. Это настоящий метод. Тип на данный момент бесполезен.

Лучше будет более общее решение. Он подойдет как для простых, так и для сложных случаев. Недостаточно мощное решение будет отсутствовать в половине случаев и легко вызовет проблемы совместимости, если в будущем потребуется принять более общее решение.

Да, нужно еще много поработать, но я также думаю, что @Artazor отлично справился с анализом и исследованием всех аспектов, о которых мы раньше не думали. Я бы не остановился, мы отклонились от исходной проблемы, мы только лучше ее понимаем.

Возможно, нам понадобится название получше, я бы попробовал, но обычно я не понимаю эти вещи правильно. А как насчет «Обобщения объектов»?

@spion , на всякий

awaitObject<p,T[p]>(obj: {...p:Promise<T[p]>}): Promise<{...p:T[p]}>

(вы пропустили обещание в выходной подписи)
-)

@ jods4 , я согласен с @spion
Существует множество реальных проблем, для которых { ...p: T[p] } является решением. Чтобы назвать одно: реагировать + поток / редукция

@spion @Artazor
Меня просто беспокоит, что это становится довольно сложно указать точно.

Мотивация, стоящая за этой проблемой, изменилась. Первоначально речь шла о поддержке API-интерфейсов, которые принимают строку для обозначения поля объекта. Сейчас он в основном обсуждает API, которые очень динамично используют объекты в качестве карт или записей. Обратите внимание: если мы сможем убить двух зайцев, я буду полностью за это.

Если мы вернемся к API-интерфейсам, которые решают проблему со строками, T[p] на мой взгляд, не является полным решением (я уже говорил, почему раньше).

_Просто для правильности_ awaitObject должен также принимать свойства, не являющиеся обещаниями, по крайней мере, Bluebird props . Итак, теперь у нас есть:

awaitObject<p,T[p]>(obj: { ...p: T[p] | Promise<T[p]> }): Promise<T>

Я изменил тип возвращаемого значения на Promise<T> потому что ожидаю, что эта запись будет работать.
Есть и другие перегрузки, например, та, которая принимает Promise для такого объекта (его подпись еще интереснее). Это означает, что нотацию { ...p } следует рассматривать как худшее соответствие, чем любой другой тип.

Уточнение всего этого будет тяжелой работой. Я бы сказал, что это следующий шаг, если вы хотите продвинуть это вперед.

@spion @ jods4

Я хотел упомянуть, что эта функция не касается дженериков и не полиморфных типов более высокого типа. Это просто синтаксис для ссылки на тип свойства через содержащий тип (с некоторыми «продвинутыми» расширениями, которые описаны ниже), что не так уж далеко от концепции typeof . Пример:

type MyType = { abcd: number };
let y: MyType["abcd"]; // Technically this could also be written as MyType.abcd

Теперь сравните с:

type MyType = { abcd: number };
let x: MyType;

let y: typeof x.abcd;

Есть два основных отличия от typeof . В отличие от typeof , это относится к типам (по крайней мере, непримитивным, факт, который здесь часто опускается), а не к экземплярам. Вдобавок (и это главное) он был расширен для поддержки использования буквальных строковых констант (которые должны быть известны во время компиляции) в качестве дескрипторов пути к свойствам и дженериков:

const propName = "abcd";
let y: MyType[propName];
// Or with a generic parameter:
let y: T[propName];

Однако технически typeof можно было бы расширить для поддержки строковых литералов (это включает случай, когда x имеет общий тип):

let x: MyType;
const propName = "abcd";
let y: typeof x[propName];

И с этим расширением его также можно использовать для решения некоторых целевых случаев здесь:

function get<T>(propName: string, obj: T): typeof obj[propName]

typeof однако, более эффективен, так как поддерживает неограниченное количество уровней вложенности:

let y: typeof x.prop.childProp.deeperChildProp

И это только один уровень. Т.е. не планируется (насколько мне известно) поддерживать:

let y: MyType["prop"]["childProp"]["deeperChildProp"];
// Or alternatively
let y: MyType["prop.childProp.deeperChildProp"];

Я думаю, что рамки этого предложения (если это вообще можно назвать предложением на его уровне расплывчатости) слишком узки. Это может помочь решить конкретную проблему (хотя могут быть альтернативные решения), что, кажется, заставляет многих людей очень стремиться ее продвигать. Однако он также потребляет ценный синтаксис языка. Без более широкого плана и подхода, ориентированного на дизайн, кажется неразумным поспешно внедрять что-то, что на сегодняшний день даже не имеет четкой спецификации.

_Редакты: исправлены очевидные ошибки в примерах кода_

Я немного исследовал альтернативу typeof :

Будущая языковая поддержка для typeof x["abcd"] и typeof x[42] была одобрена и теперь подпадает под # 6606, который в настоящее время находится в стадии разработки (есть рабочая реализация).

Это половина пути. После этого все остальное можно сделать в несколько этапов:

(1) Добавьте поддержку констант строковых литералов (или даже числовых констант - может быть полезно для кортежей?) В качестве спецификаторов свойств в выражениях типов, например:

let x: MyType;
const propName = "abcd";
let y: typeof x[propName];

(2) Разрешить применение этих спецификаторов к универсальным типам.

let x: T; // Where T should extend 'object'
const propName = "abcd";
let y: typeof x[propName];

(3) Разрешить передачу этих спецификаторов и на практике «создать экземпляры» ссылок через аргументы функции (планируется typeof list[0] поэтому я считаю, что это может охватывать более сложные случаи, такие как pluck :

function get<T extends object>(obj: T, propertyName: string): typeof obj[propertyName];
function pluck<T extends object>(list: Array<T>, propertyName: string): Array<typeof list[0][propertyName]>;

( object type - это тот, который предлагается на # 1809)

Хотя этот альтернативный подход более мощный (например, может поддерживать typeof obj[propName][nestedPropName] ), возможно, он не охватывает все случаи, описанные здесь. Пожалуйста, дайте мне знать, есть ли примеры, которые, похоже, не обрабатываются этим (один сценарий, который приходит на ум, - это когда экземпляр объекта вообще не передается, однако мне сейчас сложно представить случай, когда это будет необходимо, хотя я думаю, что это возможно).

_Редакты: исправлены ошибки в коде_

@malibuzios
Некоторые мысли:

  • Это исправляет определение API, но не помогает вызывающему. Нам все еще нужны какие-то nameof или Expression на сайте вызова.
  • Он работает для определений .d.ts , но обычно не может быть статически выведен или даже проверен в функциях / реализациях TS.

Чем больше я думаю об этом, тем больше Expression<T, U> выглядит как решение для такого рода API. Он устраняет проблемы с сайтом вызова, типизацию результата и может быть выведен + проверен в реализации.

@ jods4

Вы упомянули в предыдущем комментарии выше, что pluck можно реализовать с помощью выражений. Хотя раньше я был хорошо знаком с C #, я никогда не экспериментировал с ними, поэтому признаю, что на данный момент я не очень хорошо их понимаю.

В любом случае, просто хотел упомянуть, что TypeScript поддерживает вывод аргументов _type_ , поэтому вместо использования pluck можно просто использовать map и достичь того же результата, что даже не потребует указания универсального параметра ( это предполагается), а также даст полную проверку типа и завершение:

let x = [{name: "John", age: 34}, {name: "Mary", age: 53}];

let result = x.map(obj => obj.name);
// 'result' is ["John", "Mary"] and its type inferred as 'string[]' 

Где map (очень упрощенный для демонстрации) объявлен как

map<U>(mapFunc: (value: T) => U): U[];

Этот же шаблон вывода можно использовать как «трюк» для передачи любого результата произвольной лямбды (которую даже не нужно вызывать) функции и установки для нее возвращаемого типа:

function thisIsATrick<T, U>(obj: T, onlyPassedToInferTheReturnType: () => U): U {
   return;
}

let x = {name: "John", age: 34};

let result = thisIsATrick(x, () => x.age) // Result inferred as 'number' 

_Edit: может показаться глупым передавать здесь его в лямбде, а не только саму вещь! однако в более сложных случаях, таких как вложенные объекты (например, x.prop.Childprop ), могут быть неопределенные или нулевые ссылки, которые могут вызвать ошибку. Использование его в лямбде, которое не обязательно вызывается, позволяет избежать этого.

Признаюсь, я не очень хорошо знаком с некоторыми из обсуждаемых здесь вариантов использования. Лично я никогда не чувствовал необходимости вызывать функцию, которая принимает имя свойства как строку. Передачи лямбда (где описанные вами преимущества также имеют место) в сочетании с интерфейсом аргумента типа обычно достаточно для решения многих общих проблем.

Причина, по которой я предложил альтернативный подход typeof заключалась в основном в том, что он, кажется, охватывает примерно те же варианты использования и обеспечивает ту же функциональность, что и описанные здесь люди. Я бы сам им воспользовался? Я не знаю, никогда не чувствовал реальной необходимости (может быть, я почти никогда не использую внешние библиотеки, такие как подчеркивание, я почти всегда разрабатываю служебные функции самостоятельно, по мере необходимости).

@malibuzios Верно, pluck немного глупо со встроенными лямбдами + map .

В этом комментарии я привожу 5 различных API, которые принимают строки - все они связаны с наблюдением за изменениями.

@ jods4 и другие

Просто хотел упомянуть, что, когда # 6606 будет завершен, реализуя только этап 1 (разрешив использование константных строковых литералов в качестве спецификаторов свойств), некоторые из необходимых здесь функций могут быть достигнуты, хотя и не так элегантно, как могло бы (может потребоваться имя свойства, которое будет объявлено как константа, добавив дополнительный оператор).

function observeProperty<T, U>(obj: T, propName: string ): Subscriber<U> {
    ....
}

let x = { name: "John", age: 42 };

const propName = "age";
observeProperty<typeof x, typeof x[propName]>(x, propName);

Однако количество усилий для реализации этого может быть значительно меньше, чем реализация этапов 2 и 3 (я не уверен, но возможно, что 1 уже охвачен # 6606). При реализации этапа 2 это также будет охватывать случай, когда x имеет общий тип (но я не уверен, действительно ли это нужно?).

Изменить: Причина, по которой я использовал внешнюю константу, а не просто дважды написал имя свойства, заключалась не только в том, чтобы уменьшить ввод текста, но и для того, чтобы имена всегда совпадали, хотя это все еще нельзя использовать с такими инструментами, как «переименовать» и «найти все». ссылки », что я считаю серьезным недостатком.

@ jods4 Я все еще ищу лучшее решение, в котором не используются строки. Попробую подумать, что можно сделать с nameof и его вариантами.

Вот еще одна идея.

Поскольку строковые литералы уже поддерживаются как типы, например, уже можно писать:

let x: "HELLO"; 

Можно рассматривать буквальную строку, переданную функции, как форму общей специализации.

(_Edit: эти примеры были исправлены, чтобы гарантировать, что s является неизменным в теле функции, хотя const не поддерживается в позициях параметров в этот момент (не уверен насчет readonly хотя)._) :

function func(const s: string)

Тип параметра string связанный с s можно рассматривать как неявный универсальный (поскольку он может быть специализирован для строковых литералов). Для наглядности напишу более подробно (хотя не уверен, действительно ли в этом есть необходимость):

function func<S extends string>(const s: S)
func("TEST");

может внутренне специализироваться на:

function func(s: "TEST")

И теперь это можно использовать как альтернативный способ «передать» имя свойства, которое, как мне кажется, лучше отражает семантику.

function observeProperty<T, S extends string>(obj: T, const propName: S): Subscriber<T[S]>

x = { name: "John",  age: 33};

observeProperty(x, nameof(x.age))

Поскольку в T[S] и T , и S являются общими параметрами. Кажется более естественным объединение двух типов в позиции типа, чем смешивание элементов времени выполнения и области видимости типа (например, T[someString] ).

_Edits: переписаны примеры, чтобы иметь неизменяемый строковый тип параметра. _

Поскольку я использую TS 1.8.7 а не последнюю версию, я не знал, что в более поздних версиях в

const x = "Hello";

Тип x уже подразумевается как буквальный тип "Hello" (то есть: x: "Hello" ), что имеет большой смысл (см. # 6554).

Итак, естественно, если бы был способ определить параметр как const (или, может быть, readonly тоже подойдет?):

function func<S extends string>(const s: S): S

Тогда я считаю, что это могло произойти:

let result = func("abcd"); // type of 'result' inferred as the literal type "abcd"

Поскольку некоторые из них являются довольно новыми и основаны на последних языковых функциях, я постараюсь резюмировать их как можно яснее:

(1) Когда строковая переменная const (а может быть, и readonly ?) Получает значение, известное во время компиляции, переменная автоматически получает литеральный тип с тем же значением ( Я считаю, что это недавнее поведение, которое не происходит на 1.8.x ), например

const x = "ABCD";

Тип x предполагается как "ABCD" , а не string !, Например, можно указать это как x: "ABCD" .

(2) Если бы параметры функции readonly были разрешены, параметр readonly с универсальным типом, естественно, специализировал бы свой тип до строкового литерала, когда он получен в качестве аргумента, поскольку параметр является неизменяемым!

function func<S extends string>(readonly str: S);
func("ABCD");

Здесь S преобразовано в тип "ABCD" , а не string !

Однако, если параметр str не был неизменным, компилятор не может гарантировать, что он не будет переназначен в теле функции, поэтому предполагаемый тип будет просто string .

function func<S extends string>(str: S) {
    str = "DCBA"; // This may happen
}

func("ABCD");

(3) Можно воспользоваться этим и изменить исходное предложение, чтобы спецификатор свойства был ссылкой на тип , который должен быть ограничен производной от string (в некоторых случаях может быть даже уместным ограничить его только одноэлементными буквальными типами, хотя в настоящее время на самом деле нет способа сделать это), а не сущностью времени выполнения:

function get<T extends object, S extends string>(obj: T, readonly propName: S): T[S]

Вызов этого не требует явного указания каких-либо аргументов типа, поскольку TypeScript поддерживает вывод аргументов типа:

let x = { name: "John", age: 42 };

get(x, "age"); // result type is inferred to be 'number'
// or for stronger type safety:
get(x, nameof(x.age)); // result type is inferred to be 'number'

_Редакты: исправлены некоторые орфографические и кодовые ошибки.
_Примечание: обобщенная и расширенная версия этого модифицированного подхода теперь также отслеживается в # 7730._

Вот еще одно использование типа свойства type (или «индексированные обобщения», как я люблю называть их в последнее время), которое обсуждалось с @Raynos

Мы уже можем написать следующую общую программу проверки массивов:

function tArray<T>(f:(t:any) => t is T) {
    return function (a:any): a is Array<T> {
        if (!Array.isArray(a)) return false;
        for (var k = 0; k < a.length; ++k)
            if (!f(a[k])) return false;
        return true;
    }
}

function tNumber(n:any): n is number {
    return typeof n === 'number'
}
var isArrayOfNumber = tArray(tNumber)

function test(x: {}) {
    if (isArrayOfNumber(x)) {
        return x[x.length - 1].toFixed(2); // this type checks
    }
}

Как только мы проиндексируем дженерики, мы также сможем написать общую проверку для объектов:

function tObject<p, T[p]>(checker: {...p: (t:any) => t is T[p]}) {
  return function(obj: any): obj is {...p: T[p] } {
    for (var key in checker) if (!checker[key](obj[key])) return false;
    return true;
  }
}

которые вместе с примитивными проверками для строк, чисел, логических, нулевых и неопределенных позволяют писать такие вещи:

var isTodoList = tObject({
  items: tArray(tObject({text: tString, completed: tBoolean})),
  showCompleted: tBoolean
})

и получить результат с правильным средством проверки времени выполнения _и_ охранником типа времени компиляции, в то же время :)

Кто-нибудь уже работал над этим, или это уже никого не интересует? Это станет значительным улучшением того, сколько стандартных библиотек могут использовать типизацию, включая подобные lodash или ramda и многие интерфейсы баз данных.

@malibuzios Я думаю, вы приближаетесь к предложению @Artazor :)

Чтобы решить ваши проблемы:

function func<S extends string>(readonly str: S): T[str] {
 ...
}

Это было бы

function func<S extends string, T[S]>(str: S):T[S] { }

Таким образом, имя будет привязано к наиболее конкретному типу (тип строковой константы) при вызове с помощью:

func("test")

Тип S становится "test" (а не string ). Таким образом, str нельзя переназначить другое значение. Если вы пробовали это, например

str = "other"

Компилятор может выдать ошибку (если нет проблем с устойчивостью дисперсии;))

Вместо того, чтобы просто получать тип одного свойства, я хотел бы иметь возможность получить произвольный супертип типа.

Поэтому я предлагаю добавить следующий синтаксис: вместо того, чтобы просто иметь T[prop] , я хотел бы добавить синтаксис T[...props] . Где props должен быть массивом элементов T . И результирующий тип является супертипом T с членами T определенными в props .

Думаю, это было бы очень полезно в Sequelize - популярном ORM для node.js. По соображениям безопасности и производительности целесообразно просто запрашивать атрибуты в таблице, которые вам нужно использовать. Это часто означает супер-тип.

interface IUser {
    id: string;
    name: string;
    email: string;
    createdAt: string;
    updatedAt: string;
    password: string;
    // ...
}

interface Options<T> {
     attributes: (memberof T)[];
}

interface Model<IInstance> {
     findOne(options: Options<IInstance>): IInstance[...options.attributes];
}

declare namespace DbContext {
   define<T>(): Model<T>;
}

const Users = DbContext.define<IUser>({
   id: { type: DbContext.STRING(50), allowNull: false },
   // ...
});

const user = Users.findOne({
    attributes: ['id', 'email', 'name'],
    where: {
        id: 1,
    }
});

user.id
user.email
user.name

user.password // error
user.createdAt // error
user.updatedAt // error

(В моем примере он включает оператор memberof , как вы и ожидаете, а также выражение options.attributes которое совпадает с typeof options.attributes , но я думаю, что Оператор typeof в этом случае является избыточным, поскольку он находится в позиции, которая ожидает тип.).

Если никто не настаивает, я начал над этим работать.

Что вы думаете о безопасности типов внутри функции, т.е. убедитесь, что оператор return возвращает что-то, что можно присвоить типу возврата?

interface A {
     a: string;
}
function f(p: string): A[p] {
    return 'aaa'; // This is string, but can we ensure it is the intended A[p] ?
}

Также имя "Тип свойства", используемое здесь, кажется немного неправильным. Это как бы утверждает, что тип имеет свойства, которые есть почти у всех типов.

А как насчет «Типа ссылки на свойство»?

Что вы думаете о безопасности типов внутри функции

Мой мозговой штурм:

let a: A;
function f(p: string): A[p] {
  let x = a[p]; // typeof A[p], only when:
  // 1. p is directly referencing function argument
  // 2. function return type is Property Reference Type

  p = "abc"; // not allowed to assign a new value when p is used on Property Reference Type

  return x; // x is A[p], so okay
}

И запретить нормальную строку на обратной линии.

Проблема с @malibuzios в вашей идее заключается в том, что она требует указания всех дженериков на случай, если их невозможно вывести, что может быть вирусным / загрязнять кодовую базу.

Любые комментарии от команды TS по https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -239653337?

@RyanCavanaugh @mhegazy и т. Д.?

Что касается названия, я начал называть эту функцию (по крайней мере, форму, предложенную @Artazor ) «Индексированные универсальные

Для этой проблемы могло быть решение под другим углом зрения. Я не уверен, что это уже принесено, это длинная ветка. Разработав типовое строковое предложение, мы могли бы расширить подпись индексации. Поскольку строковые литералы могут использоваться для типа индексатора, мы могли бы сделать их эквивалентными (насколько я знаю, на данный момент их нет):

interface A1 {
    a: number;
    b: boolean;
}
interface A2 {
    [index: "a"]: number;
    [index: "b"]: boolean;
}

Итак, мы могли бы просто написать тогда

declare function pluck<P, T extends { [indexer: P]: R; }, R>(obj: T, p: P): R;

Следует учитывать несколько моментов:

  • как указать, что P может быть только строковым литералом?

    • поскольку строка технически расширяет все строковые литералы, P extends string не будет очень идеальным

    • другое обсуждение идет о разрешении ограничения P super string (# 7265, # 6613, https://github.com/Microsoft/TypeScript/issues/6613#issuecomment-175314703)

    • нам действительно нужно заботиться об этом? мы можем использовать все, что приемлемо для типа индексатора - если T имеет индексатор string s или number s. тогда P может быть string или number .

  • в настоящее время, если мы передадим "something" в качестве второго аргумента, он будет иметь тип string

    • строковые литералы # 10195 могут быть ответом

    • или TS может сделать вывод, что следует использовать более конкретный P , если это приемлемо

  • индексаторы неявно необязательны { [i: string]: number /* | undefined */ }

    • как обеспечить соблюдение, если мы действительно не хотим иметь undefined в домене?

  • автоматический вывод типа для всех отношений между P , T и R - ключ к его работе

@weswigham @mhegazy , и я недавно это обсуждал; мы сообщим вам о любых разработках, с которыми мы столкнемся, и имейте в виду, что это всего лишь прототип идеи.

Текущие идеи:

  • Оператор keysof Foo для получения объединения имен свойств из Foo виде строковых литералов.
  • Тип Foo[K] который указывает, что для некоторого типа K который является типами строковых литералов или объединением типов строковых литералов.

Из этих базовых блоков, если вам нужно вывести строковый литерал как соответствующий тип, вы можете написать

function foo<T, K extends keysof T>(obj: T, key: K): T[K] {
    // ...
}

Здесь K будет подтипом keysof T что означает, что это тип строкового литерала или объединение типов строковых литералов. Все, что вы передаете для параметра key должно быть контекстно типизировано этим литералом / объединением литералов и выведено как одноточечный строковый литерал.

Например

interface HelloWorld { hello: any; world: any; }

function foo<K extends keysof HelloWorld>(key: K): K {
    return key;
}

// 'x' has type '"hello"'
let x = foo("hello");

Самая большая проблема заключается в том, что keysof часто нужно "отложить" свою операцию. Он слишком нетерпелив в том, как он оценивает тип, что является проблемой для параметров типа, как в первом опубликованном мною примере (то есть случай, который мы действительно хотим решить, на самом деле является сложной частью: smile :).

Надеюсь, что это даст вам все обновления.

@DanielRosenwasser Спасибо за обновление. Я только что видел, как @weswigham отправил PR об операторе keysof , так что, может быть, лучше передать эту проблему вам, ребята.

Мне просто интересно, почему вы решили отойти от исходного предложенного синтаксиса?

function get(prop: string): T[prop];

и ввести keysof ?

T[prop] менее общий и требует много машин с чересстрочной разверткой. Один большой вопрос заключается в том, как связать буквальное содержимое prop с именами свойств T . Я даже не совсем уверен, что ты будешь делать. Вы бы добавили параметр неявного типа? Вам нужно изменить поведение контекстной печати? Вам нужно добавить что-то особенное в подписи?

Ответ, вероятно, на все эти вопросы положительный. Я оттолкнул нас от этого, потому что мое чутье подсказывало мне, что лучше использовать две более простые, отдельные концепции и строить оттуда. Обратной стороной является то, что в некоторых случаях используется немного больше шаблонов.

Если есть более новые библиотеки, в которых используются такие шаблоны, и из-за этого шаблона им трудно писать на TypeScript, то, возможно, нам стоит это рассмотреть. Но в целом эта функция в первую очередь предназначена для обслуживания потребителей библиотеки, потому что сайт использования - это то, где вы в любом случае получаете преимущества.

@DanielRosenwasser Едва спустился в кроличью нору. Я все еще не могу найти проблем с реализацией идеи @SaschaNaz ? Я думаю, что keysof в этом случае излишне. T[p] уже указывает, что p должен быть одним из буквальных свойств T .

Моя грубая идея реализации заключалась в том, чтобы ввести новый тип под названием PropertyReferencedType .

export interface PropertyReferencedType extends Type {
        property: Symbol;
        targetType: ObjectType;
}

При вводе функции, объявленной с типом возвращаемого значения PropertyReferencedType или вводе функции, которая ссылается на PropertyReferencedType : Тип ElementAccessExpression будет дополнен свойством, которое ссылается на символ объекта доступа.

export interface Type {
        flags: TypeFlags;                // Flags
        /* <strong i="20">@internal</strong> */ id: number;      // Unique ID
        //...
        referencedProperty: Symbol; // referenced property
}

Таким образом, типу с указанным символом свойства можно присвоить PropertyReferencedType . Во время проверки referencedProperty должно соответствовать p в T[p] . Также родительский тип выражения доступа к элементу должен быть назначен T . И чтобы упростить задачу, p также должно быть const.

Новый тип PropertyReferencedType существует только внутри функции как «неразрешенный тип». На сайте вызова нужно разрешить тип с помощью p :

interface A { a: string }
declare function getProp(p: string): A[p]
getProp('a'); // string

PropertyReferencedType распространяется только через назначения функций и не может распространяться через выражения вызова, потому что PropertyReferencedType - это только временный тип, предназначенный для помощи в проверке тела функции с возвращаемым типом T[p] .

Если вы введете операторы типа keysof и T[K] , это будет означать, что мы можем использовать их следующим образом:

interface A {
  a: number;
  b: string;
}
type AK = keysof A; // "a" | "b"
type AV = A[AK]; // number | string ?
type AA = A["a"]; // number ?
type AB = A["b"]; // string ?
type AC = A["c"]; // error?
type AN = A[number]; // error?

type X1 = keysof { [index: string]: number; }; // string ?
type X2 = keysof { [index: string]: number; [index: number]: string; }; // string | number ?

@DanielRosenwasser не будет ли ваш пример иметь то же значение с моим

function foo<T, K extends keysof T>(obj: T, key: K): T[K] {
    // ...
}
// same as ?
function foo<K, V, T extends { [k: K]: V; }>(obj: T, key: K): V {
    // ...
}

Я не вижу, как будет написана подпись для _.pick Underscore:

o2 = _.pick(o1, 'p1', 'p2');

pick(Object, ...props: String[]) : WHAT GOES HERE;

@rtm Я предложил это в https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -234724380. Хотя может быть лучше открыть новый выпуск, даже если он связан с этим.

Реализация теперь доступна в # 11929.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги